7

descriptor

描述符

描述符协议可以自定义一些东西当引用一个对象的属性的时候,描述符协议是python复杂属性获取的基础,被用来实现属性,方法,类方法,静态方法和super类型。描述符是类,并且定义了其他类的属性是如何被接触的。换句话说,一个类可以授权属性的管理权给另外一个类。

描述符类是基于三个特殊的方法,这些方法形成了描述符协议:

- __set__(self, obj, type=None),和PHP的魔术方法类似,当一个属性被设置的时候,调用该方法。

- __get__(self, obj, value) 属性被read的时候。

- __delete__(self, obj)del操作符被用在某个属性上的时候。

实现了__get__()__set__()方法的描述符被称为数据描述符,如果只实现了__get__(),那么则被称为非数据描述符.这些协议的方法会被对象的特殊的方法__getattribute__() 调用当每次属性查找的时候。无论是使用instance.attribute还是使用getattr(instance, 'attribute')形式调用,__getattribute__()方法都会被隐式调用。

属性查找的顺序:

  1. 检查属性是否是一个数据描述符
  2. 如果不是,则在实例对象的__dict__属性中查找。
  3. 最后再查看属性是否是类对象的一个非数据描述符。 优先级顺序为:数据描述符 > dict 查询 > 非数据描述符

示例

#python2.7
class RevealAccess(object):
    def __init__(self, inival=None, name='var'):
        self.val = inival
        self.name = name

    def __get__(self, obj, objtype):
        print 'Retrieving', self.name
        return self.val

    def __set__(self, obj, val):
        print 'Updating', self.name
        self.val = val

class MyClass(object):
    x = RevealAccess(100, 'var x')
    z = RevealAccess('hello', 'var z')
    y = 5

>>>m = MyClass()
>>>print m.x  #100
>>>print m.z #hello
3

尽管看起来,描述符协议好像是一个类对象,但是实际上它是一个基于类实现的数据描述符,用来给一个属性进行描述。Python使用描述符协议绑定类函数到一个实例上作为实例方法,当然数据描述符也提供classmethodstaticmethod描述符。事实上,函数对象也是非数据描述符。如果没有__dict__优先于非数据描述符,那么再运行时刻重写一个已经构造出来的对象的特定方法,将不会成功。所以由于描述符协议的存在,使得被称作monkey-patching的技术得以实现。

数据描述符如果再一个类对象内部,那么描述符才会起作用,否则,只是会当作一个普通的类对象。

@property

如果相对实例属性进行一些额外的操作,如类型检查,验证等等,那么可以使用@property

#python2.7
class Person(object):
    def __init__(self, first_name):
        self.first_name = first_name

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected string!')
        self._first_name = value

    @first_name.deleter
    def first_name(self):
        raise AttributeError('cant delete attribute!')

在上面的例子中,再__init__方法中,用first_name来存储数据,而不是_first_name,是因为该类的本意是对属性进行类型检查,所以当然也包括在初始化的时候。这样当在初始化的时候对first_name进行赋值的时候,会先调用setter方法来进行类型检查。当然上面的例子还可以如下:

class Person(object):
    def __init__(self, first_name):
        self.set_first_name(first_name)

    def get_first_name(self):
        pass

    def set_first_name(self, value)
        pass

    def del_first_name(self)
        pass

    name = property(get_first_name, set_first_name, del_first_name)

property 的使用实际上是把一系列已经存在的方法绑定到一起。对property的使用并不是所有场景都合适,只有在需要对属性进行额外的操作的时候,再使用。否则很浪费性能而且速度很慢。property是基于描述符的实现,所以,如果场景更换需要对更底层的进行操作,那么使用闭包或者描述符更好。

def typed_property(name, expected_type):
    storage_name = '_'+name

    @property
    def prop(self):
        return getattr(self, storage_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError("{} must be {}".format(name, expected_type))
        setattr(self, storage_name, value)
    return prop

class Person(object):
    name = typed_property('name', str)
    age = typed_property('age', int)

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