描述器使用指南?
- 作者
Raymond Hettinger(譯者:wh2099 at outlook dot com)
- 聯(lián)系方式
<python at rcn dot com>
目錄
描述器 讓對(duì)象能夠自定義屬性查找、存儲(chǔ)和刪除的操作。
本指南主要分為四個(gè)部分:
“入門(mén)” 部分從簡(jiǎn)單的示例著手,逐步添加特性,從而給出基本的概述。如果你是剛接觸到描述器,請(qǐng)從這里開(kāi)始。
第二部分展示了完整的、實(shí)用的描述器示例。如果您已經(jīng)掌握了基礎(chǔ)知識(shí),請(qǐng)從此處開(kāi)始。
第三部分提供了更多技術(shù)教程,詳細(xì)介紹了描述器如何工作。大多數(shù)人并不需要深入到這種程度。
最后一部分有對(duì)內(nèi)置描述器(用 C 編寫(xiě))的純 Python 等價(jià)實(shí)現(xiàn)。如果您想了解函數(shù)如何變成綁定方法或?qū)?
classmethod()
,staticmethod()
,property()
和 __slots__ 這類(lèi)常見(jiàn)工具的實(shí)現(xiàn)感興趣,請(qǐng)閱讀此部分。
入門(mén)?
現(xiàn)在,讓我們從最基本的示例開(kāi)始,然后逐步添加新功能。
簡(jiǎn)單示例:返回常量的描述器?
Ten
類(lèi)是一個(gè)描述器,其 __get__()
方法總是返回常量 10
:
class Ten:
def __get__(self, obj, objtype=None):
return 10
要使用描述器,它必須作為一個(gè)類(lèi)變量存儲(chǔ)在另一個(gè)類(lèi)中:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
用交互式會(huì)話查看普通屬性查找和描述器查找之間的區(qū)別:
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
在 a.x
屬性查找中,點(diǎn)運(yùn)算符會(huì)找到存儲(chǔ)在類(lèi)字典中的 'x': 5
。 在 a.y
查找中,點(diǎn)運(yùn)算符會(huì)根據(jù)描述器實(shí)例的 __get__
方法將其識(shí)別出來(lái),調(diào)用該方法并返回 10
。
請(qǐng)注意,值 10
既不存儲(chǔ)在類(lèi)字典中也不存儲(chǔ)在實(shí)例字典中。相反,值 10
是在調(diào)用時(shí)才取到的。
這個(gè)簡(jiǎn)單的例子展示了一個(gè)描述器是如何工作的,但它不是很有用。在查找常量時(shí),用常規(guī)屬性查找會(huì)更好。
在下一節(jié)中,我們將創(chuàng)建更有用的東西,即動(dòng)態(tài)查找。
動(dòng)態(tài)查找?
有趣的描述器通常運(yùn)行計(jì)算而不是返回常量:
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
交互式會(huì)話顯示查找是動(dòng)態(tài)的,每次都會(huì)計(jì)算不同的,經(jīng)過(guò)更新的返回值:
>>> 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
除了說(shuō)明描述器如何運(yùn)行計(jì)算,這個(gè)例子也揭示了 __get__()
參數(shù)的目的。形參 self 接收的實(shí)參是 size,即 DirectorySize 的一個(gè)實(shí)例。形參 obj 接收的實(shí)參是 g 或 s,即 Directory 的一個(gè)實(shí)例。而正是 obj 讓 __get__()
方法獲得了作為目標(biāo)的目錄。形參 objtype 接收的實(shí)參是 Directory 類(lèi)。
托管屬性?
描述器的一種流行用法是托管對(duì)實(shí)例數(shù)據(jù)的訪問(wèn)。描述器被分配給類(lèi)字典中的公開(kāi)屬性,而實(shí)際數(shù)據(jù)作為私有屬性存儲(chǔ)在實(shí)例字典中。當(dāng)訪問(wèn)公開(kāi)屬性時(shí),會(huì)觸發(fā)描述器的 __get__()
和 __set__()
方法。
在下面的例子中,age 是公開(kāi)屬性,_age 是私有屬性。當(dāng)訪問(wèn)公開(kāi)屬性時(shí),描述器會(huì)記錄下查找或更新的日志:
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__()
交互式會(huì)話展示中,對(duì)托管屬性 age 的所有訪問(wèn)都被記錄了下來(lái),但常規(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.birthday() # 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
此示例的一個(gè)主要問(wèn)題是私有名稱 _age 在類(lèi) LoggedAgeAccess 中是硬耦合的。這意味著每個(gè)實(shí)例只能有一個(gè)用于記錄的屬性,并且其名稱不可更改。
定制名稱?
當(dāng)一個(gè)類(lèi)使用描述器時(shí),它可以告知每個(gè)描述器使用了什么變量名。
在此示例中, Person
類(lèi)具有兩個(gè)描述器實(shí)例 name 和 age。當(dāng)類(lèi) Person
被定義的時(shí)候,他回調(diào)了 LoggedAccess 中的 __set_name__()
來(lái)記錄字段名稱,讓每個(gè)描述器擁有自己的 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
交互交互式會(huì)話顯示類(lèi) Person
調(diào)用了 __set_name__()
方法來(lái)記錄字段的名稱。在這里,我們調(diào)用 vars()
來(lái)查找描述器而不觸發(fā)它:
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
現(xiàn)在,新類(lèi)會(huì)記錄對(duì) name 和 age 二者的訪問(wèn):
>>> 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
這兩個(gè) Person 實(shí)例僅包含私有名稱:
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
結(jié)束語(yǔ)?
descriptor 就是任何一個(gè)定義了 __get__()
,__set__()
或 __delete__()
的對(duì)象。
可選地,描述器可以具有 __set_name__()
方法。這僅在描述器需要知道創(chuàng)建它的類(lèi)或分配給它的類(lèi)變量名稱時(shí)使用。(即使該類(lèi)不是描述器,只要此方法存在就會(huì)調(diào)用。)
在屬性查找期間,描述器由點(diǎn)運(yùn)算符調(diào)用。如果使用 vars(some_class)[descriptor_name]
間接訪問(wèn)描述器,則返回描述器實(shí)例而不調(diào)用它。
描述器僅在用作類(lèi)變量時(shí)起作用。放入實(shí)例時(shí),它們將失效。
描述器的主要目的是提供一個(gè)掛鉤,允許存儲(chǔ)在類(lèi)變量中的對(duì)象控制在屬性查找期間發(fā)生的情況。
傳統(tǒng)上,調(diào)用類(lèi)控制查找過(guò)程中發(fā)生的事情。描述器反轉(zhuǎn)了這種關(guān)系,并允許正在被查詢的數(shù)據(jù)對(duì)此進(jìn)行干涉。
描述器的使用貫穿了整個(gè)語(yǔ)言。就是它讓函數(shù)變成綁定方法。常見(jiàn)工具諸如 classmethod()
, staticmethod()
,property()
和 functools.cached_property()
都作為描述器實(shí)現(xiàn)。
完整的實(shí)際例子?
在此示例中,我們創(chuàng)建了一個(gè)實(shí)用而強(qiáng)大的工具來(lái)查找難以發(fā)現(xiàn)的數(shù)據(jù)損壞錯(cuò)誤。
驗(yàn)證器類(lèi)?
驗(yàn)證器是一個(gè)用于托管屬性訪問(wèn)的描述器。在存儲(chǔ)任何數(shù)據(jù)之前,它會(huì)驗(yàn)證新值是否滿足各種類(lèi)型和范圍限制。如果不滿足這些限制,它將引發(fā)異常,從源頭上防止數(shù)據(jù)損壞。
這個(gè) Validator
類(lèi)既是一個(gè) abstract base class 也是一個(gè)托管屬性描述器。
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
自定義驗(yàn)證器需要從 Validator
繼承,并且必須提供 validate()
方法以根據(jù)需要測(cè)試各種約束。
自定義驗(yàn)證器?
這是三個(gè)實(shí)用的數(shù)據(jù)驗(yàn)證工具:
OneOf
驗(yàn)證值是一組受約束的選項(xiàng)之一。Number
驗(yàn)證值是否為int
或float
。根據(jù)可選參數(shù),它還可以驗(yàn)證值在給定的最小值或最大值之間。String
驗(yàn)證值是否為str
。根據(jù)可選參數(shù),它可以驗(yàn)證給定的最小或最大長(zhǎng)度。它還可以驗(yàn)證用戶定義的 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}'
)
實(shí)際應(yīng)用?
這是在真實(shí)類(lèi)中使用數(shù)據(jù)驗(yàn)證器的方法:
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
描述器阻止無(wú)效實(shí)例的創(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ù)教程?
接下來(lái)是專業(yè)性更強(qiáng)的技術(shù)教程,以及描述器工作原理的詳細(xì)信息。
摘要?
定義描述器,總結(jié)協(xié)議,并說(shuō)明如何調(diào)用描述器。提供一個(gè)展示對(duì)象關(guān)系映射如何工作的示例。
學(xué)習(xí)描述器不僅能提供接觸到更多工具集的途徑,還能更深地理解 Python 工作的原理。
定義與介紹?
一般而言,描述器是一個(gè)包含了描述器協(xié)議中的方法的屬性值。 這些方法有 __get__()
, __set__()
和 __delete__()
。 如果為某個(gè)屬性定義了這些方法中的任意一個(gè),它就可以被稱為 descriptor。
屬性訪問(wèn)的默認(rèn)行為是從一個(gè)對(duì)象的字典中獲取、設(shè)置或刪除屬性。對(duì)于實(shí)例來(lái)說(shuō),a.x
的查找順序會(huì)從 a.__dict__['x']
開(kāi)始,然后是 type(a).__dict__['x']
,接下來(lái)依次查找 type(a)
的方法解析順序(MRO)。 如果找到的值是定義了某個(gè)描述器方法的對(duì)象,則 Python 可能會(huì)重寫(xiě)默認(rèn)行為并轉(zhuǎn)而發(fā)起調(diào)用描述器方法。這具體發(fā)生在優(yōu)先級(jí)鏈的哪個(gè)環(huán)節(jié)則要根據(jù)所定義的描述器方法及其被調(diào)用的方式來(lái)決定。
描述器是一個(gè)強(qiáng)大而通用的協(xié)議。 它們是屬性、方法、靜態(tài)方法、類(lèi)方法和 super()
背后的實(shí)現(xiàn)機(jī)制。 它們?cè)?Python 內(nèi)部被廣泛使用。 描述器簡(jiǎn)化了底層的 C 代碼并為 Python 的日常程序提供了一組靈活的新工具。
描述器協(xié)議?
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
描述器的方法就這些。一個(gè)對(duì)象只要定義了以上方法中的任何一個(gè),就被視為描述器,并在被作為屬性時(shí)覆蓋其默認(rèn)行為。
如果一個(gè)對(duì)象定義了 __set__()
或 __delete__()
,則它會(huì)被視為數(shù)據(jù)描述器。 僅定義了 __get__()
的描述器稱為非數(shù)據(jù)描述器(它們經(jīng)常被用于方法,但也可以有其他用途)。
數(shù)據(jù)和非數(shù)據(jù)描述器的不同之處在于,如何計(jì)算實(shí)例字典中條目的替代值。如果實(shí)例的字典具有與數(shù)據(jù)描述器同名的條目,則數(shù)據(jù)描述器優(yōu)先。如果實(shí)例的字典具有與非數(shù)據(jù)描述器同名的條目,則該字典條目?jī)?yōu)先。
為了使數(shù)據(jù)描述器成為只讀的,應(yīng)該同時(shí)定義 __get__()
和 __set__()
,并在 __set__()
中引發(fā) AttributeError
。用引發(fā)異常的占位符定義 __set__()
方法使其成為數(shù)據(jù)描述器。
描述器調(diào)用概述?
描述器可以通過(guò) d.__get__(obj)
或 desc.__get__(None, cls)
直接調(diào)用。
但更常見(jiàn)的是通過(guò)屬性訪問(wèn)自動(dòng)調(diào)用描述器。
表達(dá)式 obj.x
在命名空間的鏈中查找``obj`` 的屬性 x
。如果搜索在實(shí)例 __dict__
之外找到描述器,則根據(jù)下面列出的優(yōu)先級(jí)規(guī)則調(diào)用其 __get__()
方法。
調(diào)用的細(xì)節(jié)取決于 obj
是對(duì)象、類(lèi)還是超類(lèi)的實(shí)例。
通過(guò)實(shí)例調(diào)用?
實(shí)例查找通過(guò)命名空間鏈進(jìn)行掃描,數(shù)據(jù)描述器的優(yōu)先級(jí)最高,其次是實(shí)例變量、非數(shù)據(jù)描述器、類(lèi)變量,最后是 __getattr__()
(如果存在的話)。
如果 a.x
找到了一個(gè)描述器,那么將通過(guò) desc.__get__(a, type(a))
調(diào)用它。
點(diǎn)運(yùn)算符的查找邏輯在 object.__getattribute__()
中。這里是一個(gè)等價(jià)的純 Python 實(shí)現(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 (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__
通過(guò)類(lèi)調(diào)用?
像 A.x
這樣的點(diǎn)操作符查找的邏輯在 type.__getattribute__()
中。步驟與 object.__getattribute__()
相似,但是實(shí)例字典查找改為搜索類(lèi)的 method resolution order。
如果找到了一個(gè)描述器,那么將通過(guò) desc.__get__(None, A)
調(diào)用它。
完整的 C 實(shí)現(xiàn)可在 Objects/typeobject.c 中的 type_getattro()
和 _PyType_Lookup()
找到。
通過(guò) super 調(diào)用?
super 的點(diǎn)操作符查找的邏輯在 super()
返回的對(duì)象的 __getattribute__()
方法中。
類(lèi)似 super(A, obj).m
形式的點(diǎn)分查找將在 obj.__class__.__mro__
中搜索緊接在 A
之后的基類(lèi) B
,然后返回 B.__dict__['m'].__get__(obj, A)
。如果 m
不是描述器,則直接返回其值。
完整的 C 實(shí)現(xiàn)可以在 Objects/typeobject.c 的 super_getattro()
中找到。純 Python 等價(jià)實(shí)現(xiàn)可以在 Guido's Tutorial 中找到。
調(diào)用邏輯總結(jié)?
描述器的機(jī)制嵌入在 object
,type
和 super()
的 __getattribute__()
方法中。
要記住的重要點(diǎn)是:
描述器由
__getattribute__()
方法調(diào)用。由于描述器的邏輯在
__getattribute__()
中,因而重寫(xiě)該方法會(huì)阻止描述器的自動(dòng)調(diào)用。object.__getattribute__()
和type.__getattribute__()
會(huì)用不同的方式調(diào)用__get__()
。前一個(gè)會(huì)傳入實(shí)例,也可以包括類(lèi)。后一個(gè)傳入的實(shí)例為None
,并且總是包括類(lèi)。數(shù)據(jù)描述器始終會(huì)覆蓋實(shí)例字典。
非數(shù)據(jù)描述器會(huì)被實(shí)例字典覆蓋。
自動(dòng)名稱通知?
有時(shí),描述器想知道它分配到的具體類(lèi)變量名。創(chuàng)建新類(lèi)時(shí),元類(lèi) type
將掃描新類(lèi)的字典。如果有描述器,并且它們定義了 __set_name__()
,則使用兩個(gè)參數(shù)調(diào)用該方法。owner 是使用描述器的類(lèi),name 是分配給描述器的類(lèi)變量名。
實(shí)現(xiàn)的細(xì)節(jié)在 Objects/typeobject.c 中的 type_new()
和 set_names()
。
由于更新邏輯在 type.__new__()
中,因此通知僅在創(chuàng)建類(lèi)時(shí)發(fā)生。之后如果將描述器添加到類(lèi)中,則需要手動(dòng)調(diào)用 __set_name__()
。
ORM (對(duì)象關(guān)系映射)示例?
以下代碼展示了如何使用數(shù)據(jù)描述器來(lái)實(shí)現(xiàn)簡(jiǎn)單 object relational mapping 框架。
其核心思路是將數(shù)據(jù)存儲(chǔ)在外部數(shù)據(jù)庫(kù)中,Python 實(shí)例僅持有數(shù)據(jù)庫(kù)表中對(duì)應(yīng)的的鍵。描述器負(fù)責(zé)對(duì)值進(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
類(lèi)來(lái)定義描述了數(shù)據(jù)庫(kù)中每張表的模式的 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ù)庫(kù):
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
交互式會(huì)話顯示了如何從數(shù)據(jù)庫(kù)中檢索數(shù)據(jù)及如何對(duì)其進(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 等價(jià)實(shí)現(xiàn)?
描述器協(xié)議很簡(jiǎn)單,但它提供了令人興奮的可能性。有幾個(gè)用例非常通用,以至于它們已預(yù)先打包到內(nèi)置工具中。屬性、綁定方法、靜態(tài)方法、類(lèi)方法和 __slots__ 均基于描述器協(xié)議。
屬性?
調(diào)用 property()
是構(gòu)建數(shù)據(jù)描述器的簡(jiǎn)潔方式,該數(shù)據(jù)描述器在訪問(wèn)屬性時(shí)觸發(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é)議實(shí)現(xiàn),這里是一個(gè)純 Python 的等價(jià)實(shí)現(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
這個(gè)內(nèi)置的 property()
每當(dāng)用戶訪問(wèn)屬性時(shí)生效,隨后的變化需要一個(gè)方法的參與。
例如,一個(gè)電子表格類(lèi)可以通過(guò) Cell('b10').value
授予對(duì)單元格值的訪問(wèn)權(quán)限。對(duì)程序的后續(xù)改進(jìn)要求每次訪問(wèn)都要重新計(jì)算單元格;但是,程序員不希望影響直接訪問(wèn)該屬性的現(xiàn)有客戶端代碼。解決方案是將對(duì) value 屬性的訪問(wèn)包裝在屬性數(shù)據(jù)描述器中:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
在此示例中,內(nèi)置的 property()
或我們實(shí)現(xiàn)的的 Property()
均適用。
函數(shù)和方法?
Python 的面向?qū)ο蠊δ苁窃诨诤瘮?shù)的環(huán)境構(gòu)建的。通過(guò)使用非數(shù)據(jù)描述器,這兩方面完成了無(wú)縫融合。
在調(diào)用時(shí),存儲(chǔ)在類(lèi)詞典中的函數(shù)將被轉(zhuǎn)換為方法。方法與常規(guī)函數(shù)的不同之處僅在于對(duì)象實(shí)例被置于其他參數(shù)之前。方法與常規(guī)函數(shù)的不同之處僅在于第一個(gè)參數(shù)是為對(duì)象實(shí)例保留的。按照慣例,實(shí)例引用稱為 self ,但也可以稱為 this 或任何其他變量名稱。
可以使用 types.MethodType
手動(dòng)創(chuàng)建方法,其行為基本等價(jià)于:
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)
為了支持自動(dòng)創(chuàng)建方法,函數(shù)包含 __get__()
方法以便在屬性訪問(wèn)時(shí)綁定其為方法。這意味著函數(shù)其是非數(shù)據(jù)描述器,它在通過(guò)實(shí)例進(jìn)行點(diǎn)查找時(shí)返回綁定方法,其運(yù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)
在解釋器中運(yùn)行以下類(lèi),這顯示了函數(shù)描述器的實(shí)際工作方式:
class D:
def f(self, x):
return x
該函數(shù)具有 qualified name 屬性以支持自?。?/p>
>>> D.f.__qualname__
'D.f'
通過(guò)類(lèi)字典訪問(wèn)函數(shù)不會(huì)調(diào)用 __get__()
。相反,它只返回基礎(chǔ)函數(shù)對(duì)象:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
來(lái)自類(lèi)的點(diǎn)運(yùn)算符訪問(wèn)會(huì)調(diào)用 __get__()
,直接返回底層的函數(shù)。
>>> D.f
<function D.f at 0x00C45070>
有趣的行為發(fā)生在從實(shí)例進(jìn)行點(diǎn)訪問(wèn)期間。點(diǎn)運(yùn)算符查找調(diào)用 __get__()
,返回綁定的方法對(duì)象:
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
綁定方法在內(nèi)部存儲(chǔ)了底層函數(shù)和綁定的實(shí)例:
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
如果你曾好奇常規(guī)方法中的 self 或類(lèi)方法中的 cls 是從什么地方來(lái)的,就是這里了!
方法的種類(lèi)?
非數(shù)據(jù)描述器為把函數(shù)綁定為方法的通常模式提供了一種簡(jiǎn)單的機(jī)制。
概括地說(shuō),函數(shù)對(duì)象具有 __get__()
方法,以便在作為屬性訪問(wèn)時(shí)可以將其轉(zhuǎn)換為方法。非數(shù)據(jù)描述器將 obj.f(*args)
的調(diào)用會(huì)被轉(zhuǎn)換為 f(obj, *args)
。調(diào)用 klass.f(*args)` 因而變成 f(*args)
。
下表總結(jié)了綁定及其兩個(gè)最有用的變體:
轉(zhuǎn)換形式
通過(guò)對(duì)象調(diào)用
通過(guò)類(lèi)調(diào)用
function -- 函數(shù)
f(obj, *args)
f(*args)
靜態(tài)方法
f(*args)
f(*args)
類(lèi)方法
f(type(obj), *args)
f(cls, *args)
靜態(tài)方法?
靜態(tài)方法返回底層函數(shù),不做任何更改。調(diào)用 c.f
或 C.f
等效于通過(guò) object.__getattribute__(c, "f")
或 object.__getattribute__(C, "f")
查找。這樣該函數(shù)就可以從對(duì)象或類(lèi)中進(jìn)行相同的訪問(wèn)。
適合作為靜態(tài)方法的是那些不引用 self
變量的方法。
例如,一個(gè)統(tǒng)計(jì)用的包可能包含一個(gè)實(shí)驗(yàn)數(shù)據(jù)的容器類(lèi)。該容器類(lèi)提供了用于計(jì)算數(shù)據(jù)的平均值,均值,中位數(shù)和其他描述性統(tǒng)計(jì)信息的常規(guī)方法。但是,可能有在概念上相關(guān)但不依賴于數(shù)據(jù)的函數(shù)。例如, erf(x)
是在統(tǒng)計(jì)中的便捷轉(zhuǎn)換,但并不直接依賴于特定的數(shù)據(jù)集??梢詮膶?duì)象或類(lèi)中調(diào)用它: s.erf(1.5) --> .9332
或 Sample.erf(1.5) --> .9332
。
由于靜態(tài)方法返回的底層函數(shù)沒(méi)有任何變化,因此示例調(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)
類(lèi)方法?
與靜態(tài)方法不同,類(lèi)方法在調(diào)用函數(shù)之前將類(lèi)引用放在參數(shù)列表的最前。無(wú)論調(diào)用方是對(duì)象還是類(lèi),此格式相同:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
當(dāng)方法僅需要具有類(lèi)引用并且確實(shí)依賴于存儲(chǔ)在特定實(shí)例中的數(shù)據(jù)時(shí),此行為就很有用。類(lèi)方法的一種用途是創(chuàng)建備用類(lèi)構(gòu)造函數(shù)。例如,類(lèi)方法 dict.fromkeys()
從鍵列表創(chuàng)建一個(gè)新字典。純 Python 的等價(jià)實(shí)現(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)造一個(gè)新的唯一鍵字典:
>>> 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'"
成員對(duì)象和 __slots__?
當(dāng)一個(gè)類(lèi)定義了 __slots__
,它會(huì)用一個(gè)固定長(zhǎng)度的 slot 值數(shù)組來(lái)替換實(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.
要?jiǎng)?chuàng)建一個(gè)一模一樣的純 Python 版的 __slots__
是不可能的,因?yàn)樗枰苯釉L問(wèn) C 結(jié)構(gòu)體并控制對(duì)象內(nèi)存分配。 但是,我們可以構(gòu)建一個(gè)非常相似的模擬版,其中作為 slot 的實(shí)際 C 結(jié)構(gòu)體由一個(gè)私有的 _slotvalues
列表來(lái)模擬。 對(duì)該私有結(jié)構(gòu)體的讀寫(xiě)操作將由成員描述器來(lái)管理:
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 (self):
'Emulate member_repr() in Objects/descrobject.c'
return f'<Member {self.name!r} of {self.clsname!r}>'
type.__new__()
方法負(fù)責(zé)將成員對(duì)象添加到類(lèi)變量:
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 而非實(shí)例字典的實(shí)例。 以下是一個(gè)純 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)
要在真實(shí)的類(lèi)中使用這個(gè)模擬版,只需從 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
這時(shí),metaclass 已經(jīng)為 x 和 y 加載了成員對(duì)象:
>>> 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)實(shí)例被創(chuàng)建時(shí),它們將擁有一個(gè)用于存放屬性的 slot_values
列表:
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
錯(cuò)誤拼寫(xiě)或未賦值的屬性將引發(fā)一個(gè)異常:
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'