再次理解Python描述符

都说Python是优雅的语言,在我看来Python的语法和实现给人一种自然而然的感觉,比如容器的继承机制,迭代器和生成器,上下文管理,以及各种双下划线开头的特殊方法。了解Python的设计之后,就会觉得本该是这个样子,毫无违和感。唯一例外的是描述符(Descriptor),这玩意对Python而言可是举足轻重,但是从一般用户的角度去看就有点生硬,不知道为什么会存在这样的东西,特别是关于访问优先级,看上去就是纯粹的人为规则,一点都不自然,需要硬记。所以尝试重新理解了一下描述符并记录下来。

首先明确了,描述符的作用是用来代理另外一个类的属性,也就是提供了更细粒度的访问控制。当然,必须把描述符定义成类成员而非实例成员,这样的设计可能和该特性的具体实现有关,在没搞清楚之前就当是个死规定。我们知道通常而言类的成员可被全部实例所共享,每个实例自己还可以有一个同名的成员覆盖类成员。但是在存取上无法实现细节的控制,比如最简单的一个类:

class Person:

    def __init__(self, name):
        if not isinstance(name, str):
            raise TypeError("`name` should be a string")

        self.name = name

xiao_ming = Person("Xiao Ming")
xiao_ming.name = 123

为了避免name被赋值为非字符串,只能在构造函数内检测,如果有多个类似的成员变量,那么就要在构造函数里写上一堆东西,何况还无法阻止实例化以后再改变name的值。有了描述符就好办多了,若有个类属性是数据描述符对象,此时会优先调用描述符中定义的方法:

class Name(object):
    dic = {}

    def __init__(self, value_type):
        self.value_type = value_type

    def __get__(self, instance, owner):
        print("get name")
        return self.dic.get(instance, None)

    def __set__(self, instance, value):
        if not isinstance(value, self.value_type):
            print("failed to set name")
        else:
            print("set name")
            self.dic[instance] = value

class Person(object):
    name = Name(str)

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

xiao_ming = Person("Xiao Ming")
print(xiao_ming.name)
xiao_ming.name = 123

#====== Output =======
#
# set name
# get name
# Xiao Ming
# failed to set name

这样看来,我们把访问一个成员的细节抽离了出来,况且这个Name类还可以重用,比如取名为StringClass,更加一目了然。描述符类以及调用它的类都应该写成继承object的新式类。若描述符类仅仅定义了__get__而没有__set__和__delete__则是个非数据描述符,否则是数据描述符。接下来是访问优先级,网上有大量的说法是这样的:

1. 类属性
2. 数据描述符
3. 实例属性
4. 非数据描述符
5. 找不到的属性触发__getattr__()

个人认为把类属性排在第一位是有误导性的。从类的角度看,对类属性直接赋值,确实可以替换掉描述符,毕竟描述符本质上也是个类属性,那么通过类名来给某个属性赋值,理所当然应该覆盖之前的值(描述符对象),不然就违背了Python的语义。但是访问实例的成员,并不会首先去访问普通的类属性,当然,除非它是个数据描述符。后来看到另一段描述,显得靠谱多了:

如果user是某个类的实例,那么 user.age 以及等价的 getattr(user,’age’),首先调用__getattribute__。
如果类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,
而对于描述符__get__的调用,则是发生在__getattribute__内部。

user = User(), 那么user.age 顺序如下:

(1)如果“age”是出现在User或其基类的__dict__中, 且age是data descriptor, 那么调用其__get__方法, 否则

(2)如果“age”出现在user的__dict__中, 那么直接返回 obj.__dict__[‘age’], 否则

(3)如果“age”出现在User或其基类的__dict__中

(3.1)如果age是non-data descriptor,那么调用其__get__方法, 否则

(3.2)返回 __dict__[‘age’]

(4)如果User有__getattr__方法,调用__getattr__方法,否则

(5)抛出AttributeError

数据描述符必须优先于普通实例的成员,这个好理解,不然还提什么访问控制呢,但是为什么仅有__get__方法的非数据描述符就排后面去了呢。简单的想想,既然只有getter没有setter,那么遇到最简单的 instance.attribute = value 这样的语句要怎么办呢,既然非数据描述符无法处理,那么只能把它当成一个普通的实例成员来看待了,为保持一致,执行 print(instance.attribute) 也没必要走非数据描述符的__get__方法了,这么想似乎说得通了。那么非数据描述符还有什么作用呢,我认为可以用于延迟计算,根据传入的实例的成员来做一些新的计算,或者隐藏某些私有成员,若有必要可以将计算好的值重新赋予实例成员,这样下次访问就无须重复计算,因为实例成员的优先级更高。当然如果不用非数据描述符,我们还可以通过定义方法来达到同样目的,只不过 instance.attribute 用起来肯定比 instance.attribute() 要更舒服吧。可能有人会说,这不就是Python中的 property 吗,还真没错,property 底层正是用描述符实现的。

https://github.com/mzohaibqc/python-attribute-lookup 这个地方有两张很棒的流程图,一个是实例成员的查找顺序,一个是类成员的查找顺序,一目了然:

Object Attribute Lookup

Class Attribute Lookup

有了描述符,便可以实现 property,不过都是借用装饰器语法糖而已:

class Property(object):
    """Uused as a class decorator which returns a descriptor object."""
    def __init__(self, func):
        self.get_func = func
        self.set_func = None
        self.delete_func = None

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.get_func(instance)

    def setter(self, func):
        self.set_func = func
        return self

    def __set__(self, instance, value):
        return self.set_func(instance, value)

    def deleter(self, func):
        self.delete_func = func
        return self

    def __delete__(self, instance):
        return self.delete_func(instance)


class Person(object):
    def __init__(self, name):
        self.__name = name

    @Property  # name = Property(name)
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("`Name` must be a string")
        self.__name = value

    @name.deleter
    def name(self):
        print("Cannot delete `name`")

xiao_ming = Person("Xiao Ming")
print(xiao_ming.name)

xiao_ming.name = 'Da Ming'
print(xiao_ming.name)

del xiao_ming.name

同样可以实现 classmethod 和 staticmethod:

class ClassMethod:
    def __init__(self, func):
        self.get_func = func
        self.cls = None

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

    def __call__(self, *args, **kwargs):
        return self.get_func(self.cls, *args, **kwargs)

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

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

    def __call__(self, *args, **kwargs):
        return self.get_func(*args, **kwargs)

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

    def eat(self, sth):
        print("{} is eating {}".format(self.name, sth))

    @ClassMethod
    def friendship(cls, sth):
        print("{} and {} are friends".format(cls.__name__, sth))

    @StaticMethod
    def live(sth):
        print("Everyone lives on {}".format(sth))
        
xiao_ming = Person("Xiao Ming")
xiao_ming.eat("rice")
xiao_ming.friendship("dog")
xiao_ming.live("earth")

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值