首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python - 描述器

Python - 描述器

作者头像
小歪
发布2019-05-14 10:32:22
8540
发布2019-05-14 10:32:22
举报

很多时候我们可能需要对某个实例的属性加上除了修改、访问之外的其他处理逻辑,例如 类型检查、数值校验等,就需要用到描述器 ---《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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python爬虫与算法进阶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 描述器
  • getattribute
  • data-descriptor and no-data descriptor
  • 一些例子
    • 实现类型检查
      • property 的实现
        • staticmethod 实现
          • classmethod 实现
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档