python描述器(descriptor)学习

1.什么是描述器/描述符(descriptor)?

在正式介绍描述符之前先来看一个场景,假设现在要录入个人信息——姓名、年龄和成绩。要求年龄和成绩必须是int数据。 熟悉 @property 用法的朋友很快就会用它来进行想到属性校验,具体实现如下:

class A:
    def __init__(self, name, score, age):
        self.name = name
        self._score = score
        self._age = age

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        print('setting score here')
        if isinstance(value, int):
            self._score = value
        else:
            print('score must be integer')

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        print('setting age here')
        if isinstance(value, int):
            self._age = value
        else:
            print('age must be integer')


a = A('Bob', 90, 20)
a.age = 'hello'		# age must be integer

但是我们可以看到,这里面有大量重复的代码。假如需要校验的属性有更多,那么需要写更多重复的代码。
要想使代码更简洁,我们用描述器 就可以实现。具体实现方式如下,或许现在你还不知道描述器是什么,但是你先有一个感性的认识:

class M:
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        print("get value")
        return self.value

    def __set__(self, instance, value):
        if isinstance(value, int):
            self.value = value
        else:
            print("value must be integer")


class A:
    age = M('age')
    score = M('score')

    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score


a = A('Bob', 20, 90)
print(a.age)	# 20
print(a.score)	# 90
a.age = 50
print(a.age)	# 50
a.score = 'good'	# value must be integer

从上面代码可以看到,我们使用了一个类M,里面有__get__()和__set()__方法,它可以对属性进行读写控制操作。
其实这就是一个描述器。

2.描述器的定义与实现

通过上面的例子,我们对描述器有了一个感性的认识,现在让我们从定义入手。
python官方文档中对描述器的定义是这样的:

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are get(), set(), and delete(). If any of those methods are defined for an object, it is said to be a descriptor.

总的来说,描述器是一个有绑定行为object attribute ,它的访问控制被描述符协议方法 重写。
在这里插入图片描述
简单来说,实现了以上方法中任意一个以上就成为一个描述器

我们来看一个简单的描述器实现:

"""
    descriptor 常规的实现方式
    __get()__、__set()__、__delete__()方法
"""


class M:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        print("__get__被调用")
        return self.name

    def __set__(self, instance, value):
        print("__set__被调用")
        self.name = value

    def __delete__(self, instance):
        print("__delete__被调用")
        del self.name


class A:
    m = M('Bob')


a = A()
print(a.m)		# __get__被调用 Bob
a.m = 'Amy'		# __set__被调用
print(a.m)		# __get__被调用 Amy
del a.m			# __delete__被调用

这里M是一个描述器类,里面实现了描述其协议中的三种方法。M的实例m就是一个描述器,在这里也是一个属性。当这个属性m被操作(读写)时,就会调用描述器中的方法。

除了上面这种方法来实现一个描述器之外,我们也可以借用property()来实现,具体实现方法如下:

"""
    descriptor利用property实现
    __fset()__、__fget()__、__fdel()__
"""


class M:
    def __init__(self, name):
        self._name = name

    def fget(self):
        print("getting name")
        return self._name

    def fset(self, name):
        print("setting name = ", name)
        self._name = name

    def fdel(self):
        print("delete name")
        del self._name

    name = property(fget, fset, fdel, "I am the name property")


x = M('Bob')	
print(x.name)	# getting name  Bob
x.name = 'Amy'	# setting name = Amy
print(x.name)	# getting name  Amy
del x.name		# delete name

我们可以看到,在这里name是一个描述器,具有描述器的效果
当然上面的代码进一步修改简化,可以利用**@property** 达到同样的效果:

"""
    descriptor利用property和语法糖实现,更加简洁
"""


class M:
    def __init__(self, name):
        """
            以一个下划线开头的实例变量名,比如_age,这样的实例变量外部是可以访问的,
            但是,按照约定俗成的规定,当看到这样的变量时,意思是,"虽然可以被访问,但是,
            请视为私有变量,不要随意访问"
        """
        self._name = name

    @property
    def name(self):
        print("getting name")
        return self._name

    @name.setter
    def name(self, name):
        print("setting name = ", name)
        self._name = name

    @name.deleter
    def name(self):
        print("delete name")
        del self._name


x = M('Bob')
print(x.name)	# getting name  Bob
x.name = 'Amy'	# setting name = Amy
print(x.name)	# getting name  Amy
del x.name		# delete name

3.描述器的分类

描述器根据它实现的描述协议方法不同可以分类两类:

  • data descriptor: 同时实现了__get__()方法和__set__()方法
  • non-data descriptor: 仅实现了__get__()方法
    我们先用一个示例来看看它们的区别:
""""
    实现data descriptor
"""

# 定义一个描述器类M,data descriptor
class M:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
        print('M的__get__被调用')
        return self.x

    def __set__(self, instance, value):
        print('M的__set__被调用')
        self.x = value+1

# 定义一个non-data descriptor类N
class N:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
        print('N的__get__被调用')
        return self.x

class A:
    m = M()
    n = N()

    def __init__(self, m, n):
        self.m = m  # 属性m与描述器m同名,这里调用了M的__set__()
        self.n = n  # 属性n与描述器n同名,这里没有调用描述器方法


a = A(5, 5)
print(a.m)		# 6
print("-----------------------")
print(a.n)		# 5

在这个示例中,m是一个data descriptor,n是一个non-data desrciptor, 在calss A中有两个与描述器同名的属性m和n,我们可以看到,对于属性m,它的取值是调用了data descriptor m中的__get__()方法的,因此它的输出为5+1=6 ;而对于属性n,它并没有调用non-data descriptor n中的__get__()方法,而是直接取到了值5

