標籤:

如何理解 Python 的 Descriptor?

有人可以解釋一下python的Descriptor知識嗎?


舊文搬運:

&>&>&> class MyInt(int):
... def square(self):
... return self*self
...
&>&>&> n = MyInt(2)
&>&>&> n.name = "two"
&>&>&> n.square()
4
&>&>&> n.name
"two"

小測驗:上面代碼的最後4行,n.square和n.name分別在幾個對象的__dict__中查找"square"或"name"?

1個?2個?答案是2和4。n.square需要查找MyInt和n,n.name需要查找MyInt, int, object和n,查找順序就如我列出來這樣。

我們知道對象的屬性覆蓋類的屬性:

&>&>&> MyInt.name = "MyInt"
&>&>&> n.name
"two"

既然如此為什麼查找順序不反過來:先查找n,既然n.__dict__["name"]存在就不需要再查找3個類?原因在於Python 2.2引入的新API: descriptor。

Python中不僅是類,實例也可以有自己的方法:

&>&>&> def hello():
... print "hello"
...
&>&>&> n.hello = hello
&>&>&> n.hello()
hello

MyInt.square比hello多一個self參數,為什麼都可以用n.foo()形式來調用?因為前者是"方法"類型而後者是"函數"類型?不,我們已經知道class關鍵字不會改變def的語義:

&>&>&> type(MyInt.__dict__["square"])
&
&>&>&> type(n.__dict__["hello"])
&

Python在這裡耍了個小花招:當在n中找不到屬性square,而在n.__class__(即MyInt)中找到,而且MyInt.square是函數時,不直接返回這個函數,而是創建 一個wrapper:

&>&>&> n.square
&
&>&>&> type(n.square)
&

Wrapper中包含了n的引用,或者說,square的self參數被綁定到n。在有new-style class之前(如Python 1.5.2),這個查找過程大概是這樣(實際的代碼是C語言):

def instance_getattr(obj, name):
"Look for attribute /name/ in object /obj/."
v = obj.__dict__.get(name)
if v is not None:
# found in object
return v
v, cls = class_lookup(obj.__class__, name)
# found v in class cls
if isinstance(v, types.FunctionType): # Note this line
# function type. build method wrapper
return BoundMethod(v, obj, cls)
if v is not None:
# data attribute
return v
raise AttributeError(obj.__class__, name)

def class_lookup(cls, name):
"Look for attribute /name/ in class /cls/ and bases."
v = cls.__dict__.get(name)
if v is not None:
# found in this class
return v, cls
# search in base classes
for i in cls.__bases__:
v, c = class_lookup(i, name)
if v is not None:
return v, c
# not found
return None

這個機制也算簡單有效。可是當Python開發者們準備用new-style class整理類型系統時,下面這幾行代碼就顯得有些扎眼:

if isinstance(v, types.FunctionType):
# function type. build method wrapper
return BoundMethod(v, obj, cls)

Python的風格是不太鼓勵用isinstance的,因為它不符合duck typing的精神:不要問我是什麼,問我能做什麼。函數屬性需要創建wrapper而數據屬性不需要,這是Python的基本設計,不需要改動也不能改動。但是我們可以把這個規則一般化:
給"像函數的"屬性創建wrapper,而不給"像數據的"屬性創建。Any software problem can be solved by adding another layer of indirection.

if v.like_a_function():
# function-like type. build method wrapper
return BoundMethod(v, obj, cls)

一不做,二不休,為什麼不讓對象自己決定怎樣創建wrapper?

...
if hasattr(v, "__get__"):
# anything with a "__get__" attribute is
# a function-like descriptor
return v.__get__(obj, obj.__class__)
...

class FunctionType(object):
...
def __get__(self, obj, cls):
return BoundMethod(v, obj, cls)

好了,我們得到了descriptor的雛形。現在任何對象都可以模仿函數的行為,即使作為方法也沒有問題。但是,潘多拉的盒子已經打開,開發者們不會就此止步的。對靈活性的追求永無止境。。。

比如,staticmethod把函數的綁定方式變為"不綁定":

