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:
__get__
, __set__
, and __delete__
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.
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:
__ get__(self, instance, owner)
instance represents the current instance owner represents the class itself, when using class access, instance is None__ set__(self, instance, value)
instance represents the current instance, value is the right value, only the instance will call __set__
__ delete__(self, instance)
instance represents the current instanceinstance.descriptor
actually calls the descriptor.__get__(self, instance, owner)
method and needs to return a valueinstance.descriptor = value
actually calls the descriptor.__set__(self, instance, value)
method, and the return value is None.del instance.descriptor
actually calls the descriptor.__delete__(self, obj_instance)
method, and the return value is NoneWe 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.
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
classStaticmethod:
def __init__(self, fn):
self.fn = fn
def __get__(self, instance, cls):return self.fn
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
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
We can use Python's internal mechanism to get and set attribute values. There are three methods:
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
__get__
and __set__
methods__get__
methodThe difference between the two is:
instance.__dict__
classInt:
def __get__(self, instance, cls):return3classA:
val =Int()
def __init__(self):
self.__dict__['val']=5A().val #Returns 5
instance.__dict__
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: **
Recommended Posts