很多时候我们可能需要对某个实例的属性加上除了修改、访问之外的其他处理逻辑,例如 类型检查、数值校验等,就需要用到描述器 ---《Python Cookbook》
我们可以使用 Python 自带的 property
装饰器 来控制属性的访问,下面这个例子通过 property
控制了 Person 的 age 属性的访问和修改
class Person:
def __init__(self, name=None, age=None):
self.name = name
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise AttributeError('Must be {}'.format(int))
if value > 200:
raise AttributeError('Value Must < 200')
self._age = value
试一试,的确如代码写的一样,对属性的类型进行了检查,而且使用了 property
装饰器之后,对 age 方法的访问和对属性的访问一样,不需要加 ()
>>> a = Person()
>>> a.age
>>> a.age = 10
>>> a.age
10
>>> a.age = 'a'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "person.py", line 14, in age
raise AttributeError('Must be {}'.format(int))
AttributeError: Must be <class 'int'>
>>> a.age = 201
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "person.py", line 16, in age
raise AttributeError('Value Must < 200')
AttributeError: Value Must < 200
那么 property
是怎么实现的呢,这就要说到本文的主题 描述器了
Python 有三个特殊方法,__get__
、__set__
、__delete__
,用于覆盖属性的一些默认行为,如果一个类定义了其中一个方法,那么它的实例就是描述器
下面是一个简单的描述器的示例,Descriptor 是一个实现了 __get__
、__set__
的类,可以为其实例访问和修改时打印信息
class Descriptor:
def __init__(self, initvar=None, name='var'):
self.initvar = initvar
self.name = name
def __get__(self, instance, cls):
print('Get', self.name)
return self.initvar
def __set__(self, instance, value):
print('Set', self.name, value)
self.initvar = value
class E:
a = Descriptor(10, 'a')
b = Descriptor(20, 'b')
>>> e = E()
>>> e.a
Get a
10
>>> e.b
Get b
20
>>> e.b = 10
Set b 10
>>> e.b = 30
Set b 30
描述器是一种代理机制,对属性的操作由这个描述器来代理
访问: __get__(self, instance, cls) # instance 代表实例本身,cls 表示类本身,使用类直接访问时,instance 为 None
赋值: __set__(self, instance, value) # instance 为实例,value 为值
删除: __delete__(self, instance) # instance 为实例
下面这个例子列出了不同情况下 instance
和 cls
的值
class TestDescriptor:
def __get__(self, instance, cls):
print('instance', instance)
print('class', cls)
def __set__(self, instance, value):
print(instance)
def __delete__(self, instance):
print(instance)
class F:
f = TestDescriptor()
>>> f = F()
>>> f.f
instance <__main__.F object at 0x10ff2fa20>
class <class '__main__.F'>
>>> f.f = 'c'
<__main__.F object at 0x10ff2fa20>
>>> del f.f
<__main__.F object at 0x10ff2fa20>
>>> F.f
instance None
class <class '__main__.F'>
描述器的 __get__
方法 是通过 __getattribute__
调用的,实际上,Python 中访问实例属性时,__getattribute__
就会被调用,__getattribute__
会查找整个继承链,直到找到属性,如果没有找到属性,但是定义了 __getattr__
,那么就会调用 __getattr__
去查找属性,否则抛出 AttributeError
__getattribute__
的代码用 Python 实现如下
def __getattribute__(self, key):
val = super().__getattribute__(key)
if hasattr(val, '__get__'):
return val.__get__(None, self)
return val
可以做个测试,重写 __getattribute__
class Descriptor:
def __init__(self, name=None):
self.name = name
def __get__(self, instance, cls):
return self.name
def __set__(self, instance, value):
self.name = value
class C:
d = Descriptor('d')
def __getattribute__(self, key):
if key == 'd':
val = self.__class__.__dict__['d']
else:
val = super().__getattribute__(key)
if hasattr(val, '__get__'):
raise AttributeError('NO DESCRIPTOR !!!!!')
return val
访问描述器被 __getattribute__
拦截了
>>> c.d
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-3-c1e2befe291e> in <module>()
----> 1 c.d
<ipython-input-1-1c75c3b76140> in __getattribute__(self, key)
20 val = super().__getattribute__(key)
21 if hasattr(val, '__get__'):
---> 22 raise AttributeError('NO DESCRIPTOR !!!!!')
23 return val
24
AttributeError: NO DESCRIPTOR !!!!!
如果一个实例只定义了 __get__
那么,它就是一个非资料描述器 no-data descriptor
,如果同时定义了 __get__
和 __set__
那么就是资料描述器 data descriptor
它们的区别在于,如果实例字典中有与描述器同名的属性,如果是资料描述器,则优先使用资料描述器,否则使用实例字典中的属性
class AbsPriorityDescriptor:
def __init__(self, name=None):
self.name = name
def __get__(self, instance, cls):
return self.name
def __set__(self, instance, value):
self.name = value
class NoPriorityDescriptor:
def __init__(self, name=None):
self.name = name
def __get__(self, instance, cls):
return self.name
class C:
a = AbsPriorityDescriptor('a')
b = NoPriorityDescriptor('b')
测试,可以看出来,资料描述器 a
忽略了实例字典的值,而非资料描述器则被覆盖
>>> c = C()
>>> c.a
'a'
>>> c.__dict__['a']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'a'
>>> type(c).__dict__['a']
<__main__.AbsPriorityDescriptor object at 0x1091336d8>
>>> c.__dict__['a'] = 'ccccc'
>>> c.a
'a'
>>> c.b
'b'
>>> c.__dict__['b']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'b'
>>> c.__dict__['b'] = 'cccc'
>>> c.b
'cccc'
class Descriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class Typed(Descriptor):
ty = object
def __set__(self, instance, value):
if not isinstance(value, self.ty):
raise AttributeError('Must be {}'.format(self.ty))
super().__set__(instance, value)
class Integer(Typed):
ty = int
class Float(Typed):
ty = float
class String(Typed):
ty = str
class Boolean(Typed):
ty = bool
class Person:
name = String('name')
age = Integer('age')
测试
>>> c = Person()
>>> c.name = 1
# ignore error
AttributeError: Must be <class 'str'>
>>> c.name = 'aaaa'
>>> c.age = 'aaa'
# ignore error
AttributeError: Must be <class 'int'>
>>> c.age = 18
虽然 property
是 C 代码实现的,但是我们可以模拟出 Python 的 Property
class Property:
def __init__(self, fget, fset=None, fdel=None): #no defined fdoc
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, instance, cls):
return self.fget(instance)
def __set__(self, instance, value):
if self.fset is None:
raise AttributeError('can not set')
self.fset(instance, value)
def __delete__(self, instance):
if self.fdel is None:
raise AttributeError('can not delete')
self.fdel(instance)
def setter(self, fset):
self.fset = fset
return self
def deleter(self, fdel):
self.fdel = fdel
return self
使用自定义的 Property
class A:
def geta(self):
return self._a
def seta(self, value):
self._a = value
def dela(self):
del self._a
a = Property(fget=geta, fset=seta, fdel=dela)
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls=None):
return self.func
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls=None):
if cls is None:
cls = type(instance)
def new_func(*args, **kwargs):
return self.func(cls, *args, **kwargs)
return new_func
参考资料
本文分享自 Python爬虫与算法进阶 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!