Python描述符

何为描述符以及为什么用描述符

描述符(descriptor)是定义了__get__()、__set__()和__del__()中一个或多个方法的类。

为何叫“描述”符呢?个人理解是描述符是一个辅助类,辅助对另一个类的属性进行“描述”。

举个例子:定义一个类是Person,那么其属性age不能是负数,“不能是负数”就是对age属性的描述。

首先想到的做法是在实例初始化函数__init__()中对age进行描述,但这种方法有个缺陷,就是如果对属性进行更改的话并不调用__init__(),那么”描述失效”了!

class Person:
    def __init__(self, age):
        if age < 0:
            raise ValueError("age should be a nonnegtive value")
        self.age = age


# p = Person(-2)  # 报错
p = Person(2)
p.age = -2  # 不报错

自然而然地,我们想到在类中定义一个方法,这个方法用来对属性进行附加描述。并且无论何时赋值或修改该属性时,该描述方法均被调用。Python引入@property很好地实现了这一点。@property是一个装饰器(decorator),其功能是修饰方法,使其能够像访问属性那样调用方法。这样,定义方法与属性名相同就实现目的了。

class Person:
    def __init__(self, age):
        self._age = None
        self.age = age

    @property
    def age(self):
        return self._age  # 避免无穷递归,引入辅助变量_age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("age should be a nonnegtive value")
        self._age = value


p = Person(2)
# p.age = -2 # 报错
p.age = 3

@property功能非常强大,可以满足大部分需求,但是如果需要描述的属性非常多,并且“描述内容”一致时,那么采用@property就显得臃肿,因为做了很多重复性的描述工作。例如,Person类的属性height,weight等同样需要描述其大于零。一个自然的想法就是,将这种”描述内容”独立出来,并组成一个NonNegtive类,用类来描述属性(属性也是对象,此时就相当于实例化NonNegtive类得到的实例作为属性),这个类就是描述符

class NonNegtive:
    def __init__(self):
        self.data = dict()  # 属性(需要描述的对象)为key,其值为value

    def __get__(self, instance, owner):
        return self.data[instance]

    def __set__(self, instance, value):  # 这里instance就是待赋值的属性
        if value < 0:
            raise ValueError("age should be a nonnegtive value")
        else:
            self.data[instance] = value


class Person:
    age = NonNegtive()  # 对属性的描述内容是class-level的,故应该作为类属性。相当于将属性包装了一下而已
    height = NonNegtive()
    weight = NonNegtive()

    def __init__(self, age, height, weight):
        self.age = age
        self.height = height
        self.weight = weight


#P = Person(-18, 170, 150) # 报错
p = Person(18, 170, 150)
# p.weight = -100 # 报错

补充

装饰器可以是高阶函数也可以是类。无论是哪一类装饰器,其初始化时传入的参数均为方法(函数)。查看Python源码可以看到property是一个类,并且在类中定义了__set__()、__get__()等方法,所以property本身就是一个描述符。
我们知道@property可以使一个方法可以像访问属性一样调用和赋值,其本质就是将函数的计算过程包装隐藏起来,下面这段代码摘自《精通Python设计模式》一书,感觉对描述符的运用非常的Pythonic,故拿来剖析一下:

class LazyProperty:

    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__
        # print('function overriden: {}'.format(self.method))
        # print("function's name: {}".format(self.method_name))

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        # print('value {}'.format(value))
        setattr(obj, self.method_name, value)
        return value


class Test:

    def __init__(self):
        self.x = 'foo'
        self.y = 'bar'
        self._resource = None

    @LazyProperty
    def resource(self):
        print('initializing self._resource which is: {}'.format(self._resource))
        self._resource = tuple(range(5))    # 假设代价大的计算
        return self._resource


t = Test()
print(t.x)
print(t.y)
# 做更多的事情。。。
print(t.resource)
print(t.resource)

输出结果:
foo
bar
initializing self._resource which is: None
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)

可以看到,计算代价大的代码块仅仅只运行了一次,并且是延迟计算,这就要归功于描述符了。resource是Test类中的一个方法,@LazyProperty将其作为参数传入LazyProperty类中,将其包装起来视为一个属性。第一次访问resources属性时,其没有值,故调用resource方法初始化,同时运用setattr方法将初始化结果赋予resource属性,即以方法名作为名称的属性。(该过程等同于为resource方法缓存计算结果)。第二次调用时直接返回属性值。

参考资料

解密 Python 的描述符(descriptor)

猜你喜欢

转载自blog.csdn.net/slx_share/article/details/80444055