class StaticMethod(object):
def __init__(self, f):
self.f = f

def __get__(self, obj, cls):
return self.f

class C(object):
@StaticMethod
def f(): # no self param
pass

或者,log每次函數調用:

&>&>&> import types
&>&>&> class Log(object):
... def __init__(self, f):
... self.f = f
... def __get__(self, obj, cls):
... print self.f.__name__, "called"
... return types.MethodType(self.f, obj, cls)
...
&>&>&> class C(object):
... @Log
... def f(self):
... pass
...
&>&>&> c = C()
&>&>&> c.f()
f called

Descriptor也不僅限於用在函數上。立即想到的是用它來做property。可是用__get__只能做出readonly property,那就再加個__set__吧:

&>&>&> class Property(object):
... def __init__(self, fget, fset):
... self.fget = fget
... self.fset = fset
... def __get__(self, obj, cls):
... return self.fget(obj)
... def __set__(self, obj, val):
... self.fset(obj, val)
...
&>&>&> class C(object):
... def fget(self):
... print "fget called"
... def fset(self, val):
... print "fset called with", val
... f = Property(fget, fset)
...
&>&>&> c = C()
&>&>&> c.f
fget called
&>&>&> c.f = 1
fset called with 1

且慢,上面這段代碼要能正常工作,還要克服一個困難:賦值總是作用於實例,根本不會去類中查找:

&>&>&> c = C()
&>&>&> c.n
0
&>&>&> c.n = 1
&>&>&> c.n
1
&>&>&> C.n
0

這樣一來,c.f = 1這個操作根本不會查找到我們在類中定義的property f,__set__方法也無從發揮作用。所以,我們只能改變賦值操作的語義,讓類里定義的descriptor能夠攔截對實例的屬性賦值。現在要先在類和基類中查找名為"f",而且定義了__set__方法的descriptor,只有找不到時,才在實例中進行賦值。

可是,我們之前為函數設計的__get__方法,查找順序是在實例屬性之後的;而__set__方法查找順序又必須在實例屬性之前。如果同一個descriptor的兩個方法查找順序竟然不一樣,那看上去可不太美。怎麼解決descriptor用於函數和property時,對查找順序的不同要求呢?

Python的解決方法說也簡單:如果一個descriptor只有__get__方法(如FunctionType),我們就認為它是function-like descriptor,適用"實例-類-基類"的普通查找順序;如果它有__set__方法(如Property),就是data-like descriptor,適用"類-基類-實例"的特殊查找順序。但是找到descriptor之前又怎麼可能知道它的類型呢?所以無論如何都得先查找類和基類,再根據是否找到descriptor,和descriptor的類型,來決定是否需要查找實例。現在的查找演算法成了這樣:

def object_getattr(obj, name):
"Look for attribute /name/ in object /obj/."
# First look in class and base classes.
v, cls = class_lookup(obj.__class__, name)
if (v is not None) and hasattr(v, "__get__") and hasattr(v, "__set__"):
# Data descriptor. Overrides instance member.
return v.__get__(obj, cls)
w = obj.__dict__.get(name)
if w is not None:
# Found in object
return w
if v is not None:
if hasattr(v, "__get__"):
# Function-like descriptor.
return v.__get__(obj, cls)
else:
# Normal data member in class
return v
raise AttributeError(obj.__class__, name)

現在我們可以回答第一節末尾的問題了。接觸descriptor之前,每個人概念里的查找順序大概都是"實例-類-基類",而實際的查找過程卻是"類-基類-實例"。概念上實例屬性應該只需一次查找,實際上卻是查找次數最多的(需要查找全部基類);查找次數最少的是方法(2次:類-找到function-like descriptor,實例-未找到)。另一個意外的結果是基類越多,查找實例屬性越慢,儘管這個查找看上去和基類不相干。好在Python是動態類型,類層次一般不深。

這一切都是為了支持property。值不值得呢?能在類上攔截對實例屬性的訪問,由此可以引出很多有趣的用法,和metaclass結合起來更是如此。對於Python來說"性能"似乎從來不是犧牲"功能"(以及其他各種美德)的理由,這次也不例外。


