如何正確地使用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/

相關文章
相關標籤/搜索