標籤:python 替代 接收 面向 類執行個體化 尋找 one 使用 文法
Python中的描述符描述符的定義:通常情況下,我們可以認為"假設對象的某個屬性被綁定了(
__get__,
__set__,
__delete__)這三個方法中的任意一個方法",那麼我們稱該屬性為"描述符"
class Foo(object): def init(self, name, age): self.name = name self.age = agefoo = Foo("pizza", 18)
我們不能稱 foo.name, foo.age 這兩個屬性為描述符,因為它們都沒有綁定上面三個方法。預設情況下, 對象的屬性訪問是通過get, set, delete這三個方法訪問屬性的字典__dict__來實現的。比如說,a.x會首先尋找a.__dict__[‘x‘], 如果沒有找到則尋找type(a).__dict__[‘x‘], 然後不斷的往上尋找直到metaclass(不包括metaclass)。如下代碼所示:
class Foo(object): country = "China" def __init__(self, name, age): self.name = name self.age = age foo = Foo("pizza", 18)print(foo.__dict__) # {‘name‘: ‘pizza‘, ‘age‘: 18}print(type(foo).__dict__) # {‘__module__‘: ‘__main__‘, ‘country‘: ‘China‘, ‘__init__‘: <function Foo.__init__ at 0x103802488>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Foo‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Foo‘ objects>, ‘__doc__‘: None}
上面的代碼中,如果print(foo.name)或者print(foo.age)或尋找
foo.__dict__, 如果print(foo.country)則會尋找type(foo)既
Foo.__dict__。如果尋找過程中遇到描述符,那麼Python解譯器就會用描述符中的方法來替代尋找順序,到底是先尋找對象的__dict__還是描述符,取決於描述符類型,我們將在下面的小節中示範。描述符協議
descr.__get__(self, obj, type=None) --> valuedescr.__set__(self, obj, value) --> Nonedescr.__delete__(self, obj) --> None
如果定義了以上三個方法中的任意一個,那麼,我們就可以認為該對象是一個描述符對象,它會覆蓋對象屬性的尋找順序。如下代碼所示:
class Bar(object): def __get__(self, instance, owner): print("__get__") def __set__(self, instance, value): print("__set__") def __delete__(self, instance, value): print("__delete__") class Foo(object): bar = Bar() foo = Foo()
以上代碼中,foo的bar屬性就被認為是一個描述符。上文提到了描述符類型,描述符分為,Data Descriptor和Non-data Descriptor。如果一個對象定義了
__get__()和
__set__()這兩個方法,那麼我們認為該對象是一個Data Descriptor。如果只定義了
__get__()方法,那就是Non-data Descriptor,如下代碼所示:Data Descriptor
class Bar(object): def __get__(self, instance, owner): print("get") def __set__(self, instance, value): print("__set__") def __delete__(self, instance, value): print("__delete__")class Foo(object): bar = Bar() foo = Foo()
以上代碼中,foo的bar屬性就被認為是一個描述符,而且是Data Descriptor。Non-data Descriptor
class Bar(object): def __get__(self, instance, owner): print("__get__")class Foo(object): bar = Bar()foo = Foo()
以上代碼中,foo的bar屬性就被認為是一個描述符,而且是Non-data Descriptor。Data and non-data descriptors的不同點在於訪問對象屬性的方式。如果對象的字典
__dict__中有一個跟Data Descriptor同名的屬性,那麼,Data Descriptor會覆蓋
__dict__的尋找,如下代碼所示:
class Bar(object): def __get__(self, instance, owner): print("__get__") def __set__(self, obj, value): print("__set__")class Foo(object): bar = Bar() def __init__(self, name, age): self.name = name self.age = age self.bar = "bar"foo = Foo("pizza", 18)foo.bar # __get__
以上代碼中,foo對象的bar屬性尋找會執行對象的
__get__方法。因為,Data Descriptor會覆蓋
__dict__的尋找。如果對象的字典
__dict__中有一個跟Non-data Descriptor同名的屬性,那麼,對象的
__dict__尋找會覆蓋Non-data Descriptor,如下代碼所示:
class Bar(object): def __get__(self, instance, owner): print("__get__") class Foo(object): bar = Bar() def __init__(self, name, age): self.name = name self.age = age self.bar = "bar"foo = Foo("pizza", 18)foo.bar # "bar"
以上代碼中,foo對象的bar屬性尋找會列印“bar”,因為,對象的
__dict__尋找會覆蓋Non-data Descriptor。Python中預設的property在Python物件導向的設計中,有一個非常重要的知識點,叫做property,它的實現方式有多種,我們通過下面的代碼示範其中一種:
class Foo(object): def __init__(self, name): self._name = name @property def name(self): return self._name foo = Foo("Pizza")print(foo._name) # "Pizza"print(foo.name) # "Pizza"
在上面的程式碼範例中,我們使用foo._name能夠找到該屬性的值”Pizza“,我們也可以通過foo.name找到該屬性的值”Pizza“(因為該屬性返回
self._name)。我們通過在Foo類中定義一個name方法,然後通過預設的裝飾器property實現存取方法時,不進行常用的函數調用方式。在這個過程中,存在一個疑問,既然希望通過屬性的方式訪問對象的方法,且傳回值就是某個屬性,那麼為什麼不直接在
__init__裡面定義一個屬性。其實,屬性的更多的時候,是動態擷取某個值,並保留屬性的訪問方式,而不是簡單的返回已存在的屬性的值,比如:
class Foo(object): def __init__(self, name): self._name = name @property def stock(self): return 100 + 100 foo = Foo("Pizza")print(foo._name) # "Pizza"print(foo.stock) # 200
我們在前面的章節中提到過,Python中的property,static method,class method的實現都依賴於descriptor的機制來實現。那麼,接下來,我們來自訂一個property。使用Descriptor自訂property從上一小節中,我們可以看到,將類中的方法修改為一個property,就是利用了裝飾器@property。我們知道裝飾器文法糖@decorator,等價於 func = decorator(func),如下代碼所示:
class Foo(object): def stock(self): return 100 + 100 print(stock) # <function Foo.stock at 0x101a57048>class Foo(object): @property def stock(self): return 100 + 100 print(stock) # <property object at 0x103811c78>
上面的程式碼範例中,print(stock)的列印結果是
<property object at 0x103811c78>和
<function Foo.stock at 0x101a57048>,既,在stock方法上面加上@property之後,stock這個方法變為了property的對象,與第一個print(stock)的
<function Foo.stock at 0x101a57048>不同。接下來,我們的目的是通過descriptor來實現自訂property。在實現自訂property之前,我們先假設有一個類,如下代碼所示:
class Foo(object): def stock(self): return 100 + 100foo = Foo()foo.stock
我們已知如下幾點:
- 裝飾器文法糖 @property等價於 stock = property(stock);
- 描述符是一個類的執行個體化對象,如 bar = Bar(),然後在Bar這個類中定義了
__get__, __set__, __delete__;
我們的目的是,通過類似屬性訪問的方式(foo.stock)而非方法調用的方式(foo.stock()),獲得傳回值200。首選,我們通過描述符的方式,來實現簡單的屬性訪問,如下代碼所示:
class Stock(object): def __get__(self, instance, owner): print("__get__") return 100 + 100 class Foo(object): stock = Stock() foo = Foo()foo.stock
此時,通過訪問foo.stock會先列印
__get__, 然後顯示200。那麼,如果我們將stock變為類中的一個方法呢?如下代碼所示:
class Stock(object): def __get__(self, instance, owner): print("__get__") return 100 + 100class Foo(object): def stock(self): print("stock")
如果能將stock方法變為一個descriptor,那麼我們就可以通過foo.stock訪問該descriptor的
__get__方法,然後擷取其傳回值,既,200。我們知道,將屬性變為descriptor,直接通過給該屬性綁定
__get__方法即可,如:stock = Stock(),但是,如何利用裝飾器文法糖呢?我們知道,裝飾器文法糖@Stock等價於stock = Stock(stock),因此,我們需要,在Stock這個類中定義一個
__init__方法,並定義一個形參來接收Stock類執行個體化時傳入的stock函數,如下代碼所示:
class Stock(object): def __init__(self, stock): self.stock = stock def __get__(self, instance, owner): print("__get__") return 100 + 100 class Foo(object): @Stock # stock = Stock(stock) def stock(self): print("stock")
通過以上,代碼,我們就將Foo類中的stock方法,成功的變成了一個descriptor,接下來我們可以通過Foo類的執行個體化對象來訪問stock方法,並且使用普通的屬性調用方法,因為此時Foo類中的stock方法已經是一個descriptor了。
foo = Foo()foo.stock
以上代碼會先列印
__get__, 然後顯示200。事實上,細心的同學會發現,如果採用這種實現方式,我們實現了自訂的property,但是,與官方正版的property還有差距,這個差距在於,訪問foo.stock的時候,Foo類中的stock並沒有被執行,而正版的property中的屬性是被執行了的,也就是說,我們最後需要擷取的值,是直接從該屬性中計算來獲得的,如下代碼所示:
class Foo(object): @property def stock(self): return 100 + 100 foo = Foo()foo.stock
在上面的程式碼範例中,我們通過foo.stock擷取到的結果200,是通過Foo類中的stock這個方法來計算獲得的。如果我希望在自訂的property中也採用同樣的方式,該如何做呢?我們知道,在定義
__get__方法時,它接受三個參數,第一個self表示descriptor,下面我們,分別print第二個和第三個參數,看看它們分別表示什麼:
class Stock(object): def __init__(self, stock): self.stock = stock def __get__(self, instance, owner): print("__get__") print("instance: ", instance) print("owner:", owner) return 100 + 100 class Foo(object): @Stock def stock(self): print("stock") foo = Foo()foo.stock
以上代碼的執行結果如下:
__get__instance: <__main__.Foo object at 0x106c2b9b0>owner: <class ‘__main__.Foo‘>200
從以上代碼的執行結果可以看出,instance和owner這兩個形參,分別被傳入了foo和Foo這兩個對象,一個是Foo類的執行個體化對象,一個是Foo類本身,那麼,我們是否可以使用foo或者Foo在
__get__方法中,調用stock呢?答案是否定的,因為此時的stock已經是一個descriptor了,如果在
__get__方法中調用,那麼就進入死迴圈了,一直重複的執行
__get__方法。最原始的那個Foo類中的stock方法,在進行@Stock時,被傳入了Stock類中的
__init__方法進行初始化,因此,此時我們只能通過如下程式碼範例中使用的方式,進行調用:
class Stock(object): def __init__(self, stock): self.stock = stock def __get__(self, instance, owner): return self.stock(instance) class Foo(object): @Stock def stock(self): return 100 + 100 foo = Foo()foo.stock
以上代碼的執行結果如下:
200
通過結果我們可以看出,與之前的執行結果是一致的。簡單修改為更能理解的程式碼範例,如下所示:
class myproperty(object): def __init__(self, stock): self.stock = stock def __get__(self, instance, owner): return self.stock(instance) class Foo(object): @myproperty def stock(self): return 100 + 100 foo = Foo()foo.stock
至此,我們實現了自訂的property。
Python中的Descriptor