以為是Python3.4里新加的什麼牛逼功能,還和單繼承聯繫起來了。嚇得我趕緊翻了一下文檔。

Descriptor就是一類實現了__get__(), __set__(), __delete__()方法的對象。沒了。就這樣了。沒看懂的話去學習一下duck typing。

用途就很多了。把函數包裝成property,把property包裝成private property等等。


簡單來講,描述符就是一個Python對象,但這個對象比較特殊,特殊性在於其屬性的訪問方式不再像普通對象那樣訪問,它通過一種叫描述符協議的方法來訪問。這些方法包括__get__、__set__、__delete__。定義了其中任意一個方法的對象都叫描述符。舉個例子:


普通對象

class Parent(object):
name = "p"

class Person(Parent):
name = "zs"

zhangsan = Person()
zhangsan.name = "zhangsan"
print zhangsan.name
#&>&> zhangsan

普通的Python對象操作(get,set,delete)屬性時都是在這個對象的__dict__基礎之上進行的。比如上例中它在訪問屬性name的方式是通過如下順序去查找,直到找到該屬性位置,如果在父類中還沒找到那麼就拋異常了。

  1. 通過實例對象的__dict__屬性訪問:zhangsan.__dict__["name"]
  2. 通過類型對象的__dict__屬性訪問:type(zhangsan).__dict__["name"] 等價於 Person.__dict__["name"]
  3. 通過父類對象的__dict__屬性訪問:zhangsan.__class__.__base__.__dict__["name"] 等價於 Parent.__dict__["name"]

類似地修改屬性name的值也是通過__dict__的方式:

zhangsan.__dict__["name"] = "lisi"
print zhangsan.name
#&>&> lisi

描述符

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

def __get__(self, instance, owner):
print "__get__", instance, owner
return self.name

def __set__(self, instance, value):
print "__set__", instance, value
self.name = value

class Person(object):
name = DescriptorName("zhangsan")

zhangsan = Person()
print zhangsan.name
#&>&>__get__ &<__main__.Person object at 0x10bc59d50&> &
#&>&>zhangsan

這裡的DescriptorName就是一個描述符,訪問Person對象的name屬性時不再是通過__dict__屬性來訪問,而是通過調用DescriptorName的__get__方法獲取,同樣的道理,給name賦值的時候是通過調用__set__方法實現而不是通過__dict__屬性。


zhangsan.__dict__["name"] = "lisi"
print zhangsan.name
#&>&>__get__ &<__main__.Person object at 0x10bc59d50&> &
#&>&>zhangsan
#通過dict賦值給name但值並不是"lisi",而是通過調用get方法獲取的值

zhangsan.name = "lisi"
print zhangsan.name
#&>&>__set__ &<__main__.Person object at 0x108b35d50&> lisi
#&>&>__get__ &<__main__.Person object at 0x108b35d50&> &
#&>&>lisi

類似地,刪除屬性的值也是通過調用__delete__方法完成的。此時,你有沒有發現描述符似曾相識,沒錯,用過Django就知道在定義model的時候,就用到了描述符。比如:

from django.db import models

class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")

上面的例子是基於類的方式來創建描述符,你還可以通過property()函數來創建描述符,例如:

class Person(object):

def __init__(self):
self._email = None

def get_email(self):
return self._email

def set_email(self, value):
m = re.match("w+@w+.w+", value)
if not m:
raise Exception("email not valid")
self._email = value

def del_email(self):
del self._email

#使用property()函數創建描述符
email = property(get_email, set_email, del_email, "this is email property")

&>&>&> p = Person()
&>&>&> p.email
&>&>&> p.email = "dsfsfsd"
Traceback (most recent call last):
File "&", line 1, in &
File "test.py", line 71, in set_email
raise Exception("email not valid")
Exception: email not valid
&>&>&> p.email = "lzjun567@gmail.com"
&>&>&> p.email
"lzjun567@gmail.com"
&>&>&>

