Python descriptor

Introducing Descriptor#

Start with questions about descriptors on stackoverflow.

classCelsius:

 def __get__(self, instance, owner):return5*(instance.fahrenheit -32)/9

 def __set__(self, instance, value):
  instance.fahrenheit =32+9* value /5classTemperature:

 celsius =Celsius()

 def __init__(self, initial_f):
  self.fahrenheit = initial_f

t =Temperature(212)print(t.celsius)  #Output 100.0
t.celsius =0print(t.fahrenheit)  #Output 32.0

The above code realizes the automatic conversion between the temperature in Celsius and Fahrenheit. The Temperature class contains the instance variable fahrenheit and the class variable celsius. Celsius is represented by the descriptor Celsius. Three questions from this code:

  1. Question 1: What is a descriptor?
  2. Question 2: The parameters of the three methods of __get__, __set__, and __delete__
  3. Question 3: What are the application scenarios of the descriptor
  4. Question 4: What is the difference between property and descriptor?

Question 1: What is a descriptor?

A descriptor is a class object that implements one or more methods of __get__, __set__, and __delete__. When a class variable points to such a decorator, accessing this class variable will call the __get__ method, and assigning a value to this class variable will call the __set__ method. This class variable is called a descriptor.

The descriptor is actually a proxy mechanism: when a class variable is defined as a descriptor, operations on this class variable will be represented by this descriptor.

Question 2: The parameters of the three methods of the descriptor#

classdescriptor:
 def __get__(self, instance, owner):print(instance)print(owner)return'desc'

 def __set__(self, instance, value):print(instance)print(value)

 def __delete__(self, instance):print(instance)classA:
 a =descriptor()

del A().a  #Output<__main__.A object at 0x7f3fc867cbe0>A().a  #Return desc, output<__main__.A object at 0x7f3fc86741d0>,<class'__main__.A'>
A.a  #Return desc, output None,<class'__main__.A'>A().a =5  #Output<__main__.A object at 0x7f3fc86744a8>,5
A.a =5  #Modify the class variable of class A directly, that is, a is no longer proxied by the descriptor descriptor.

A conclusion can be drawn from the above output results:

Parameter explanation##

The essence of the three methods##

Question 3: What are the application scenarios of the descriptor#

We want to create a new form of instance attributes, in addition to modification and access, there are some additional functions, such as type checking, numerical verification, etc., we need to use the descriptor "Python Cookbook"

That is, the descriptor is mainly used to take over the operation of instance variables.

Implement classmethod decorator##

from functools import partial
from functools import wraps

classClassmethod():
 def __init__(self, fn):
  self.fn = fn

 def __get__(self, instance, owner):returnwraps(self.fn)(partial(self.fn, owner))

Fix the first parameter of the method fn to the class of the instance. You can refer to another way of writing in the official python documentation: descriptor

classClassMethod(object):
 def __init__(self, fn):
  self.fn = fn

 def __get__(self, instance, owner=None):if owner is None:
   owner =type(obj)
  def newfunc(*args):return self.f(owner,*args)return newfunc

Implement staticmethod decorator##

classStaticmethod:
 def __init__(self, fn):
  self.fn = fn

 def __get__(self, instance, cls):return self.fn

Implement property decorator##

classProperty:
 def __init__(self, fget, fset=None, fdel=None, doc=''):
  self.fget = fget
  self.fset = fset
  self.fdel = fdel
  self.doc = doc

 def __get__(self, instance ,owner):if instance is not None:return self.fget(instance)return self

 def __set__(self, instance, value):if not callable(self.fset):
   raise AttibuteError('cannot set')
  self.fset(instance, value)

 def __delete__(self, instance):if not callable(self.fdel):
   raise AttributeError('cannot delete')
  self.fdel(instance)

 def setter(self, fset):
  self.fset = fset
  return self

 def deleter(self, fdel):
  self.fdel = fdel
  return self

Use custom Property to describe farenheit and celsius class variables:

