Descriptor Guide¶
- Автор:
Raymond Hettinger
- контакт:
<python at rcn dot com>
Дескриптори дозволяють об’єктам налаштовувати пошук атрибутів, зберігання та видалення.
Цей посібник складається з чотирьох основних розділів:
«Буквар» дає базовий огляд, обережно переходячи від простих прикладів, додаючи одну функцію за раз. Почніть тут, якщо ви новачок у дескрипторах.
Другий розділ показує повний, практичний приклад дескриптора. Якщо ви вже знаєте основи, почніть з цього.
Третій розділ містить більш технічний підручник, який детально описує механізм роботи дескрипторів. Більшості людей не потрібен такий рівень деталізації.
Останній розділ містить чисті еквіваленти Python для вбудованих дескрипторів, написаних мовою C. Прочитайте це, якщо вам цікаво, як функції перетворюються на зв’язані методи, або про реалізацію звичайних інструментів, таких як
classmethod()
,staticmethod()
,property()
і __slots__.
Буквар¶
У цьому посібнику ми починаємо з найпростішого можливого прикладу, а потім будемо додавати нові можливості одну за одною.
Простий приклад: дескриптор, який повертає константу¶
The Ten
class is a descriptor whose __get__()
method always
returns the constant 10
:
class Ten:
def __get__(self, obj, objtype=None):
return 10
Щоб використовувати дескриптор, його потрібно зберегти як змінну класу в іншому класі:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
Інтерактивний сеанс показує різницю між звичайним пошуком атрибута та пошуком дескриптора:
>>> 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
оператор точки знаходить екземпляр дескриптора, розпізнаний його методом __get__
. Виклик цього методу повертає 10
.
Зверніть увагу, що значення 10
не зберігається ні в словнику класу, ні в словнику екземпляра. Натомість значення 10
обчислюється на вимогу.
Цей приклад показує, як працює простий дескриптор, але він не дуже корисний. Для отримання констант кращим буде звичайний пошук атрибутів.
У наступному розділі ми створимо щось більш корисне, динамічний пошук.
Динамічні пошуки¶
Цікаві дескриптори зазвичай запускають обчислення замість повернення констант:
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
Інтерактивний сеанс показує, що пошук є динамічним — він щоразу обчислює різні оновлені відповіді:
>>> 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
Besides showing how descriptors can run computations, this example also
reveals the purpose of the parameters to __get__()
. The self
parameter is size, an instance of DirectorySize. The obj parameter is
either g or s, an instance of Directory. It is the obj parameter that
lets the __get__()
method learn the target directory. The objtype
parameter is the class Directory.
Керовані атрибути¶
A popular use for descriptors is managing access to instance data. The
descriptor is assigned to a public attribute in the class dictionary while the
actual data is stored as a private attribute in the instance dictionary. The
descriptor’s __get__()
and __set__()
methods are triggered when
the public attribute is accessed.
У наступному прикладі age є публічним атрибутом, а _age є приватним. Коли здійснюється доступ до публічного атрибута, дескриптор реєструє пошук або оновлення:
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 реєструється, але звичайний атрибут 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
Однією з основних проблем у цьому прикладі є те, що приватне ім’я _age закріплено в класі LoggedAgeAccess. Це означає, що кожен екземпляр може мати лише один зареєстрований атрибут і що його ім’я не змінюється. У наступному прикладі ми вирішимо цю проблему.
Індивідуальні імена¶
Коли клас використовує дескриптори, він може інформувати кожен дескриптор про те, яке ім’я змінної було використано.
In this example, the Person
class has two descriptor instances,
name and age. When the Person
class is defined, it makes a
callback to __set_name__()
in LoggedAccess so that the field names can
be recorded, giving each descriptor its own public_name and 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
An interactive session shows that the Person
class has called
__set_name__()
so that the field names would be recorded. Here
we call vars()
to look up the descriptor without triggering it:
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_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}
Закриття думок¶
A descriptor is what we call any object that defines __get__()
,
__set__()
, or __delete__()
.
Optionally, descriptors can have a __set_name__()
method. This is only
used in cases where a descriptor needs to know either the class where it was
created or the name of class variable it was assigned to. (This method, if
present, is called even if the class is not a descriptor.)
Дескриптори викликаються оператором крапки під час пошуку атрибутів. Якщо до дескриптора звертаються опосередковано за допомогою vars(some_class)[descriptor_name]
, екземпляр дескриптора повертається без його виклику.
Дескриптори працюють лише тоді, коли використовуються як змінні класу. Якщо їх поставити в інстанції, вони не мають ефекту.
Основна мотивація для дескрипторів полягає в тому, щоб надати гачок, який дозволяє об’єктам, що зберігаються у змінних класу, контролювати те, що відбувається під час пошуку атрибутів.
Традиційно викликаючий клас контролює те, що відбувається під час пошуку. Дескриптори інвертують цей зв’язок і дозволяють шуканим даним мати право голосу в цьому питанні.
Дескриптори використовуються в усій мові. Саме так функції перетворюються на зв’язані методи. Загальні інструменти, такі як classmethod()
, staticmethod()
, property()
і functools.cached_property()
, усі реалізовані як дескриптори.
Повний практичний приклад¶
У цьому прикладі ми створюємо практичний і потужний інструмент для виявлення помилок пошкодження даних, які, як відомо, важко знайти.
Клас валідатора¶
Валідатор — це дескриптор керованого доступу до атрибутів. Перш ніж зберігати будь-які дані, він перевіряє, чи нове значення відповідає різним обмеженням типу та діапазону. Якщо ці обмеження не виконуються, створюється виняток, щоб запобігти пошкодженню даних у їх джерелі.
This Validator
class is both an abstract base class and a
managed attribute descriptor:
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
Custom validators need to inherit from Validator
and must supply a
validate()
method to test various restrictions as needed.
Спеціальні валідатори¶
Ось три практичні утиліти перевірки даних:
OneOf
verifies that a value is one of a restricted set of options.Number
verifies that a value is either anint
orfloat
. Optionally, it verifies that a value is between a given minimum or maximum.String
verifies that a value is astr
. Optionally, it validates a given minimum or maximum length. It can validate a user-defined predicate as well.
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}'
)
Практичне застосування¶
Ось як валідатори даних можна використовувати в реальному класі:
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
Дескриптори запобігають створенню недійсних екземплярів:
>>> 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
Технічний підручник¶
Далі є більш технічний підручник для механіки та деталей роботи дескрипторів.
Анотація¶
Визначає дескриптори, підсумовує протокол і показує, як викликаються дескриптори. Надає приклад, який показує, як працюють об’єктно-реляційні відображення.
Вивчення дескрипторів не тільки забезпечує доступ до більшого набору інструментів, це створює глибше розуміння того, як працює Python.
Визначення та вступ¶
In general, a descriptor is an attribute value that has one of the methods in
the descriptor protocol. Those methods are __get__()
, __set__()
,
and __delete__()
. If any of those methods are defined for an
attribute, it is said to be a descriptor.
Поведінка за умовчанням для доступу до атрибутів полягає в отриманні, установці або видаленні атрибута зі словника об’єкта. Наприклад, a.x
має ланцюжок пошуку, починаючи з a.__dict__['x']
, потім type(a).__dict__['x']
і продовжуючи через дозвіл методу порядок type(a)
. Якщо шукане значення є об’єктом, що визначає один із методів дескриптора, тоді Python може замінити поведінку за замовчуванням і замість цього викликати метод дескриптора. Де це відбувається в ланцюжку пріоритетів, залежить від того, які методи дескриптора були визначені.
Descriptors are a powerful, general purpose protocol. They are the mechanism
behind properties, methods, static methods, class methods, and
super()
. They are used throughout Python itself. Descriptors
simplify the underlying C code and offer a flexible set of new tools for
everyday Python programs.
Дескрипторний протокол¶
descr.__get__(self, obj, type=None)
descr.__set__(self, obj, value)
descr.__delete__(self, obj)
Це все. Визначте будь-який із цих методів, і об’єкт вважатиметься дескриптором і зможе замінити поведінку за замовчуванням, якщо його шукати як атрибут.
If an object defines __set__()
or __delete__()
, it is considered
a data descriptor. Descriptors that only define __get__()
are called
non-data descriptors (they are often used for methods but other uses are
possible).
Дескриптори даних і не даних відрізняються тим, як обчислюються перевизначення щодо записів у словнику екземпляра. Якщо в словнику екземпляра є запис із таким самим іменем, як і дескриптор даних, дескриптор даних має пріоритет. Якщо в словнику екземпляра є запис із таким же ім’ям, що й дескриптор, що не є даними, пріоритет має словниковий запис.
To make a read-only data descriptor, define both __get__()
and
__set__()
with the __set__()
raising an AttributeError
when
called. Defining the __set__()
method with an exception raising
placeholder is enough to make it a data descriptor.
Огляд виклику дескриптора¶
Дескриптор можна викликати безпосередньо за допомогою desc.__get__(obj)
або desc.__get__(None, cls)
.
Але частіше дескриптор викликається автоматично з доступу до атрибутів.
The expression obj.x
looks up the attribute x
in the chain of
namespaces for obj
. If the search finds a descriptor outside of the
instance __dict__
, its __get__()
method is
invoked according to the precedence rules listed below.
Деталі виклику залежать від того, чи є obj
об’єктом, класом або екземпляром super.
Виклик із екземпляра¶
Instance lookup scans through a chain of namespaces giving data descriptors
the highest priority, followed by instance variables, then non-data
descriptors, then class variables, and lastly __getattr__()
if it is
provided.
Якщо для a.x
знайдено дескриптор, він викликається за допомогою: desc.__get__(a, type(a))
.
Логіка пошуку з пунктиром міститься в object.__getattribute__()
. Ось чистий еквівалент Python:
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(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__
Виклик із класу¶
The logic for a dotted lookup such as A.x
is in
type.__getattribute__()
. The steps are similar to those for
object.__getattribute__()
but the instance dictionary lookup is replaced
by a search through the class’s method resolution order.
Якщо дескриптор знайдено, він викликається за допомогою desc.__get__(None, A)
.
The full C implementation can be found in type_getattro()
and
_PyType_Lookup()
in Objects/typeobject.c.
Заклик від супер¶
The logic for super’s dotted lookup is in the __getattribute__()
method for
object returned by super()
.
Пошук із пунктиром, наприклад super(A, obj).m
, шукає obj.__class__.__mro__
для базового класу B
відразу після A
, а потім повертає B". __dict__['m'].__get__(obj, A)
. Якщо не є дескриптором, m
повертається без змін.
The full C implementation can be found in super_getattro()
in
Objects/typeobject.c. A pure Python equivalent can be found in
Guido’s Tutorial.
Короткий опис логіки виклику¶
The mechanism for descriptors is embedded in the __getattribute__()
methods for object
, type
, and super()
.
Важливо пам’ятати:
Descriptors are invoked by the
__getattribute__()
method.Класи успадковують цей механізм від
object
,type
абоsuper()
.Overriding
__getattribute__()
prevents automatic descriptor calls because all the descriptor logic is in that method.object.__getattribute__()
andtype.__getattribute__()
make different calls to__get__()
. The first includes the instance and may include the class. The second puts inNone
for the instance and always includes the class.Дескриптори даних завжди перевизначають словники примірників.
Дескриптори, не пов’язані з даними, можуть бути перевизначені словниками примірників.
Автоматичне сповіщення про ім’я¶
Sometimes it is desirable for a descriptor to know what class variable name it
was assigned to. When a new class is created, the type
metaclass
scans the dictionary of the new class. If any of the entries are descriptors
and if they define __set_name__()
, that method is called with two
arguments. The owner is the class where the descriptor is used, and the
name is the class variable the descriptor was assigned to.
The implementation details are in type_new()
and
set_names()
in Objects/typeobject.c.
Since the update logic is in type.__new__()
, notifications only take
place at the time of class creation. If descriptors are added to the class
afterwards, __set_name__()
will need to be called manually.
Приклад ORM¶
The following code is a simplified skeleton showing how data descriptors could be used to implement an object relational mapping.
Основна ідея полягає в тому, що дані зберігаються у зовнішній базі даних. Екземпляри Python містять лише ключі до таблиць бази даних. Дескриптори піклуються про пошук або оновлення:
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()
We can use the Field
class to define models that describe the schema for
each table in a database:
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
Щоб використовувати моделі, спочатку підключіться до бази даних:
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
Інтерактивний сеанс показує, як дані витягуються з бази даних і як їх можна оновити:
>>> 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¶
Протокол дескриптора простий і пропонує захоплюючі можливості. Кілька варіантів використання настільки поширені, що вони були попередньо упаковані у вбудовані інструменти. Властивості, прив’язані методи, статичні методи, методи класу та __слоти__ базуються на протоколі дескриптора.
Властивості¶
Виклик property()
— це короткий спосіб створення дескриптора даних, який ініціює виклик функції після доступу до атрибута. Його підпис:
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.")
To see how property()
is implemented in terms of the descriptor protocol,
here is a pure Python equivalent:
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!r} of {type(obj).__name__!r} object has no getter'
)
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no setter'
)
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object 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
Вбудована функція property()
допомагає щоразу, коли інтерфейс користувача надає доступ до атрибутів, а подальші зміни вимагають втручання методу.
Наприклад, клас електронної таблиці може надати доступ до значення клітинки через Cell('b10').value
. Подальші вдосконалення програми вимагають перерахунку комірки при кожному доступі; однак програміст не хоче впливати на існуючий клієнтський код, який безпосередньо звертається до атрибута. Рішення полягає в тому, щоб загорнути доступ до атрибута значення в дескриптор даних властивості:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
Either the built-in property()
or our Property()
equivalent would
work in this example.
Функції та методи¶
Об’єктно-орієнтовані функції Python побудовані на функціональному середовищі. Використовуючи дескриптори, не пов’язані з даними, ці два об’єднуються без проблем.
Функції, що зберігаються в словниках класів, під час виклику перетворюються на методи. Методи відрізняються від звичайних функцій лише тим, що екземпляр об’єкта додається до інших аргументів. За домовленістю екземпляр називається self, але може мати назву this або будь-яке інше ім’я змінної.
Методи можна створити вручну за допомогою types.MethodType
, що приблизно еквівалентно:
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)
To support automatic creation of methods, functions include the
__get__()
method for binding methods during attribute access. This
means that functions are non-data descriptors that return bound methods
during dotted lookup from an instance. Here’s how it works:
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)
Запуск наступного класу в інтерпретаторі показує, як дескриптор функції працює на практиці:
class D:
def f(self, x):
return x
Функція має атрибут qualified name для підтримки самоаналізу:
>>> D.f.__qualname__
'D.f'
Accessing the function through the class dictionary does not invoke
__get__()
. Instead, it just returns the underlying function object:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
Dotted access from a class calls __get__()
which just returns the
underlying function unchanged:
>>> D.f
<function D.f at 0x00C45070>
The interesting behavior occurs during dotted access from an instance. The
dotted lookup calls __get__()
which returns a bound method object:
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
Внутрішньо зв’язаний метод зберігає базову функцію та зв’язаний екземпляр:
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x00B18C90>
Якщо ви коли-небудь задавалися питанням, звідки береться self у звичайних методах або звідки cls у методах класу, то це все!
Види методів¶
Дескриптори, не пов’язані з даними, надають простий механізм для варіацій звичайних шаблонів зв’язування функцій у методи.
To recap, functions have a __get__()
method so that they can be converted
to a method when accessed as attributes. The non-data descriptor transforms an
obj.f(*args)
call into f(obj, *args)
. Calling cls.f(*args)
becomes f(*args)
.
Ця діаграма підсумовує прив’язку та два її найкорисніші варіанти:
Трансформація
Викликано з об’єкта
Подзвонили з класу
функція
f(obj, *args)
f(*args)
статичний метод
f(*args)
f(*args)
метод класу
f(type(obj), *args)
f(cls, *args)
Статичні методи¶
Статичні методи повертають базову функцію без змін. Виклик c.f
або C.f
є еквівалентом прямого пошуку object.__getattribute__(c, "f")
або object.__getattribute__(C, "f")
. У результаті функція стає однаково доступною з об’єкта або класу.
Хорошими кандидатами на статичні методи є методи, які не посилаються на змінну self
.
For instance, a statistics package may include a container class for
experimental data. The class provides normal methods for computing the average,
mean, median, and other descriptive statistics that depend on the data. However,
there may be useful functions which are conceptually related but do not depend
on the data. For instance, erf(x)
is handy conversion routine that comes up
in statistical work but does not directly depend on a particular dataset.
It can be called either from an object or the class: s.erf(1.5) --> .9332
or
Sample.erf(1.5) --> .9332
.
Оскільки статичні методи повертають базову функцію без змін, приклади викликів нецікаві:
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
Використовуючи протокол дескриптора без даних, чиста версія staticmethod()
на Python виглядала б так:
import functools
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
The functools.update_wrapper()
call adds a __wrapped__
attribute
that refers to the underlying function. Also it carries forward
the attributes necessary to make the wrapper look like the wrapped
function: __name__
, __qualname__
,
__doc__
, and __annotations__
.
Методи класу¶
На відміну від статичних методів, методи класу додають посилання на клас до списку аргументів перед викликом функції. Цей формат є однаковим для того, чи є викликаючий об’єкт чи клас:
class F:
@classmethod
def f(cls, x):
return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)
Така поведінка корисна, коли метод потребує лише посилання на клас і не покладається на дані, що зберігаються в конкретному екземплярі. Одним із способів використання методів класу є створення альтернативних конструкторів класу. Наприклад, метод класу dict.fromkeys()
створює новий словник зі списку ключів. Чистий еквівалент Python:
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
Тепер новий словник унікальних ключів можна сконструювати так:
>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
Використовуючи протокол дескриптора без даних, чиста версія classmethod()
на Python виглядала б так:
import functools
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, 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'"
The functools.update_wrapper()
call in ClassMethod
adds a
__wrapped__
attribute that refers to the underlying function. Also
it carries forward the attributes necessary to make the wrapper look
like the wrapped function: __name__
,
__qualname__
, __doc__
,
and __annotations__
.
Об’єкти-члени та __slots__¶
Коли клас визначає __slots__
, він замінює словники примірників на масив значень слотів фіксованої довжини. З точки зору користувача, це має кілька ефектів:
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.
Неможливо створити точну версію __slots__
на Python, оскільки для цього потрібен прямий доступ до структур C і контроль над розподілом пам’яті об’єктів. Однак ми можемо побудувати здебільшого точну симуляцію, де фактична структура C для слотів емулюється приватним списком _slotvalues
. Читанням і записом у цю приватну структуру керують дескриптори членів:
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
if obj is None:
return self
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}>'
The type.__new__()
method takes care of adding member objects to class
variables:
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
def __new__(mcls, clsname, bases, mapping, **kwargs):
'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, **kwargs)
Метод object.__new__()
піклується про створення екземплярів, які мають слоти замість словника екземплярів. Ось приблизне моделювання на чистому Python:
class Object:
'Simulate how object.__new__() allocates memory for __slots__'
def __new__(cls, *args, **kwargs):
'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'{cls.__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'{cls.__name__!r} object has no attribute {name!r}'
)
super().__delattr__(name)
To use the simulation in a real class, just inherit from Object
and
set the metaclass to 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
На даний момент метаклас завантажив об’єкти-члени для 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'>}
Коли екземпляри створюються, вони мають список slot_values
, де зберігаються атрибути:
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
Помилково введені або непризначені атрибути викликають виняток:
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'