property()函數返回的是一個描述符對象,它可接收四個參數:property(fget=None, fset=None, fdel=None, doc=None)

  • fget:屬性獲取方法
  • fset:屬性設置方法
  • fdel:屬性刪除方法
  • doc: docstring

採用property實現描述符與使用類實現描述符的作用是一樣的,只是實現方式不一樣。python裡面的property是使用C語言實現的,不過你可以使用純python的方式來實現property函數,如下:

class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)

def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can"t set attribute")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can"t delete attribute")
self.fdel(obj)

def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

留心的你發現property裡面還有getter,setter,deleter方法,那他們是做什麼用的呢?來看看第三種創建描述符的方法。

使用@property裝飾器

class Person(object):

def __init__(self):
self._email = None

@property
def email(self):
return self._email

@email.setter
def email(self, value):
m = re.match("w+@w+.w+", value)
if not m:
raise Exception("email not valid")
self._email = value

@email.deleter
def email(self):
del self._email

&>&>&>
&>&>&> Person.email
&


&>&>&> p.email = "lzjun"
Traceback (most recent call last):
File "&", line 1, in &
File "test.py", line 93, in email
raise Exception("email not valid")
Exception: email not valid
&>&>&> p.email = "lzjun@gmail.com"
&>&>&> p.email
"lzjun@gmail.com"
&>&>&>

發現沒有,其實裝飾器property只是property函數的一種語法糖而已,setter和deleter作用在函數上面作為裝飾器使用。

哪些場景用到了描述符

其實python的實例方法就是一個描述符,來看下面代碼塊:

&>&>&> class Foo(object):
... def my_function(self):
... pass
...
&>&>&> Foo.my_function
&
&>&>&> Foo.__dict__["my_function"]
&
&>&>&> Foo.__dict__["my_function"].__get__(None, Foo) #my_function函數實現了__get__方法
&
&>&>&> Foo().my_function
&&>
&>&>&> Foo.__dict__["my_function"].__get__(Foo(), Foo) #Foo的實例對象實現了__get__方法
&&>


兩篇拙文,入門級水平

簡明Python魔法-1
簡明Python魔法-2


圖片python裝飾器語法,foo函數被裝飾後,不再是原函數,
@logger是foo=logger(foo) 的語法糖,
foo是logger(foo)的返回值inner,inner是一個函數,foo(5)
等於inner(5).inner函數的形式參數是可變長度參數*args,**kwargs,
隨便你傳元組形式或字典形式的數據都可以。


從某種角度來看,我們所寫的絕大多數代碼中,「對象,都不擁有函數」,對象真正擁有的是obj.__dict__裡面的東西。當我們寫下class的定義時,那些函數都是屬於class的(即 在 klass.__dict__中),包括什麼所謂的靜態方法,實例方法,類方法都是屬於class的。

那麼我們為什麼能夠執行這些函數,或者說為什麼對象能夠訪問到這些函數,這就是descriptor的功勞。

當我們在訪問一個對象屬性(或者方法)時,我們其實是在調用__getattribute__(可能舊版本的是調用__getattr__),具體的descriptor的訪問順序是由這個函數來決定。

剩下的就不多說了,順序就如高贊答案所說。

可能有點難記,但是按照下面的想法來記或許好記一點:

在按照 類--&> 基類 --&> 實例 這個順序 的__dict__中找到的同名的屬性,python總是希望那個 定義了 __get__ __set__ 的屬性 , 否則的話,總是傾向於使用實例自己__dict__中的。此外,知道了這個屬性是在哪個__dict__中的並沒有完成工作,因為只要是訪問這個值,他如果有__get__方法,必然是調用這個__get__方法來得到最終的值。

此外,如果setter寫不好的話,很容易遞歸調用。。一個小技巧,可以將self.hello = new_value改成self.__dict__["hello"] = new_value


推薦閱讀:

如何優化 Python 爬蟲的速度?
為什麼網路爬蟲好難,涉及到的知識我不會?
做python爬蟲需要會web後端嗎,不會的話能做嗎?
python3爬蟲中文亂碼問題求解?(beautifulsoup4)
求大神們推薦python入門書籍(爬蟲方面)?

TAG:Python | Python3x |