classTemperature:
 def __init__(self, cTemp):
  self.cTemp = cTemp  #There is an instance variable cTemp: celsius temperature

 def fget(self):return self.celsius *9/5+32

 def fset(self, value):
  self.celsius =(float(value)-32)*5/9

 def fdel(self):print('Farenhei cannot delete')

 farenheit =Property(fget, fset, fdel, doc='Farenheit temperature')

 def cget(self):return self.cTemp

 def cset(self, value):
  self.cTemp =float(value)

 def cdel(self):print('Celsius cannot delete')

 celsius =Property(cget, cset, cdel, doc='Celsius temperature')

Use result:

t =Temperature(0)
t.celsius  #Returns 0.0
del t.celsius  #Output Celsius cannot delete
t.celsius =5
t.farenheit  #Returns 41.0
t.farenheit =212
t.celsius  #Returns 100.0
del t.farenheit  #Output Farenhei cannot delete

Use decorators to decorate the two properties of Temperature, farenheit and celsius:

classTemperature:
 def __init__(self, cTemp):
  self.cTemp = cTemp

 @ Property  # celsius =Property(celsius)
 def celsius(self):return self.cTemp

 @ celsius.setter
 def celsius(self, value):
  self.cTemp = value

 @ celsius.deleter
 def celsius(self):print('Celsius cannot delete')

 @ Property  # farenheit =Property(farenheit)
 def farenheit(self):return self.celsius *9/5+32

 @ farenheit.setter
 def farenheit(self, value):
  self.celsius =(float(value)-32)*5/9

 @ farenheit.deleter
 def farenheit(self):print('Farenheit cannot delete')

Use the result to describe the class variable directly with the descriptor

Implement attribute type checking##

First implement a type checking descriptor Typed

classTyped:
 def __init__(self, name, expected_type):
  # Each attribute has a name and corresponding type
  self.name = name
  self.expected_type = expected_type

 def __get__(self, instance, cls):if instance is None:return self
  return instance.__dict__[self.name]

 def __set__(self, instance ,value):if not isinstance(value, self.expected_type):
   raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))
  instance.__dict__[self.name]= value

 def __delete__(self, instance):
  del instance.__dict__[self.name]

Then implement a Person class, the attributes name and age of the Person class are described by Typed

classPerson:
 name =Typed('name', str)
 age =Typed('age', int)

 def __init__(self, name: str, age: int):
  self.name = name
  self.age = age

Type checking process:

>>> Person.__dict__
mappingproxy({'__dict__':<attribute '__dict__'of'Person' objects>,'__doc__': None,'__init__':<function __main__.Person.__init__>,'__module__':'__main__','__weakref__':<attribute '__weakref__'of'Person' objects>,'age':<__main__.Typed at 0x7fe2f440bd68>,'name':<__main__.Typed at 0x7fe2f440bc88>})>>> p =Person('suncle',18)>>> p.__dict__
{' age':18,'name':'suncle'}>>> p =Person(18,'suncle')---------------------------------------------------------------------------
TypeError                                 Traceback(most recent call last)<ipython-input-88-ca4808b23f89>in<module>()---->1 p =Person(18,'suncle')<ipython-input-84-f876ec954895>in__init__(self, name, age)45     def __init__(self, name: str, age: int):---->6         self.name = name
  7   self.age = age

< ipython-input-83-ac59ba73c709>in__set__(self, instance, value)11     def __set__(self, instance ,value):12if not isinstance(value, self.expected_type):--->13             raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))14         instance.__dict__[self.name]= value
     15 

TypeError: Attribute name expected <class'str'>

However, there are some problems with the above-mentioned type checking method. The Person class may have many attributes, so each attribute needs to be described once with the Typed descriptor. We can write a class decorator with parameters to solve this problem:

def typeassert(**kwargs):
 def wrap(cls):for name, expected_type in kwargs.items():setattr(cls, name,Typed(name, expected_type))  #Classic writing
  return cls
 return wrap

