寫在前面
我覺得抓住以下幾處重點大概就搞明白這玩意兒了
- 一個描述符是一個有“繫結行為”的物件屬性(object attribute),它的訪問控制會被描述器協議方法重寫。
- 任何定義了
__get__
,__set__
或者__delete__
任一方法的類稱為描述符類,其例項物件便是一個描述符,這些方法稱為描述符協議。 - 當對一個例項屬性進行訪問時,Python 會按
obj.__dict__
→type(obj).__dict__
→type(obj)的父類.__dict__
順序進行查詢,如果查詢到目標屬性並發現是一個描述符,Python 會呼叫描述符協議來改變預設的控制行為。 - 描述符是 @property @classmethod @staticmethod 和 super 的底層實現機制。
同時定義了
__get__
和__set__
的描述符稱為 資料描述符(data descriptor);僅定義了__get__
的稱為 非資料描述符(non-data descriptor) 。兩者區別在於:如果obj.__dict__
中有與描述符同名的屬性,若描述符是資料描述符,則優先呼叫描述符,若是非資料描述符,則優先使用obj.__dict__
中屬性。描述符協議必須定義在類的層次上,否則無法被自動呼叫。翻譯原文
原文:Python Descriptors: An Introduction
描述符是Python的一項特定功能,可為語言隱藏的許多魔力提供強大的支援。如果您曾經以為Python描述符是很少使用的高階主題,那麼本教程就是幫助您瞭解此強大功能的理想工具。您將瞭解為什麼Python描述符如此有趣,以及在什麼情況下使用它們。
在本教程結束時,您將瞭解:
- 什麼是 Python 的描述符
- 它們在 Python 內部使用的地方
- 如何實現自己的描述符
- 何時使用 Python 描述符
本教程適用於中級到高階 Python 開發人員,因為它涉及 Python 內部。但是,如果您還沒有達到這個水平,那就繼續閱讀吧!您會找到有關 Python 和 屬性查詢鏈的有用資訊。
什麼是Python描述符?
描述符是實現描述符協議方法的Python物件,當您將其作為其他物件的屬性進行訪問時,該描述符使您能夠建立具有特殊行為的物件。在這裡,您可以看到描述符協議的正確定義:
__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None
__delete__(self, obj) -> None
__set_name__(self, owner, name)
如果您的描述符僅實現__get__()
,則稱其為非資料描述符。如果它實現__set__()
或__delete__()
,則稱其為資料描述符。請注意,這種區別不僅在於名稱,還在於行為上的區別。這是因為資料描述符在查詢過程中具有優先順序,這將在後面介紹。
請看以下示例,該示例定義了一個描述符,該描述符在訪問控制檯時將其記錄在控制檯上:
# descriptors.py
class Verbose_attribute():
def __get__(self, obj, type=None) -> object:
print("accessing the attribute to get the value")
return 42
def __set__(self, obj, value) -> None:
print("accessing the attribute to set the value")
raise AttributeError("Cannot change the value")
class Foo():
attribute1 = Verbose_attribute()
my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)
在上面的示例中,Verbose_attribute()實現了描述符協議。將其例項化為Foo的屬性後,就可以視為描述符。
作為描述符,當使用點表示法訪問時,它具有繫結行為。在這種情況下,每次訪問描述符以獲取或設定值時,描述符都會在控制檯上記錄一條訊息:
- 當訪問
__get__()
值時,它總是返回值42。 - 當訪問
__set__()
的特定值時,它會引發AttributeError異常,這是實現只讀描述符的推薦方法。
現在,執行上面的示例,您將看到描述符在返回常量值之前將其記錄在控制檯上:
$ python descriptors.py
accessing the attribute to get the value
42
在這裡,當您嘗試訪問attribute1時,描述符按照.__ get __()中的定義將此訪問記錄到控制檯
描述符在Python內部的工作方式
如果您是具有豐富的物件導向(開發)經驗的Python開發人員,那麼您可能會認為上一個示例的方法有些過度。透過使用屬性,您可以實現相同的結果。雖然這是事實,但您可能會驚訝地發現Python中的屬性也是……描述符!稍後您會看到,屬性不是唯一使用Python描述符的功能。
屬性中的Python描述符
如果要在不顯式使用Python描述符的情況下獲得與上一個示例相同的結果,則最直接的方法是使用 property。以下示例使用 property,該屬性在訪問時將資訊記錄到控制檯:
# property_decorator.py
class Foo():
@property
def attribute1(self) -> object:
print("accessing the attribute to get the value")
return 42
@attribute1.setter
def attribute1(self, value) -> None:
print("accessing the attribute to set the value")
raise AttributeError("Cannot change the value")
my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)
譯者注:使用 property 裝飾後,name 變成 property 類的一個例項,第二個name 函式使用 name.setter 來裝飾,本質是呼叫 propetry.setter 來產生一個新的 property 例項賦值給第二個 name。第一個 name 和第二個 name 是兩個不同 property 例項,但他們都屬於同一個描述符類 property。當對 name 賦值時,就會進入property.__set__
,當對 name 取值時,就會進入property.__get__
。
上面的示例使用裝飾器來定義屬性,但是您可能知道,裝飾器只是語法糖。實際上,前面的示例可以編寫如下:
# property_function.py
class Foo():
def getter(self) -> object:
print("accessing the attribute to get the value")
return 42
def setter(self, value) -> None:
print("accessing the attribute to set the value")
raise AttributeError("Cannot change the value")
attribute1 = property(getter, setter)
my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)
現在您可以看到該屬性是透過使用property()建立的。該函式的簽名如下:
property(fget=None, fset=None, fdel=None, doc=None) -> object
property()返回實現描述符協議的屬性物件。它使用引數fget,fset和fdel來表示協議的三種方法的實際實現。
方法中的Python描述符
如果您曾經用Python編寫過物件導向的程式,那麼您肯定會使用方法。這些常規函式為物件例項保留第一個引數。使用點表示法訪問方法時,您將呼叫相應的函式並將物件例項作為第一個引數傳遞。
將obj.method( args)轉換為 method(obj, args)的魔力在於函式物件的__get__()
實現內部,實際上是一個非資料描述符。特別是,該函式物件實現__get__()
,以便在您使用點表示法訪問它時返回一個繫結方法。後面的(* args)透過傳遞所有需要的額外引數來呼叫函式。
要了解其工作原理,請看一下官方檔案中的這個純Python示例:
import types
class Function(object):
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return types.MethodType(self, obj)
在上面的示例中,當使用點符號訪問該函式時,將呼叫__get__()
並返回一個繫結方法。
這適用於常規例項方法,同樣適用於類方法或靜態方法。因此,如果您使用obj.method( args)呼叫靜態方法,則該方法會自動轉換為method( args)。同樣,如果您使用obj.method(type(obj), args)呼叫類方法,則該類方法會自動轉換為method(type(obj), args)。
在官方檔案中,您可以找到一些示例,說明如果使用純Python而不是C實現編寫如何實現靜態方法和類方法。例如,可能的靜態方法實現可能是這樣的:
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
同樣,這可能是可能的類方法實現:
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
請注意,在Python中,類方法只是將類引用作為引數列表的第一個引數的靜態方法。
如何使用查詢鏈訪問屬性
要了解有關Python描述符和Python內部的更多資訊,您需要了解訪問屬性時Python中會發生什麼。在Python中,每個物件都有一個內建的__dict__
屬性。這是一個字典,其中包含物件本身中定義的所有屬性。要檢視實際效果,請考慮以下示例:
class Vehicle():
can_fly = False
number_of_weels = 0
class Car(Vehicle):
number_of_weels = 4
def __init__(self, color):
self.color = color
my_car = Car("red")
print(my_car.__dict__)
print(type(my_car).__dict__)
此程式碼建立一個例項,並列印例項和類的__dict__
屬性的內容。現在,執行指令碼並分析輸出以檢視__dict__
屬性集:
{'color': 'red'}
{'__module__': '__main__', 'number_of_weels': 4, '__init__': <function Car.__init__ at 0x10fdeaea0>, '__doc__': None}
__dict__
屬性集符合預期。請注意,在Python中一切都是物件。類實際上也是一個物件,因此它還將具有__dict__
屬性,其中包含該類的所有屬性和方法。
那麼,當您訪問Python中的屬性時,到底發生了什麼?讓我們使用前一個示例的修改版本進行一些測試。考慮以下程式碼:
# lookup.py
class Vehicle(object):
can_fly = False
number_of_weels = 0
class Car(Vehicle):
number_of_weels = 4
def __init__(self, color):
self.color = color
my_car = Car("red")
print(my_car.color)
print(my_car.number_of_weels)
print(my_car.can_fly)
在此示例中,您將建立一個Car類的例項,Car類繼承自Vehicle類。然後,您訪問一些屬性。如果執行此示例,則可以看到獲得了所有期望的值:
$ python lookup.py
red
4
False
在這裡,當您訪問例項my_car的屬性顏色時,實際上是在訪問物件my_car的__dict__
屬性的單個值。當您訪問物件my_car的屬性number_of_wheels時,實際上是在訪問Car類的__dict__
屬性的單個值。最後,當您訪問can_fly屬性時,實際上是在使用Vehicle類的__dict__
屬性來訪問它。
這意味著可以重寫上面的示例:
# lookup2.py
class Vehicle():
can_fly = False
number_of_weels = 0
class Car(Vehicle):
number_of_weels = 4
def __init__(self, color):
self.color = color
my_car = Car("red")
print(my_car.__dict__['color'])
print(type(my_car).__dict__['number_of_weels'])
print(type(my_car).__base__.__dict__['can_fly'])
在測試這個新示例時,您應該得到相同的結果:
$ python lookup2.py
red
4
False
那麼,當您使用點符號訪問物件的屬性時會發生什麼?Python 直譯器是如何知道您的真正需求?好吧,這裡有一個叫做查詢鏈的概念:
- 首先,您將從以所要查詢的屬性命名的資料描述符
__get__
方法返回結果。 - 如果失敗,那麼您將獲得例項物件的
__dict__
值,該值是根據要查詢的屬性命名的鍵。 - 如果失敗,那麼將從以您要查詢的屬性命名的非資料描述符
__get__
方法中返回結果。 - 如果失敗,那麼您將獲得型別物件的
__dict__
值,該值是根據要查詢的屬性命名的鍵。 - 如果失敗,那麼您將獲得父類的
__dict__
值,該值是根據要查詢的屬性命名的鍵。 - 如果失敗,那麼將按照物件的方法解析順序對所有父類重複上一步。
- 如果其他所有操作均失敗,則將出現AttributeError異常。
現在,您明白為什麼要知道描述符是資料描述符還是非資料描述符是如此重要了吧?它們位於查詢鏈的不同層次上,稍後您會發現這種行為上的差異非常方便。
譯者注:同時定義了__get__
和__set__
的描述符稱為 資料描述符(data descriptor);僅定義了__get__
的稱為 非資料描述符(non-data descriptor) 。兩者區別在於:如果 obj.__dict__
中有與描述符同名的屬性,若描述符是資料描述符,則優先呼叫描述符,若是非資料描述符,則優先使用 obj.__dict__
中屬性。透過型別物件的__dict__
屬性訪問,透過父類物件的__dict__
屬性訪問。
如何正確使用Python描述符
如果要在程式碼中使用Python描述符,則只需實現描述符協議。該協議最重要的方法是__get__()
和__set__()
,它們具有以下簽名:
__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None
在實現協議時,請記住以下幾點:
- self是您正在編寫的描述符的例項。
- obj是描述符附加到的物件的例項。
- type是描述符附加到的物件的型別。
在__set__()
中,您沒有型別變數,因為您只能在物件上呼叫__set__()
。相反,您可以在物件和類上都呼叫__get__()
。
要知道的另一件事是,每個類僅將Python描述符例項化一次。這意味著包含描述符的類的每個例項都共享該描述符例項。這是您可能不會想到的,並且可能導致經典的陷阱,如下所示:
# descriptors2.py
class OneDigitNumericValue():
def __init__(self):
self.value = 0
def __get__(self, obj, type=None) -> object:
return self.value
def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value = value
class Foo():
number = OneDigitNumericValue()
my_foo_object = Foo()
my_second_foo_object = Foo()
my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)
my_third_foo_object = Foo()
print(my_third_foo_object.number)
在這裡,您有一個Foo類,它定義一個屬性number,它是一個描述符。該描述符接受一個數字數值並將其儲存在描述符本身的屬性中。但是,這種方法行不通,因為Foo的每個例項都共享相同的描述符例項。您實質上建立的只是一個新的類屬性。
嘗試執行程式碼並檢查輸出:
$ python descriptors2.py
3
3
3
您可以看到Foo的所有例項都具有相同的屬性編號值,即使最後一個例項是在設定my_foo_object.number屬性之後建立的。
那麼,如何解決這個問題呢?您可能會認為,最好使用字典來儲存描述符所附加的所有物件的所有描述符值。這似乎是一個不錯的解決方案,因為__get__()
和__set__()
具有obj屬性,這是您附加到的物件的例項。您可以將此值用作字典的鍵。
不幸的是,此解決方案有很大的缺點,您可以在以下示例中看到:
# descriptors3.py
class OneDigitNumericValue():
def __init__(self):
self.value = {}
def __get__(self, obj, type=None) -> object:
try:
return self.value[obj]
except:
return 0
def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value[obj] = value
class Foo():
number = OneDigitNumericValue()
my_foo_object = Foo()
my_second_foo_object = Foo()
my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)
my_third_foo_object = Foo()
print(my_third_foo_object.number)
在此示例中,您使用字典來儲存描述符內所有物件的number屬性值。執行此程式碼時,您會看到它執行正常並且行為符合預期:
$ python descriptors3.py
3
0
0
不幸的是,這裡的缺點是描述符對所有者物件(描述符附加到的物件例項)保持強引用。這意味著如果銷燬物件,則不會釋放記憶體,因為垃圾收集器會在描述符中查詢到對該物件的引用!
您可能認為這裡的解決方案可能是使用弱引用。儘管可能那樣,但您必須面對這樣一個事實,即並非所有事物都可以被認為是弱的,並且當您收集物件時,它們會從字典中消失。
您可能認為這裡的解決方案可能是使用弱引用。儘管可能那樣,但您必須面對這樣一個事實,並不是所有型別(tuple,int)都支援弱引用,並且當您收集物件時,它們會從字典中消失。
最好的解決方案是不將值儲存在描述符本身中,而是將它們儲存在描述符所附加的物件例項中。接下來嘗試這種方法:
# descriptors4.py
class OneDigitNumericValue():
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None) -> object:
return obj.__dict__.get(self.name) or 0
def __set__(self, obj, value) -> None:
obj.__dict__[self.name] = value
class Foo():
number = OneDigitNumericValue("number")
my_foo_object = Foo()
my_second_foo_object = Foo()
my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)
my_third_foo_object = Foo()
print(my_third_foo_object.number)
在此示例中,當您為物件的number屬性設定一個值時,描述符將使用與描述符本身相同的名稱將其儲存在所附加物件的__dict__
屬性中。 唯一的問題是,在例項化描述符時,必須將名稱指定為引數:
number = OneDigitNumericValue("number")
number= OneDigitNumericValue()
是更好的方案嗎?可能是,但如果您執行的Python版本低於3.6,您將需要一些帶有元類和裝飾器的魔法。但是,如果您使用Python 3.6或更高版本,描述符協議具有一個新方法__ set_name __()
,它可以為您完成所有這些魔法,是在 PEP 487提出的:
__set_name__(self, owner, name)
使用此新方法,無論何時例項化描述符,都會呼叫此方法並自動設定name引數。
現在,嘗試為Python 3.6及更高版本重寫前面的示例:
# descriptors5.py
class OneDigitNumericValue():
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None) -> object:
return obj.__dict__.get(self.name) or 0
def __set__(self, obj, value) -> None:
obj.__dict__[self.name] = value
class Foo():
number = OneDigitNumericValue()
my_foo_object = Foo()
my_second_foo_object = Foo()
my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)
my_third_foo_object = Foo()
print(my_third_foo_object.number)
現在,已刪除__init__()
並實現了__ set_name __()
。這樣就可以建立描述符,而無需指定用於儲存值的內部屬性的名稱。您的程式碼現在看起來也更好更乾淨了!
再執行一次此示例,以確保一切正常:
$ python descriptors5.py
3
0
0
如果您使用的是Python 3.6或更高版本,則此示例應該可以正常執行。
為什麼要使用Python描述符
現在,您知道什麼是Python描述符,以及Python本身如何使用它們來支援其某些功能,例如方法和屬性。您還瞭解瞭如何建立Python描述符,同時避免了一些常見的陷阱。現在一切都應該清楚了,但是您可能仍然想知道為什麼要使用它們。
以我的經驗,我認識許多高階Python開發人員,他們以前從未使用過此功能,也不需要它。這是很正常的,因為在很多情況下都不需要使用Python描述符。但是,這並不意味著Python描述符僅僅是針對高階使用者的學術主題。仍然有一些很好的用例可以證明學習使用描述符是值得的。
Lazy Properties
第一個也是最直接的示例是惰性屬性。這些屬性的初始值只有在首次訪問它們時才會載入。然後,他們載入其初始值並保留該值以供以後重用。 考慮以下示例。您有一個DeepThought類,其中包含一個方法meaning_of_life(),該方法在很久之後會返回一個值:
# slow_properties.py
import random
import time
class DeepThought:
def meaning_of_life(self):
time.sleep(3)
return 42
my_deep_thought_instance = DeepThought()
print(my_deep_thought_instance.meaning_of_life())
print(my_deep_thought_instance.meaning_of_life())
print(my_deep_thought_instance.meaning_of_life())
如果您執行此程式碼並嘗試訪問該方法三次,則每三秒鐘您將得到一個答案,這是方法內部睡眠時間的長度。
現在,惰性屬性可以在第一次執行此方法時對其進行一次評估。然後,它將快取結果值,這樣,如果再次需要它,就可以立即獲得它。您可以使用Python描述符來實現:
# lazy_properties.py
import random
import time
class LazyProperty:
def __init__(self, function):
self.function = function
self.name = function.__name__
def __get__(self, obj, type=None) -> object:
obj.__dict__[self.name] = self.function(obj)
return obj.__dict__[self.name]
class DeepThought:
@LazyProperty
def meaning_of_life(self):
time.sleep(3)
return 42
my_deep_thought_instance = DeepThought()
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)
花些時間研究此程式碼並瞭解其工作原理。您可以在這裡看到Python描述符的功能嗎?在此示例中,當您使用@LazyProperty裝飾器時,mean_of_life將變成LazyProperty的一個例項(跟@property裝飾器作用一樣)。該描述符將方法及其名稱都儲存為例項變數。
因為它是一個非資料描述符,所以當您第一次訪問meaning_of_life屬性的值時,將自動呼叫__get__()
並在my_deep_thought_instance物件上執行meaning_of_life()。結果值儲存在物件本身的__dict__
屬性中。當您再次訪問Meaning_of_life屬性時,Python將使用查詢鏈在__dict__
屬性中查詢該屬性的值,並且該值將立即返回。
譯者注 : 實現惰性求值(訪問時才計算,並將值快取)利用了obj.__dict__
優先順序高於 non-data descriptor 的特性第一次呼叫__get__
以同名屬性存於例項字典中,之後就不再呼叫__get__
請注意,此方法之所以有效,是因為在此示例中,您僅使用了描述符協議的一種方法__get__()
。您只實現了一個非資料描述符。如果您實現了資料描述符,那麼該技巧將無法奏效。在查詢鏈之後,它將優先於__dict__
中儲存的值。要對此進行測試,請執行以下程式碼:
# wrong_lazy_properties.py
import random
import time
class LazyProperty:
def __init__(self, function):
self.function = function
self.name = function.__name__
def __get__(self, obj, type=None) -> object:
obj.__dict__[self.name] = self.function(obj)
return obj.__dict__[self.name]
def __set__(self, obj, value):
pass
class DeepThought:
@LazyProperty
def meaning_of_life(self):
time.sleep(3)
return 42
my_deep_tought_instance = DeepThought()
print(my_deep_tought_instance.meaning_of_life)
print(my_deep_tought_instance.meaning_of_life)
print(my_deep_tought_instance.meaning_of_life)
在此示例中,您可以看到實現__set__()
以後,即使它根本不執行任何操作,也會建立一個資料描述符。現在,惰性屬性的訣竅不再起作用。
D.R.Y. Code
描述符的另一個典型用例是編寫可重用的程式碼並使程式碼 D.R.Y. (DRY原則)Python描述符為開發人員提供了一個出色的工具,可以編寫可在不同屬性甚至不同類之間共享的可重用程式碼。
考慮一個示例,其中您具有五個具有相同行為的不同屬性。每個屬性只能設定為特定值,即它要麼是偶數要麼為0:
class Values:
def __init__(self):
self._value1 = 0
self._value2 = 0
self._value3 = 0
self._value4 = 0
self._value5 = 0
@property
def value1(self):
return self._value1
@value1.setter
def value1(self, value):
self._value1 = value if value % 2 == 0 else 0
@property
def value2(self):
return self._value2
@value2.setter
def value2(self, value):
self._value2 = value if value % 2 == 0 else 0
@property
def value3(self):
return self._value3
@value3.setter
def value3(self, value):
self._value3 = value if value % 2 == 0 else 0
@property
def value4(self):
return self._value4
@value4.setter
def value4(self, value):
self._value4 = value if value % 2 == 0 else 0
@property
def value5(self):
return self._value5
@value5.setter
def value5(self, value):
self._value5 = value if value % 2 == 0 else 0
my_values = Values()
my_values.value1 = 1
my_values.value2 = 4
print(my_values.value1)
print(my_values.value2)
如您所見,這裡有很多重複的程式碼。可以使用Python描述符在所有屬性之間共享行為。您可以建立一個EvenNumber描述符,並將其用於所有這樣的屬性:
# properties2.py
class EvenNumber:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None) -> object:
return obj.__dict__.get(self.name) or 0
def __set__(self, obj, value) -> None:
obj.__dict__[self.name] = (value if value % 2 == 0 else 0)
class Values:
value1 = EvenNumber()
value2 = EvenNumber()
value3 = EvenNumber()
value4 = EvenNumber()
value5 = EvenNumber()
my_values = Values()
my_values.value1 = 1
my_values.value2 = 4
print(my_values.value1)
print(my_values.value2)
這段程式碼看起來好多了!重複項不復存在,現在可以在一個地方實現邏輯,因此,如果需要更改它,則可以輕鬆實現。
結論
既然您已經知道Python如何使用描述符來支援其一些強大功能,那麼您將成為一個更具意識的開發人員,能夠瞭解為什麼某些Python功能已經按這種方式實現。
您已經瞭解到:
- 什麼是Python描述符以及何時使用它們
- 在Python內部使用描述符的地方
- 如何實現自己的描述符
而且,您現在知道了一些特定的用例,其中Python描述符特別有用。例如,當您具有必須在許多屬性(甚至不同類的屬性)之間共享的常見行為時,描述符非常有用。
後記
最近接觸到 Python 描述符的概念,官方檔案沒太看懂,搜了一大圈發現 realpython 這篇文章可以,就順便翻譯過來了,如有翻譯不當的地方,歡迎拍磚。如果想更深入地瞭解Python描述符,請檢視官方的Python描述符指南。