我们可以得出结论——当属性名和描述器名相同时,在访问这个同名属性时,如果是data descriptor就会先访问描述器,如果是non-data descriptor就会先访问属性。

同时我们可以看到,non-data descriptor中是没有__set__()方法的,因此它是一个read-only的描述器。

那如果我们需要实现一个read-only的data descriptor ,也很简单,只需要在__set__()方法中抛出异常即可,具体实现如下:

"""定义一个只读的data descriptor"""


class M:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
        print('__get__被调用')
        return self.x

    def __set__(self, instance, value):
        raise AttributeError("This is a read only descriptor")


class A:
    m = M()


a = A()
print(a.m)	# 1
a.m = 5		# This is a read only descriptor

4.描述器的应用

  • 描述器的一个常见的用途是在属性校验的时候,这个可以参考文章开头。
  • 描述器也可以用于实现许多其他的python方法。比如@property本质上就是一个描述器:
"""property的python等价实现"""
"""这个类可以等价达到@property的效果"""


class Property(object):
    """Emulate PyProperty_Type() in Objects/descrobject.c"""
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
  • @classmethod也是可以用描述器实现:(代码写在公司电脑上了,只能把ppt上的图搬过来了…凑合)
    在这里插入图片描述
  • 同理还有@staticmethod
    在这里插入图片描述

5.描述器的调用机制

所谓描述器的调用机制,就是说描述器在什么情况下会被谁调用。
在回答此问题之前我们需要先了解python中三种属性查找方法:

  • getattribute : 在实例访问属性时无条件调用;
  • getattr : 只有当getattribute抛出异常或显式调用getattr时才会调用;
  • get : 根据类访问还是实例访问决定是否调用。(这句话暂时可能不理解,但它其实就是描述器的调用,后面会有解释)
class M:
    a = 'abc'

    def __getattribute__(self, *args, **kwargs):
        print("__getattribute__() is called")
        # print(1, object.__getattribute__(self, *args, **kwargs))
        return object.__getattribute__(self, *args, **kwargs)

    def __getattr__(self, name):
        print("__getattr__() is called")
        return name + " from getattr"

    def __get__(self, instance, owner):
        print("__get__() is called", instance, owner)  # instance 是访问desciptor的实例
        return self

    def foo(self, x):
        print(x)

    def __call__(self, *args, **kwargs):
        print('__call__() is called', args, kwargs)


class C:
    d = M()


if __name__ == '__main__':
	"""
	m是M的一个实例,当m调用它的属性时(m.a),会首先调用__getattribute__方法
	如果无法找到的话就会接着调用__getattr__方法。
	"""
    m = M()
    print(m.a)  # __getattribute__() is called
    print("------------------------")
    print(m.x)  # __getattribute__() is called   __getattr__() is called   x from getattr
    print("------------------------")
    """
    c是C的一个实例,属性d是一个描述器。
    当c访问描述器属性时,会调用__get__方法
    注意:在python本来的情况下会先调用__getattribute__,然后根据一定的规则来判断是否调用__get__,但是这里因为重写了__getattribute__方法,所以原本的规则是已经失效了的。
	"""
    c = C()
    print(c.d)  # __get__() is called
    print("------------------------")
    print(c.x)  # AttributeError: 'C' object has no attribute 'x'

了解了属性查找的三个方法之后我们还需要来了解一下属性的查找顺序。所谓查找顺序就是会先找类还是先找实例?
关于这个问题我没有找到太好的demo来展示,只看到一段python内部__getattribute__方法的python实现版本(源码是c++),我们可以看一下:
代码源自

    def object_getattr(obj, name):
        'Look for attribute /name/ in object /obj/.'
        
        """首先在类/基类中查找"""
        v, cls = class_lookup(obj.__class__, name)
        
        """如果在类或基类中查到,且是一个描述器,则调用描述器__get__方法"""
        if (v is not None) and hasattr(v, '__get__') and hasattr(v, '__set__'):
            # Data descriptor.  Overrides instance member.
            return v.__get__(obj, cls)
        """然后才在实例中查找, 如果实例中能找到就直接返回"""
        w = obj.__dict__.get(name)
        if w is not None:
            # Found in object
            return w
        if v is not None:
            if hasattr(v, '__get__'):
                # Function-like descriptor.
                return v.__get__(obj, cls)
            else:
                # Normal data member in class
                return v
        raise AttributeError(obj.__class__, name)

在这段实现里,我们可以看到在属性查找的实际过程中的顺序是按照类–>基类–>实例 这样的顺序来找的。(这与我们通常想的不太一样,一般情况下我们都会觉得应该先从实例开始查找。为什么python要这样设计呢,其实与描述器的设计有关,也相当于可以说是因为描述器才把规则设计成这样,具体可以参看这里
同时我们看到另一点,当在实例中找到这个属性时,是没有判断它是否是一个描述器,而是直接返回值的,这就是上面分析三个属性查找方法时提到的__get__的调用。

根据以上两点,我们可以得出如下结论:
在这里插入图片描述
描述器的分享就到此结束啦!后面遇到或者有新的理解时候会再来补充。如有错误之处,请多指教~

部分参考资料如下:
https://docs.python.org/3/howto/descriptor.html
https://zhuanlan.zhihu.com/p/32764345
https://www.geeksforgeeks.org/descriptor-in-python/
由于还看了略看其他许多博客文章,不一一列举,自行搜索关键字”描述器“或”描述符“(后者能搜到更多结果~)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值