python 描述符descriptor

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/duxiangwushirenfei/article/details/53364009

前言

之前被人问起描述符Descriptor,自己仅有一些模糊认知,此便详细梳理下这个神器。
演示环境Mac python3.5.2。

Descriptor

所谓描述符Descriptor是python的一个高级语法,python3 官网上给出详细介绍。
https://docs.python.org/3/howto/descriptor.html
我觉得总结起来,descriptor就是一个定义了__get__和__set__方法的对象。 这种用法似曾相识,就如同迭代器是定义了__iter__和__next__方法的对象,该对象就是一个可迭代对象,可以通过for…in…形式调用。那么它有什么用途?往后看。
官网中给出了一个规定,定义了__get__和__set__两个方法的descriptor称之为data descriptor,如果仅仅定义了__get__称之为non-data descriptor。二者是有区别的!!

假设我们现在有一个类Book用来记录图书,它有几个属性,分别是名称(name),订价(price),发行量(number),评分(mark)。

类基础知识

方法

在定义类时,通常会定义类的方法。其中一共大概分为四个类型:

class A(object):

    def func1(self, param):
        pass

    @staticmethod
    def func2(param):
        pass

    @classmethod
    def func3(cls, param):
        pass

    @property
    def func4(self):
        pass

1, func1普通的方法定义,第一个参数self为A的实例对象不需要调用者手动传入,param函数参数需传入。调用方法通过实例化类A得到a=A(),后a.func1(p)调用。
2, func2静态方法,此种方法不会有普通方法的默认self参数传入。调用可以通过类名点调用A.func2(p),也可以是a.func2(p)调用。
3, func3类方法,默认将类A传入给函数,调用只能通过实例化对象调用a.func3(p)。
4, func4属性方法,此种方法不能传入额外参数param,调用时通过a.func4调用。
在函数有默认参数传入时,self,cls只是人为去区分他们默认传入参数不同,self表示实例化对象,cls表示类,这里时形参,你若是喜欢可以通通叫default_param

属性

class A(object):
    p = 'class param'

    def __init__(self, s):
        self.obj = s

a = A('a')
b = A('b')
print('a:{}, b:{}'.format(a.obj, b.obj)) #结果 a:a, b:b
print('a:{}, b:{}'.format(a.p, b.p)) #结果 a:class param, b:class param
a.p = 'a param'
print('a:{}, b:{}'.format(a.p, b.p)) #结果 a:a param, b:class param

这里的属性obj是一个对象的属性,而属性p是一个类属性。obj是跟着对象走的只能通过对象调用,而p是跟着类走的,可以通过A.p调用,也可以self调用。

但是为什么给a.p赋值后,a.p变了,但b.p没变?我们后面解答。

除此之外有一些固有的类属性,这些属性可以通过dir()方法查看,此方法会把对象所能访问到的属性全部罗列出来。
多数情况,我们可以通过查看__dict__属性来查看对象有哪些属性,不妨自己调A.__dict__和a.__dict__看看结果。

完成Book类

由之前需求很容易完成Book类定义:

class Book(object):
    def __init__(self, name, price, number, mark):
        self.name = name
        self.price = price
        self.number = number
        self.mark = mark  

但是这样的定义不完备,因为常识性知道价格price应该为非负值,那么可以在__init__方法中限定,但是如果这样做,后期修改还是需要去判定。优化起来我们可以用利用property方法实现如下:

class Book(object):
    def __init__(self, name, price, number, mark):
        self.name = name
        self.price = price
        self.number = number
        self.mark = mark

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            ValueError('Price should be non-negative number.')
        else:
            self._price = value

b1 = Book('b', 10, 0, 0)
print(m.price) #结果10
b2 = Book('b', -10, 0, 0) #结果异常

这种就完成了price属性的非负限定。这个非常类似c++,java中的get,set方法,不过可以接受。但是发行量number,评分mark都需要非负,那么都得改造,如果有10个这样的非负属性呢?这个方法立马就显示出短板了。

扫描二维码关注公众号,回复: 3734863 查看本文章

描述符改写Book

class Descriptor(object):

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

    def __get__(self, instance, owner):
        print('__get__  instance: {}, owner: {}'.format(instance, owner))
        return self.value

    def __set__(self, instance, value):
        print('__set__  instance: {}, value: {}'.format(instance, value))
        if value < 0:
            raise ValueError('Price should be non-negative number.')
        self.value = value

