python描述器深度解析

写在篇前

  在之前的博客Python面向对象、魔法方法中曾简单提到魔法方法__get____set____delete__,但只给出一个例子,这篇文章将对它做一个更详细的总结,因为这三个魔法函数表征着一个专业术语描述器

什么是描述器

  简而言之,如果一个类中定义了__get____set____delete__中的任意一个,这个类的实例就可以叫做一个描述器,其功能强大,应用广泛,它可以控制访问属性、方法的行为,是属性、方法、静态方法、类方法、super函数背后的实现机制。

  首先看一个例子,说明什么是描述器:

# 例一
class A():
    """
    描述器类
    该描述器的实现改变了 name属性 默认的存取行为
    这种类是当做工具使用的,不单独使用
    """
    def __init__(self):
        print('descriptor init')
        self.name = 'descriptor'

    def __get__(self, instance, owner):
        print('descriptor get')
        return self.name

    def __set__(self, instance, value):
      if isinstance(value, str):
        self.name = value
        return
      raise TypeError('name must be string type')


class B(object):
    name = A()

    def __init__(self):
        self.name = 'common instance'

        
>>> b = B()
>>> b.name
>>> b.name = 'jeffery'

# 输出
descriptor init
descriptor get
common instance

  上面代码中,A类实现了__get____set__方法,因此是一个描述器类,B类的类属性name就是一个描述器。这里我们首先需要知道程序是如何访问到name描述器的?以上述代码中b对象为例,类属性的访问过程是:

  1. 查找b.__dict__里面是否存在name
  2. 查找type(b).__dict__里面是否存在name
  3. 查找super(b)中是否存在属性name
  4. 若存在,返回值;若都不存在,抛出异常

  此时,不知道你有没有疑问,为什么最后得到的值是类属性的值?猜想一下的话,最可能的就是描述器覆盖了实例属性。但是,是不是所有描述器都会’覆盖’实例属性呢?答案是否定的,因为描述器分为以下两类:

  • 资料描述器

    同时定义了__get____set__方法的描述器

  • 非资料描述器

    只定义了__get__的描述器

另外,还有一种称为 只读描述器,其实现__get____set__,但是__set__函数抛出AttributeError,但是这同时也是一个资料描述器。

  它们存在潜在优先级关系:资料描述器 > 实例属性 > 非资料描述器;实例属性 > 类属性,请看以下实例:

# 例子二
class A:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
      """
      以下两个参数都是「必须参数」,约定使用
      instance: 描述器所在类的实例
      owner:调用描述器的类
      """
        return self.x

    def __set__(self, instance, value):
      """
      以下两个参数都是「必须参数」,约定使用
      instance: 描述器所在类的实例
      value:用来设置属性的值
      """
        self.x = value
        
    def __delete__(self, instance):
      """
      以下参数是「必须参数」,约定使用
      instance: 描述器所在类的实例
      """
      pass


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

    def __get__(self, instance, owner):
        return self.x


class C:
    a = A()
    b = B()

    def __init__(self, a, b):
        self.b = a
        self.b = b

  上面在class C中,分别定义了资料描述器和非资料描述器a,b以及同名的实例属性,进行以下输出测试,发现实例c只存在实例属性b,不存在实例属性a,说明其确实是被资料描述器,即类属性a给屏蔽了,从而证实了优先级关系:资料描述器 > 实例属性 > 非资料描述器。

>>> c = C(7,8)
>>> c.__dict__
{'b': 8}

  关于资料描述器、非资料描述器的调用,可用如下方式:

# 资料描述器
>>> c.a
1
>>> type(c).__dict__['a'].__get__(c,C)
1

# 非资料描述器
>>> C.b
1
>>> type(c).__dict__['b'].__get__(c,C)
1

描述器调用原理

  为了探究描述器的底层调用原理,我们借用例一中定义的class Aclass B,实际上如果我们直接初始化一个class A对象,那么其调用方式很显然是:

>>> a = A()
>>> a.__get__(None,None)

  那class B实例中的描述器是如何调用的呢?实际上,python是通过__getattribute__()函数来实现的,但它的使用C语言实现的,如果用python来写的话,类似于下面的代码:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

  也就是说,当在字典中找到一个值之后,会判断他是不是一个描述器,如果是的话,就调用__get__方法,从而实现描述器的访问功能。如果你重写__getattribute__()函数,那么就可以改变对描述器访问的行为,如下面例子所示:

class A():
    def __init__(self):
        self.name = 'descriptor'

    def __get__(self, instance, owner):
        return self.name

    def __set__(self, instance, value):
        raise TypeError('name must be string type')


class B(object):
    name = A()

    def __getattribute__(self, item):
        if item in type(self).__dict__:
            if hasattr(type(self).__dict__[item], '__get__'):
                print('oh, be careful, it is a descriptor')


if __name__ == '__main__':
    b = B()
    b.name
    
