如何正确地使用Python的属性和描述符

关于@property装饰器html

在Python中咱们使用@property装饰器来把对函数的调用假装成对属性的访问。python

那么为何要这样作呢?由于@property让咱们将自定义的代码同变量的访问/设定联系在了一块儿,同时为你的类保持一个简单的访问属性的接口。django

举个栗子,假如咱们有一个须要表示电影的类:ide

class Movie(object):
    def __init__(self, title, description, score, ticket):
        self.title = title
        self.description = description
        self.score = scroe
        self.ticket = ticket
  

你开始在项目的其余地方使用这个类,可是以后你意识到:若是不当心给电影打了负分怎么办?你以为这是错误的行为,但愿Movie类能够阻止这个错误。 你首先想到的办法是将Movie类修改成这样:函数

class Movie(object):
    def __init__(self, title, description, score, ticket):
        self.title = title
        self.description = description
     self.ticket = ticket
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self.score = scroe

但这行不通。由于其余部分的代码都是直接经过Movie.score来赋值的。这个新修改的类只会在__init__方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。若是有人试着运行m.scrore= -100,那么谁也无法阻止。那该怎么办?ui

Python的property解决了这个问题。this

咱们能够这样作.net

class Movie(object):
    def __init__(self, title, description, score):
        self.title = title
        self.description = description
        self.score = score
     self.ticket = ticket

    @property
    def score(self):
        return self.__score


    @score.setter
    def score(self, score):
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self.__score = score

    @score.deleter
    def score(self):
        raise AttributeError("Can not delete score")

这样在任何地方修改score都会检测它是否小于0。代理

property的不足code

对property来讲,最大的缺点就是它们不能重复使用。举个例子,假设你想为ticket字段也添加非负检查。下面是修改过的新类:

class Movie(object):
    def __init__(self, title, description, score, ticket):
        self.title = title
        self.description = description
        self.score = score
        self.ticket = ticket

    @property
    def score(self):
        return self.__score


    @score.setter
    def score(self, score):
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self.__score = score

    @score.deleter
    def score(self):
        raise AttributeError("Can not delete score")


    @property
    def ticket(self):
        return self.__ticket

    @ticket.setter
    def ticket(self, ticket):
        if ticket < 0:
            raise ValueError("Negative value not allowed:{}".format(ticket))
        self.__ticket = ticket


    @ticket.deleter
    def ticket(self):
        raise AttributeError("Can not delete ticket")

能够看到代码增长了很多,但重复的逻辑也出现了很多。虽然property可让类从外部看起来接口整洁漂亮,可是却作不到内部一样整洁漂亮。

描述符登场

什么是描述符?

通常来讲,描述符是一个具备绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、__set__()和__delete__(),一个对象中只要包含了这三个方法中的至少一个就称它为描述符。

描述符有什么做用?

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting witha.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.-----摘自官方文档

简单的说描述符会改变一个属性的基本的获取、设置和删除方式。

先看如何用描述符来解决上面 property逻辑重复的问题。

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

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

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Negative value not allowed")
        instance.__dict__[self.name] = value

class Movie(object):
    score = Integer('score')
    ticket = Integer('ticket')

由于描述符优先级高而且会改变默认的get、set行为,这样一来,当咱们访问或者设置Movie().score的时候都会受到描述符Integer的限制。

不过咱们也总不能用下面这样的方式来建立实例。

a = Movie()
a.score = 1
a.ticket = 2
a.title = 'test'
a.descript = '...'

这样太生硬了,因此咱们还缺一个构造函数。

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

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Negative value not allowed')
        instance.__dict__[self.name] = value


class Movie(object):
    score = Integer('score')
    ticket = Integer('ticket')
    
    def __init__(self, title, description, score, ticket):
        self.title = title
        self.description = description
        self.score = score
        self.ticket = ticket

这样在获取、设置和删除score和ticket的时候都会进入Integer的__get__、__set__,从而减小了重复的逻辑。

如今虽然问题获得了解决,可是你可能会好奇这个描述符究竟是如何工做的。具体来讲,在__init__函数里访问的是本身的self.score和self.ticket,怎么和类属性score和ticket关联起来的?

描述符如何工做

看官方的说明

If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).

Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.

The important points to remember are:

descriptors are invoked by the __getattribute__() method
overriding __getattribute__() prevents automatic descriptor calls
object.__getattribute__() and type.__getattribute__() make different calls to __get__().
data descriptors always override instance dictionaries.
non-data descriptors may be overridden by instance dictionaries.
类调用__getattribute__()的时候大概是下面这样子:

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

  

下面是摘自国外一篇博客上的内容。

Given a Class “C” and an Instance “c” where “c = C(…)”, calling “c.name” means looking up an Attribute “name” on the Instance “c” like this:

Get the Class from Instance
Call the Class’s special method getattribute__. All objects have a default __getattribute
Inside getattribute

Get the Class’s mro as ClassParents
For each ClassParent in ClassParents
If the Attribute is in the ClassParent’s dict
If is a data descriptor
Return the result from calling the data descriptor’s special method __get__()
Break the for each (do not continue searching the same Attribute any further)
If the Attribute is in Instance’s dict
Return the value as it is (even if the value is a data descriptor)
For each ClassParent in ClassParents
If the Attribute is in the ClassParent’s dict
If is a non-data descriptor
Return the result from calling the non-data descriptor’s special method __get__()
If it is NOT a descriptor
Return the value
If Class has the special method getattr
Return the result from calling the Class’s special method__getattr__.
我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的data descriptor若是有,就用这个data descriptor代理该属性,若是没有再寻找该实例自身的__dict__,若是有就返回。任然没有再查找它和它父类里的non-data descriptor,最后查找是否有__getattr__

描述符的应用场景

python的property、classmethod修饰器自己也是一个描述符,甚至普通的函数也是描述符(non-data discriptor)

django model和SQLAlchemy里也有描述符的应用

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

    def __init__(self, username, email):
        self.username = username
        self.email = email

    def __repr__(self):
        return '<User %r>' % self.username
  

后记

只有当确实须要在访问属性的时候完成一些额外的处理任务时,才应该使用property。否则代码反而会变得更加啰嗦,并且这样会让程序变慢不少。

参考文章:

https://docs.python.org/3.5/h...

http://www.betterprogramming....

http://stackoverflow.com/ques...

http://www.jianshu.com/p/250f...

http://www.geekfan.net/7862/

相关文章
相关标签/搜索