class Book(object):
    price = Descriptor(0)
    number = Descriptor(0)
    mark = Descriptor(0)

    def __init__(self, name, price, number, mark):
        self.name = name
        self.price = price
        self.number = number
        self.mark = mark

实际运行结果:
这里写图片描述

可以看出,这里在新建Book类实例时调用了Descriptor对象的__set__方法,而获取price对象是调用了__get__方法。为什么是这种结果?我们必须要了解python中获取属性的搜寻顺序:
obj.attribute
1,如果attribute是python自动生成的属性,诸如__dict__,__class__和__module__ 等,那么直接获取。
2,倘若不是1,那么首先从type(obj).__dict__中去搜寻(其实就是父类的__dict__),如果attribute存在其key中,且对应value是一个data descriptor(之前提到过,后面详细说明),那么返回attribute的__get__方法调用结果。
3,前两者都不符合,那就搜寻obj.__dict__中是否有attribute,如果有则返回obj.__dict__中的value。注意哦,如果是@property装饰的属性方法,仍然适用,因为方法也是属性
4,若3也不符合,那么只能去type(obj).__dict__中去搜寻attribute为non-data descriptor的属性,或者直接是类属性。
5,以上都找不到,抛出属性不存在异常。

这下好了,有这么个搜寻顺序的理解,可以理解为什么上面的描述符改写后会是这个结果了吧?
此外,之前属性部分运行结果就好解释了:
1,第一次a.obj, b.obj调用a,b搜寻顺序1,2都不符合,且自己没有obj属性,3也不符合,那么去4父类中__dict__寻找找到了。因此结果相同都是 ‘class param’。
2,给a.obj赋值操作结束后,a的__dict__中就有了obj属性了,因此再往后调用a.obj就搜寻顺序第3个就满足了,而b.obj仍然要到搜寻书序4,所以结果不同。

data descriptor和 non-data descriptor区别

其实上面在介绍属性的搜寻顺序就已经告诉data descriptor和non-data descriptor的区别,就是data descriptor属性在同名属性存在不同位置时,搜寻顺序优先级更高。

non-data descriptor

先看non-data descriptor,还是用之前的Descriptor,不过注释__set__方法,运行如下:
这里写图片描述

看见没有这里的Descriptor是一个non-data descriptor,f实例开始访问获取的时non-data descriptor对象foo,就是类Foo的属性foo。但是在给f添加了属性foo后再f.foo调用,就按搜索顺序到3就获取了。并且可以看出f有属性foo,且 type (f)的属性也有foo。只因是non-data descriptor,优先级低。

data descriptor

将__set__方法解注释,运行结果如下:
这里写图片描述

由结果可见,还是f自身的属性foo和 type (f)的属性都有foo,但是实际调出的是type (f)的foo。只因是data descriptor,优先级高。

Book类改写方法二

除了上面提供的descriptor方法完成Book类的需求外,还有另外一种方法。
python类定义复写几个内置方法:
1,__getattribute__任何类实例在调用属性时无条件调用。
2,__getattr__在之前搜寻实例属性的过程中,前4步都没找到,就调用此方法。
3,__setattr__在给实例属性赋值时调用。
那么Book类可以再次改写如下:

class Book(object):

    p = 'class p'

    def __init__(self, name, price, number, mark):
        self.name = name
        self.price = price
        self.number = number
        self.mark = mark

    def __getattr__(self, item):
        print('__getattr__')
        if item not in self.__dict__.keys():
            raise AttributeError('No attribute {}.'.format(item))
        return self.item

    def __setattr__(self, key, value):
        print('__setattr__')
        value_valid_key = ['price', 'number', 'mark']
        if key in value_valid_key and value < 0:
            raise ValueError('key should be non-negative number.')
        self.__dict__[key] = value

print(Book.p) #结果'class p'
b1 = Book('b', 10, 0, 0) #调用了
print(b1.price) #结果__setattr__赋值
print(b1.p) #结果'class p'
print(b1.non) #调用__getattr__,抛出异常
b2 = Book('b', -10, 0, 0) #调用__setattr__赋值,到self.price = price出异常

猜你喜欢

转载自blog.csdn.net/duxiangwushirenfei/article/details/53364009