# 输出
oh, be careful, it is a descriptor

描述器实例

  在我的上一篇博客python装饰器详细剖析中,我举例分析了实例方法、类方法、静态方法、抽象方法的差异,这里我们进行深度挖掘一下其实现原理:

首先定义一个类ICU996,用于相关测试:

import abc


class ICU996():

    @staticmethod
    def static_m():
        print('static_m')

    @classmethod
    def class_m(cls):
        print('class_m')

    def instance_m(self):
        print('instance_m')

    @abc.abstractmethod
    def abstract_m(self):
        print('abstract_m')

  下面代码分别使用属性访问和字典访问的方式,来看看各类函数的差异。可以看出,实例对象直接调用时,类方法、抽象方法、实例方法都是bound method,而静态方法是function。值得注意的是,静态方法和类方法用字典调用得到的其实分别是staticmethod和classmethod两个类的对象,这两个类其实是定义描述器的类,所以用字典访问的两个方法得到的都是描述器对象。

icu = ICU996()

print(icu.static_m)
print(ICU996.__dict__['static_m'])

print(icu.class_m)
print(ICU996.__dict__['class_m'])

print(icu.instance_m)
print(ICU996.__dict__['instance_m'])

print(icu.abstract_m)
print(ICU996.__dict__['abstract_m'])

# 输出
<function ICU996.static_m at 0x11a9cff28>
<staticmethod object at 0x10edcd208>

<bound method ICU996.class_m of <class '__main__.ICU996'>>
<classmethod object at 0x10edcd320>

<bound method ICU996.instance_m of <__main__.ICU996 object at 0x10edcd2b0>>
<function ICU996.instance_m at 0x11a9f60d0>

<bound method ICU996.abstract_m of <__main__.ICU996 object at 0x10edcd2b0>>
<function ICU996.abstract_m at 0x11a9f6158>

  既然它们是描述器对象,那么调用就可以使用__get__函数来实现;另外,实际上其他函数也都是通过描述器实现的(通过非资料描述器实现的),只不过其他方法应该传入一个self参数,不妨试一试:

ICU996.__dict__['class_m'].__get__(None, ICU996)
ICU996.__dict__['static_m'].__get__(None, ICU996)

ICU996.__dict__['instance_m'].__get__(icu, ICU996)
ICU996.__dict__['abstract_m'].__get__(icu, ICU996)

property

  因为property的实现是用C语言实现的,我们可以看一个等效的python实现,大致是这样的:

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
        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__)

  举个使用的示例,通过property类来实现属性代理,其效果与@property示例等效,但是重点是要去思考,为什么等效?

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

    def del_score(self):
        del self._score

    score = property(get_score, set_score, del_score, 'doc string')

  接着上面的问题,为什么等效?是怎么调用的,思考一下描述器原理,不难想出调用的过程就是:

>>> s = Student()
>>> s.score = 10
>>> type(s).__dict__['score'].__get__(s, type(s))

Functions&methods

  Python的面向对象特征是建立在基于函数的环境之上的,非资料描述器把两者无缝地连接起来。为了支持方法调用,函数包含一个 __get__() 方法以便在属性访问时绑定方法。这就是说所有的函数都是非资料描述器,它们返回绑定(bound)还是非绑定(unbound)的方法取决于他们是被实例调用还是被类调用。借用上文定义的class ICU996,调用一个方法的实际过程是:

>>> icu = ICU996()
>>> ICU996.__dict__['instance_m'].__get__(icu, ICU996)()

  因此可以猜测到,__get__()函数的实现方式如下面代码所描述。但是可能依旧会有疑问,就算真的存在这样一个描述器,我们定义上面的instance_m方法并没有像使用property描述器一样显示调用呀?怎么就起作用了呢?原因很简单,隐式的调用了!那在哪里调用了呢?我的猜想是,一切皆为对象,函数也是对象,那么函数也就必然属于一个类,比如他叫Function,Function类实现了如下的代码,也就是函数就是一个描述器,因此通过以下__get__方法实现方法到对象的绑定。

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        return types.MethodType(self, obj, objtype)

staticmethod&classmethod

  其实类似的,通过实现__get__方法,使其变成一个描述器,下面分别实现以下classmethod和staticmethod:

class myclassmethod(object):
    def __init__(self, method):
        self.method = method
        
    def __get__(self, instance, cls):
        return lambda *args, **kw: self.method(cls, *args, **kw)
class myclassmethod(object):
    def __init__(self, method):
        self.method = method
        
    def __get__(self, instance, cls):
        return self.method

小结

  一般方法、静态方法、类方法的调用转换可以归结为下表:

TransformationCalled from an ObjectCalled from a Class
functionf(obj, *args)f(*args)
staticmethodf(*args)f(*args)
classmethodf(type(obj), *args)f(klass, *args)

参考来源

descriptor HowTo

location of classmethod

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值