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

参考资料

  • Python3 元编程 PyCon David beazley
  • Python Descriptor How to Guide

原文发布于微信公众号 - Python爬虫与算法进阶(zhangslob)

原文发表时间:2019-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券