Then redefine the Person class using the typeassert class decorator:

@ typeassert(name=str, age=int)classPerson:
 def __init__(self, name, age):
  self.name = name
  self.age = age

You can see that the parameter of the typeassert class decorator is the key-value pair of the passed-in attribute name and type.

If we want the typeassert class decorator to automatically recognize the initialization parameter type of the class, and add the corresponding class variables, we can use the inspect library and python type annotations to achieve:

import inspect
def typeassert(cls):
 params = inspect.signature(cls).parameters
 for name, param in params.items():if param.annotation != inspect._empty:setattr(cls, name,Typed(name, param.annotation))return cls

@ typeassert
classPerson:
 def __init__(self, name: str, age: int):  #Parameters without type annotations will not be managed
  self.name = name
  self.age = age

Question 4: The difference between property and descriptor#

We can use Python's internal mechanism to get and set attribute values. There are three methods:

  1. Getters and Setter. We can use methods to encapsulate each instance variable, get and set the value of the instance variable. In order to ensure that instance variables are not accessed externally, these instance variables can be defined as private. Therefore, access to the properties of the object requires an explicit function: anObject.setPrice(someValue); anObject.getValue().
  2. property. We can use the built-in property function to bind getter, setter (and deleter) functions to the property name. Therefore, the reference to the property looks as simple as direct access, but is essentially calling the corresponding function of the object. For example, anObject.price = someValue; anObject.value.
  3. Descriptor. We can bind getter, setter (and deleter) functions to a single class. Then, we assign objects of that class to the attribute name. At this time, the reference to each attribute is also like direct access, but in essence it calls the corresponding method of the descriptor object, for example, anObject.price = someValue; anObject.value.

The design patterns of Getter and Setter are not Pythonic enough. Although they are common in C++ and JAVA, Python pursues introduction and direct access.

Appendix 1, data-descriptor and no-data descriptor

Translated into Chinese is actually a data descriptor and a non-data descriptor

The difference between the two is:

classInt:
 def __get__(self, instance, cls):return3classA:
 val =Int()

 def __init__(self):
  self.__dict__['val']=5A().val  #Returns 5
classInt:
 def __get__(self, instance, cls):return3

 def __set__(self, instance, value):
  pass

classA:
 val =Int()

 def __init__(self):
  self.__dict__['val']=5A().val  #Returns 3

**Appendix 2. Descriptor mechanism analysis data: **

  1. Official document-descriptor
  2. understanding-get-and-set-and-python-descriptors
  3. anyisalin-Python-Descriptor
  4. Python Descriptor Guide (Translation)
  5. Properties and Descriptors

Recommended Posts

Python descriptor
Python CookBook
Python FAQ
Python3 module
python (you-get)
Python string
Python basics
Python basics 2
Python exec
Python notes
Python3 tuple
CentOS + Python3.6+
Python advanced (1)
Python decorator
Python IO
Python multithreading
Python toolchain
Python3 list
Python multitasking-coroutine
Python overview
python introduction
Python analytic
Python basics
07. Python3 functions
Python basics 3
Python multitasking-threads
Python functions
python sys.stdout
python operator
Python entry-3
Centos 7.5 python3.6
Python string
python queue Queue
Python basics 4
Python basics 5
Centos6 install Python2.7.13
Python answers questions
Python exit loop
Ubuntu16 upgrade Python3
Centos7 install Python 3.6.
ubuntu18.04 install python2
Python classic algorithm
Python2.7 [Installation Tutorial]
Python string manipulation
Python 3.9 is here!
python learning route
CentOS7 upgrade python3
Python3 basic syntax
Python review one
linux+ubuntu solve python
Functions in python
CentOS install Python 3.6
7 features of Python3.9
Python file operation
ubuntu12.04 install python3
Python design patterns
Python - centos6 installation
Centos7 install Python2.7
01. Introduction to Python
100 small Python examples
Python network programming