描述器使用指南?
- 作者
Raymond Hettinger(譯者:wh2099 at outlook dot com)
- 聯(lián)系方式
<python at rcn dot com>
目錄
描述器 讓對象能夠自定義屬性查找、存儲和刪除的操作。
本指南主要分為四個部分:
“入門” 部分從簡單的示例著手,逐步添加特性,從而給出基本的概述。如果你是剛接觸到描述器,請從這里開始。
第二部分展示了完整的、實用的描述器示例。如果您已經(jīng)掌握了基礎(chǔ)知識,請從此處開始。
第三部分提供了更多技術(shù)教程,詳細(xì)介紹了描述器如何工作。大多數(shù)人并不需要深入到這種程度。
最后一部分有對內(nèi)置描述器(用 C 編寫)的純 Python 等價實現(xiàn)。如果您想了解函數(shù)如何變成綁定方法或?qū)?
classmethod()
,staticmethod()
,property()
和 __slots__ 這類常見工具的實現(xiàn)感興趣,請閱讀此部分。
入門?
現(xiàn)在,讓我們從最基本的示例開始,然后逐步添加新功能。
簡單示例:返回常量的描述器?
Ten
類是一個描述器,其 __get__()
方法總是返回常量 10
:
class Ten:
def __get__(self, obj, objtype=None):
return 10
要使用描述器,它必須作為一個類變量存儲在另一個類中:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
用交互式會話查看普通屬性查找和描述器查找之間的區(qū)別:
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
在 a.x
屬性查找中,點運算符會找到存儲在類字典中的 'x': 5
。 在 a.y
查找中,點運算符會根據(jù)描述器實例的 __get__
方法將其識別出來,調(diào)用該方法并返回 10
。
請注意,值 10
既不存儲在類字典中也不存儲在實例字典中。相反,值 10
是在調(diào)用時才取到的。
這個簡單的例子展示了一個描述器是如何工作的,但它不是很有用。在查找常量時,用常規(guī)屬性查找會更好。
在下一節(jié)中,我們將創(chuàng)建更有用的東西,即動態(tài)查找。
動態(tài)查找?
有趣的描述器通常運行計算而不是返回常量:
import os
class DirectorySize:
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # Descriptor instance
def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute
交互式會話顯示查找是動態(tài)的,每次都會計算不同的,經(jīng)過更新的返回值:
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size # The songs directory has twenty files
20
>>> g.size # The games directory has three files
3
>>> os.remove('games/chess') # Delete a game
>>> g.size # File count is automatically updated
2
除了說明描述器如何運行計算,這個例子也揭示了 __get__()
參數(shù)的目的。形參 self 接收的實參是 size,即 DirectorySize 的一個實例。形參 obj 接收的實參是 g 或 s,即 Directory 的一個實例。而正是 obj 讓 __get__()
方法獲得了作為目標(biāo)的目錄。形參 objtype 接收的實參是 Directory 類。
托管屬性?
描述器的一種流行用法是托管對實例數(shù)據(jù)的訪問。描述器被分配給類字典中的公開屬性,而實際數(shù)據(jù)作為私有屬性存儲在實例字典中。當(dāng)訪問公開屬性時,會觸發(fā)描述器的 __get__()
和 __set__()
方法。
在下面的例子中,age 是公開屬性,_age 是私有屬性。當(dāng)訪問公開屬性時,描述器會記錄下查找或更新的日志:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
def __get__(self, obj, objtype=None):
value = obj._age
logging.info('Accessing %r giving %r', 'age', value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', 'age', value)
obj._age = value
class Person:
age = LoggedAgeAccess() # Descriptor instance
def __init__(self, name, age):
self.name = name # Regular instance attribute
self.age = age # Calls __set__()
def birthday(self):
self.age += 1 # Calls both __get__() and __set__()
交互式會話展示中,對托管屬性 age 的所有訪問都被記錄了下來,但常規(guī)屬性 name 則未被記錄:
>>> mary = Person('Mary M', 30) # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
>>> vars(mary) # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}
>>> mary.age # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.() # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name # Regular attribute lookup isn't logged
'David D'
>>> dave.age # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40
此示例的一個主要問題是私有名稱 _age 在類 LoggedAgeAccess 中是硬耦合的。這意味著每個實例只能有一個用于記錄的屬性,并且其名稱不可更改。
定制名稱?
當(dāng)一個類使用描述器時,它可以告知每個描述器使用了什么變量名。
在此示例中, Person
類具有兩個描述器實例 name 和 age。當(dāng)類 Person
被定義的時候,他回調(diào)了 LoggedAccess 中的 __set_name__()
來記錄字段名稱,讓每個描述器擁有自己的 public_name 和 private_name:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
logging.info('Accessing %r giving %r', self.public_name, value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', self.public_name, value)
setattr(obj, self.private_name, value)
class Person:
name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age):
self.name = name # Calls the first descriptor
self.age = age # Calls the second descriptor
def birthday(self):
self.age += 1
交互交互式會話顯示類 Person
調(diào)用了 __set_name__()
方法來記錄字段的名稱。在這里,我們調(diào)用 vars()
來查找描述器而不觸發(fā)它:
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
現(xiàn)在,新類會記錄對 name 和 age 二者的訪問:
>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20
這兩個 Person 實例僅包含私有名稱:
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
結(jié)束語?
descriptor 就是任何一個定義了 __get__()
,__set__()
或 __delete__()
的對象。
可選地,描述器可以具有 __set_name__()
方法。這僅在描述器需要知道創(chuàng)建它的類或分配給它的類變量名稱時使用。(即使該類不是描述器,只要此方法存在就會調(diào)用。)
在屬性查找期間,描述器由點運算符調(diào)用。如果使用 vars(some_class)[descriptor_name]
間接訪問描述器,則返回描述器實例而不調(diào)用它。
描述器僅在用作類變量時起作用。放入實例時,它們將失效。
描述器的主要目的是提供一個掛鉤,允許存儲在類變量中的對象控制在屬性查找期間發(fā)生的情況。
傳統(tǒng)上,調(diào)用類控制查找過程中發(fā)生的事情。描述器反轉(zhuǎn)了這種關(guān)系,并允許正在被查詢的數(shù)據(jù)對此進(jìn)行干涉。
描述器的使用貫穿了整個語言。就是它讓函數(shù)變成綁定方法。常見工具諸如 classmethod()
, staticmethod()
,property()
和 functools.cached_property()
都作為描述器實現(xiàn)。
完整的實際例子?
在此示例中,我們創(chuàng)建了一個實用而強(qiáng)大的工具來查找難以發(fā)現(xiàn)的數(shù)據(jù)損壞錯誤。
驗證器類?
驗證器是一個用于托管屬性訪問的描述器。在存儲任何數(shù)據(jù)之前,它會驗證新值是否滿足各種類型和范圍限制。如果不滿足這些限制,它將引發(fā)異常,從源頭上防止數(shù)據(jù)損壞。
這個 Validator
類既是一個 abstract base class 也是一個托管屬性描述器。
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
自定義驗證器需要從 Validator
繼承,并且必須提供 validate()
方法以根據(jù)需要測試各種約束。
自定義驗證器?
這是三個實用的數(shù)據(jù)驗證工具:
OneOf
驗證值是一組受約束的選項之一。Number
驗證值是否為int
或float
。根據(jù)可選參數(shù),它還可以驗證值在給定的最小值或最大值之間。String
驗證值是否為str
。根據(jù)可選參數(shù),它可以驗證給定的最小或最大長度。它還可以驗證用戶定義的 predicate。
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
實際應(yīng)用?
這是在真實類中使用數(shù)據(jù)驗證器的方法:
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
描述器阻止無效實例的創(chuàng)建:
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
Traceback (most recent call last):
...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
Traceback (most recent call last):
...
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid
技術(shù)教程?
接下來是專業(yè)性更強(qiáng)的技術(shù)教程,以及描述器工作原理的詳細(xì)信息。
摘要?
定義描述器,總結(jié)協(xié)議,并說明如何調(diào)用描述器。提供一個展示對象關(guān)系映射如何工作的示例。
學(xué)習(xí)描述器不僅能提供接觸到更多工具集的途徑,還能更深地理解 Python 工作的原理。
定義與介紹?
一般而言,描述器是一個包含了描述器協(xié)議中的方法的屬性值。 這些方法有 __get__()
, __set__()
和 __delete__()
。 如果為某個屬性定義了這些方法中的任意一個,它就可以被稱為 descriptor。
屬性訪問的默認(rèn)行為是從一個對象的字典中獲取、設(shè)置或刪除屬性。對于實例來說,a.x
的查找順序會從 a.__dict__['x']
開始,然后是 type(a).__dict__['x']
,接下來依次查找 type(a)
的方法解析順序(MRO)。 如果找到的值是定義了某個描述器方法的對象,則 Python 可能會重寫默認(rèn)行為并轉(zhuǎn)而發(fā)起調(diào)用描述器方法。這具體發(fā)生在優(yōu)先級鏈的哪個環(huán)節(jié)則要根據(jù)所定義的描述器方法及其被調(diào)用的方式來決定。
描述器是一個強(qiáng)大而通用的協(xié)議。 它們是屬性、方法、靜態(tài)方法、類方法和 super()
背后的實現(xiàn)機(jī)制。 它們在 Python 內(nèi)部被廣泛使用。 描述器簡化了底層的 C 代碼并為 Python 的日常程序提供了一組靈活的新工具。
描述器協(xié)議?
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
描述器的方法就這些。一個對象只要定義了以上方法中的任何一個,就被視為描述器,并在被作為屬性時覆蓋其默認(rèn)行為。
如果一個對象定義了 __set__()
或 __delete__()
,則它會被視為數(shù)據(jù)描述器。 僅定義了 __get__()
的描述器稱為非數(shù)據(jù)描述器(它們經(jīng)常被用于方法,但也可以有其他用途)。
數(shù)據(jù)和非數(shù)據(jù)描述器的不同之處在于,如何計算實例字典中條目的替代值。如果實例的字典具有與數(shù)據(jù)描述器同名的條目,則數(shù)據(jù)描述器優(yōu)先。如果實例的字典具有與非數(shù)據(jù)描述器同名的條目,則該字典條目優(yōu)先。
為了使數(shù)據(jù)描述器成為只讀的,應(yīng)該同時定義 __get__()
和 __set__()
,并在 __set__()
中引發(fā) AttributeError
。用引發(fā)異常的占位符定義 __set__()
方法使其成為數(shù)據(jù)描述器。
描述器調(diào)用概述?
描述器可以通過 d.__get__(obj)
或 desc.__get__(None, cls)
直接調(diào)用。
但更常見的是通過屬性訪問自動調(diào)用描述器。
表達(dá)式 obj.x
在命名空間的鏈中查找``obj`` 的屬性 x
。如果搜索在實例 __dict__
之外找到描述器,則根據(jù)下面列出的優(yōu)先級規(guī)則調(diào)用其 __get__()
方法。
調(diào)用的細(xì)節(jié)取決于 obj
是對象、類還是超類的實例。
通過實例調(diào)用?
實例查找通過命名空間鏈進(jìn)行掃描,數(shù)據(jù)描述器的優(yōu)先級最高,其次是實例變量、非數(shù)據(jù)描述器、類變量,最后是 __getattr__()
(如果存在的話)。
如果 a.x
找到了一個描述器,那么將通過 desc.__get__(a, type(a))
調(diào)用它。
點運算符的查找邏輯在 object.__getattribute__()
中。這里是一個等價的純 Python 實現(xiàn):
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
Note, there is no __getattr__()
hook in the __getattribute__()
code. That is why calling __getattribute__()
directly or with
super().__getattribute__
will bypass __getattr__()
entirely.
Instead, it is the dot operator and the getattr()
function that are
responsible for invoking __getattr__()
whenever __getattribute__()
raises an AttributeError
. Their logic is encapsulated in a helper
function:
def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__
通過類調(diào)用?
像 A.x
這樣的點操作符查找的邏輯在 type.__getattribute__()
中。步驟與 object.__getattribute__()
相似,但是實例字典查找改為搜索類的 method resolution order。
如果找到了一個描述器,那么將通過 desc.__get__(None, A)
調(diào)用它。
完整的 C 實現(xiàn)可在 Objects/typeobject.c 中的 type_getattro()
和 _PyType_Lookup()
找到。
通過 super 調(diào)用?
super 的點操作符查找的邏輯在 super()
返回的對象的 __getattribute__()
方法中。
類似 super(A, obj).m
形式的點分查找將在 obj.__class__.__mro__
中搜索緊接在 A
之后的基類 B
,然后返回 B.__dict__['m'].__get__(obj, A)
。如果 m
不是描述器,則直接返回其值。
完整的 C 實現(xiàn)可以在 Objects/typeobject.c 的 super_getattro()
中找到。純 Python 等價實現(xiàn)可以在 Guido's Tutorial 中找到。
調(diào)用邏輯總結(jié)?
描述器的機(jī)制嵌入在 object
,type
和 super()
的 __getattribute__()
方法中。
要記住的重要點是:
描述器由
__getattribute__()
方法調(diào)用。由于描述器的邏輯在
__getattribute__()
中,因而重寫該方法會阻止描述器的自動調(diào)用。object.__getattribute__()
和type.__getattribute__()
會用不同的方式調(diào)用__get__()
。前一個會傳入實例,也可以包括類。后一個傳入的實例為None
,并且總是包括類。數(shù)據(jù)描述器始終會覆蓋實例字典。
非數(shù)據(jù)描述器會被實例字典覆蓋。
自動名稱通知?
有時,描述器想知道它分配到的具體類變量名。創(chuàng)建新類時,元類 type
將掃描新類的字典。如果有描述器,并且它們定義了 __set_name__()
,則使用兩個參數(shù)調(diào)用該方法。owner 是使用描述器的類,name 是分配給描述器的類變量名。
實現(xiàn)的細(xì)節(jié)在 Objects/typeobject.c 中的 type_new()
和 set_names()
。
由于更新邏輯在 type.__new__()
中,因此通知僅在創(chuàng)建類時發(fā)生。之后如果將描述器添加到類中,則需要手動調(diào)用 __set_name__()
。
ORM (對象關(guān)系映射)示例?
以下代碼展示了如何使用數(shù)據(jù)描述器來實現(xiàn)簡單 object relational mapping 框架。
其核心思路是將數(shù)據(jù)存儲在外部數(shù)據(jù)庫中,Python 實例僅持有數(shù)據(jù)庫表中對應(yīng)的的鍵。描述器負(fù)責(zé)對值進(jìn)行查找或更新:
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
我們可以用 Field
類來定義描述了數(shù)據(jù)庫中每張表的模式的 models。
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
def __init__(self, key):
self.key = key
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
要使用模型,首先要連接到數(shù)據(jù)庫:
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
交互式會話顯示了如何從數(shù)據(jù)庫中檢索數(shù)據(jù)及如何對其進(jìn)行更新:
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'
純 Python 等價實現(xiàn)?
描述器協(xié)議很簡單,但它提供了令人興奮的可能性。有幾個用例非常通用,以至于它們已預(yù)先打包到內(nèi)置工具中。屬性、綁定方法、靜態(tài)方法、類方法和 __slots__ 均基于描述器協(xié)議。
屬性?
調(diào)用 property()
是構(gòu)建數(shù)據(jù)描述器的簡潔方式,該數(shù)據(jù)描述器在訪問屬性時觸發(fā)函數(shù)調(diào)用。它的簽名是:
property(fget=None, fset=None, fdel=None, doc=None) -> property
該文檔顯示了定義托管屬性 x
的典型用法:
class C:
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
要了解 property()
如何根據(jù)描述器協(xié)議實現(xiàn),這里是一個純 Python 的等價實現(xiàn):
class Property:
"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
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"property '{self._name}' has no setter")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"property '{self._name}' has no deleter")
self.fdel(obj)
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
這個內(nèi)置的 property()
每當(dāng)用戶訪問屬性時生效,隨后的變化需要一個方法的參與。
例如,一個電子表格類可以通過 Cell('b10').value
授予對單元格值的訪問權(quán)限。對程序的后續(xù)改進(jìn)要求每次訪問都要重新計算單元格;但是,程序員不希望影響直接訪問該屬性的現(xiàn)有客戶端代碼。解決方案是將對 value 屬性的訪問包裝在屬性數(shù)據(jù)描述器中:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
在此示例中,內(nèi)置的 property()
或我們實現(xiàn)的的 Property()
均適用。
函數(shù)和方法?
Python 的面向?qū)ο蠊δ苁窃诨诤瘮?shù)的環(huán)境構(gòu)建的。通過使用非數(shù)據(jù)描述器,這兩方面完成了無縫融合。
在調(diào)用時,存儲在類詞典中的函數(shù)將被轉(zhuǎn)換為方法。方法與常規(guī)函數(shù)的不同之處僅在于對象實例被置于其他參數(shù)之前。方法與常規(guī)函數(shù)的不同之處僅在于第一個參數(shù)是為對象實例保留的。按照慣例,實例引用稱為 self ,但也可以稱為 this 或任何其他變量名稱。
可以使用 types.MethodType
手動創(chuàng)建方法,其行為基本等價于:
class MethodType:
"Emulate PyMethod_Type in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
為了支持自動創(chuàng)建方法,函數(shù)包含 __get__()
方法以便在屬性訪問時綁定其為方法。這意味著函數(shù)其是非數(shù)據(jù)描述器,它在通過實例進(jìn)行點查找時返回綁定方法,其運作方式如下:
class Function:
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return MethodType(self, obj)
在解釋器中運行以下類,這顯示了函數(shù)描述器的實際工作方式:
class D:
def f(self, x):
return x
該函數(shù)具有 qualified name 屬性以支持自?。?/p>
>>> D.f.__qualname__
'D.f'
通過類字典訪問函數(shù)不會調(diào)用 __get__()
。相反,它只返回基礎(chǔ)函數(shù)對象:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
來自類的點運算符訪問會調(diào)用 __get__()
,直接返回底層的函數(shù)。
>>> D.f
<function D.f at 0x00C45070>
有趣的行為發(fā)生在從實例進(jìn)行點訪問期間。點運算符查找調(diào)用 __get__()
,返回綁定的方法對象:
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
綁定方法在內(nèi)部存儲了底層函數(shù)和綁定的實例:
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
如果你曾好奇常規(guī)方法中的 self 或類方法中的 cls 是從什么地方來的,就是這里了!
方法的種類?
非數(shù)據(jù)描述器為把函數(shù)綁定為方法的通常模式提供了一種簡單的機(jī)制。
概括地說,函數(shù)對象具有 __get__()
方法,以便在作為屬性訪問時可以將其轉(zhuǎn)換為方法。非數(shù)據(jù)描述器將 obj.f(*args)
的調(diào)用會被轉(zhuǎn)換為 f(obj, *args)
。調(diào)用 klass.f(*args)` 因而變成 f(*args)
。
下表總結(jié)了綁定及其兩個最有用的變體:
轉(zhuǎn)換形式
通過對象調(diào)用
通過類調(diào)用
function -- 函數(shù)
f(obj, *args)
f(*args)
靜態(tài)方法
f(*args)
f(*args)
類方法
f(type(obj), *args)
f(cls, *args)
靜態(tài)方法?
靜態(tài)方法返回底層函數(shù),不做任何更改。調(diào)用 c.f
或 C.f
等效于通過 object.__getattribute__(c, "f")
或 object.__getattribute__(C, "f")
查找。這樣該函數(shù)就可以從對象或類中進(jìn)行相同的訪問。
適合作為靜態(tài)方法的是那些不引用 self
變量的方法。
例如,一個統(tǒng)計用的包可能包含一個實驗數(shù)據(jù)的容器類。該容器類提供了用于計算數(shù)據(jù)的平均值,均值,中位數(shù)和其他描述性統(tǒng)計信息的常規(guī)方法。但是,可能有在概念上相關(guān)但不依賴于數(shù)據(jù)的函數(shù)。例如, erf(x)
是在統(tǒng)計中的便捷轉(zhuǎn)換,但并不直接依賴于特定的數(shù)據(jù)集。可以從對象或類中調(diào)用它: s.erf(1.5) --> .9332
或 Sample.erf(1.5) --> .9332
。
由于靜態(tài)方法返回的底層函數(shù)沒有任何變化,因此示例調(diào)用也是意料之中:
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
使用非數(shù)據(jù)描述器,純 Python 版本的 staticmethod()
如下所示:
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
類方法?
與靜態(tài)方法不同,類方法在調(diào)用函數(shù)之前將類引用放在參數(shù)列表的最前。無論調(diào)用方是對象還是類,此格式相同:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
當(dāng)方法僅需要具有類引用并且確實依賴于存儲在特定實例中的數(shù)據(jù)時,此行為就很有用。類方法的一種用途是創(chuàng)建備用類構(gòu)造函數(shù)。例如,類方法 dict.fromkeys()
從鍵列表創(chuàng)建一個新字典。純 Python 的等價實現(xiàn)是:
class Dict(dict):
@classmethod
def fromkeys(cls, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c"
d = cls()
for key in iterable:
d[key] = value
return d
現(xiàn)在可以這樣構(gòu)造一個新的唯一鍵字典:
>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
使用非數(shù)據(jù)描述器協(xié)議,純 Python 版本的 classmethod()
如下:
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
The code path for hasattr(type(self.f), '__get__')
was added in
Python 3.9 and makes it possible for classmethod()
to support
chained decorators. For example, a classmethod and property could be
chained together. In Python 3.11, this functionality was deprecated.
class G:
@classmethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"
成員對象和 __slots__?
當(dāng)一個類定義了 __slots__
,它會用一個固定長度的 slot 值數(shù)組來替換實例字典。 從用戶的視角看,效果是這樣的:
1. Provides immediate detection of bugs due to misspelled attribute
assignments. Only attribute names specified in __slots__
are allowed:
class Vehicle:
__slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
2. Helps create immutable objects where descriptors manage access to private
attributes stored in __slots__
:
class Immutable:
__slots__ = ('_dept', '_name') # Replace the instance dictionary
def __init__(self, dept, name):
self._dept = dept # Store to private attribute
self._name = name # Store to private attribute
@property # Read-only descriptor
def dept(self):
return self._dept
@property
def name(self): # Read-only descriptor
return self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
...
AttributeError: property 'dept' of 'Immutable' object has no setter
>>> mark.location = 'Mars'
Traceback (most recent call last):
...
AttributeError: 'Immutable' object has no attribute 'location'
3. Saves memory. On a 64-bit Linux build, an instance with two attributes
takes 48 bytes with __slots__
and 152 bytes without. This flyweight
design pattern likely only
matters when a large number of instances are going to be created.
4. Improves speed. Reading instance variables is 35% faster with
__slots__
(as measured with Python 3.10 on an Apple M1 processor).
5. Blocks tools like functools.cached_property()
which require an
instance dictionary to function correctly:
from functools import cached_property
class CP:
__slots__ = () # Eliminates the instance dict
@cached_property # Requires an instance dict
def pi(self):
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
for n in reversed(range(100_000)))
>>> CP().pi
Traceback (most recent call last):
...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
要創(chuàng)建一個一模一樣的純 Python 版的 __slots__
是不可能的,因為它需要直接訪問 C 結(jié)構(gòu)體并控制對象內(nèi)存分配。 但是,我們可以構(gòu)建一個非常相似的模擬版,其中作為 slot 的實際 C 結(jié)構(gòu)體由一個私有的 _slotvalues
列表來模擬。 對該私有結(jié)構(gòu)體的讀寫操作將由成員描述器來管理:
null = object()
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/structmember.h'
# Also see descr_new() in Objects/descrobject.c
self.name = name
self.clsname = clsname
self.offset = offset
def __get__(self, obj, objtype=None):
'Emulate member_get() in Objects/descrobject.c'
# Also see PyMember_GetOne() in Python/structmember.c
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
return value
def __set__(self, obj, value):
'Emulate member_set() in Objects/descrobject.c'
obj._slotvalues[self.offset] = value
def __delete__(self, obj):
'Emulate member_delete() in Objects/descrobject.c'
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
obj._slotvalues[self.offset] = null
def __repr__(self):
'Emulate member_repr() in Objects/descrobject.c'
return f'<Member {self.name!r} of {self.clsname!r}>'
type.__new__()
方法負(fù)責(zé)將成員對象添加到類變量:
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
def __new__(mcls, clsname, bases, mapping):
'Emulate type_new() in Objects/typeobject.c'
# type_new() calls PyTypeReady() which calls add_methods()
slot_names = mapping.get('slot_names', [])
for offset, name in enumerate(slot_names):
mapping[name] = Member(name, clsname, offset)
return type.__new__(mcls, clsname, bases, mapping)
object.__new__()
方法負(fù)責(zé)創(chuàng)建具有 slot 而非實例字典的實例。 以下是一個純 Python 的粗略模擬版:
class Object:
'Simulate how object.__new__() allocates memory for __slots__'
def __new__(cls, *args):
'Emulate object_new() in Objects/typeobject.c'
inst = super().__new__(cls)
if hasattr(cls, 'slot_names'):
empty_slots = [null] * len(cls.slot_names)
object.__setattr__(inst, '_slotvalues', empty_slots)
return inst
def __setattr__(self, name, value):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{type(self).__name__!r} object has no attribute {name!r}'
)
super().__setattr__(name, value)
def __delattr__(self, name):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{type(self).__name__!r} object has no attribute {name!r}'
)
super().__delattr__(name)
要在真實的類中使用這個模擬版,只需從 Object
繼承并將 metaclass 設(shè)為 Type
:
class H(Object, metaclass=Type):
'Instance variables stored in slots'
slot_names = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
這時,metaclass 已經(jīng)為 x 和 y 加載了成員對象:
>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
'__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'],
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
'x': <Member 'x' of 'H'>,
'y': <Member 'y' of 'H'>}
當(dāng)實例被創(chuàng)建時,它們將擁有一個用于存放屬性的 slot_values
列表:
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
錯誤拼寫或未賦值的屬性將引發(fā)一個異常:
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'