Python - 描述器

很多时候我们可能需要对某个实例的属性加上除了修改、访问之外的其他处理逻辑,例如 类型检查、数值校验等,就需要用到描述器 《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 为实例

下面这个例子列出了不同情况下 instancecls 的值

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'>

getattribute

描述器的 __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 !!!!!

data-descriptor and no-data 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 的实现

虽然 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)

staticmethod 实现

class StaticMethod:
def __init__(self, func):
self.func = func

def __get__(self, instance, cls=None):
return self.func

classmethod 实现

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

参考资料