Descriptor HowTo Guide¶
- Автор
Raymond Hettinger
- контакт
<python at rcn dot com>
Зміст
Дескриптори дозволяють об’єктам налаштовувати пошук атрибутів, зберігання та видалення.
Цей посібник складається з чотирьох основних розділів:
«Буквар» дає базовий огляд, обережно переходячи від простих прикладів, додаючи одну функцію за раз. Почніть тут, якщо ви новачок у дескрипторах.
Другий розділ показує повний, практичний приклад дескриптора. Якщо ви вже знаєте основи, почніть з цього.
Третій розділ містить більш технічний підручник, який детально описує механізм роботи дескрипторів. Більшості людей не потрібен такий рівень деталізації.
Останній розділ містить чисті еквіваленти Python для вбудованих дескрипторів, написаних мовою C. Прочитайте це, якщо вам цікаво, як функції перетворюються на зв’язані методи, або про реалізацію звичайних інструментів, таких як
classmethod()
,staticmethod()
,property()
і __slots__.
Буквар¶
У цьому посібнику ми починаємо з найпростішого можливого прикладу, а потім будемо додавати нові можливості одну за одною.
Простий приклад: дескриптор, який повертає константу¶
The Ten
class is a descriptor that always returns the constant 10
from its __get__()
method:
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
In the a.x
attribute lookup, the dot operator finds the key x
and the
value 5
in the class dictionary. In the a.y
lookup, the dot operator
finds a descriptor instance, recognized by its __get__
method, and calls
that method which returns 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
Крім показу того, як дескриптори можуть запускати обчислення, цей приклад також показує призначення параметрів для __get__()
. Параметр self — це size, екземпляр DirectorySize. Параметр obj — це або g, або s, екземпляр Directory. Саме параметр obj дозволяє методу __get__()
вивчати цільовий каталог. Параметр objtype є класом Directory.
Керовані атрибути¶
Популярним використанням дескрипторів є керування доступом до даних екземплярів. Дескриптор призначається публічному атрибуту в словнику класу, а фактичні дані зберігаються як приватний атрибут у словнику екземпляра. Методи дескриптора __get__()
і __set__()
запускаються під час доступу до публічного атрибута.
У наступному прикладі 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. Це означає, що кожен екземпляр може мати лише один зареєстрований атрибут і що його ім’я не змінюється. У наступному прикладі ми вирішимо цю проблему.
Індивідуальні імена¶
Коли клас використовує дескриптори, він може інформувати кожен дескриптор про те, яке ім’я змінної було використано.
У цьому прикладі клас Person
має два екземпляри дескриптора, name і age. Коли визначено клас Person
, він здійснює зворотний виклик до __set_name__()
у LoggedAccess, щоб можна було записати назви полів, надаючи кожному дескриптору власні 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
викликав __set_name__()
, щоб назви полів були записані. Тут ми викликаємо vars()
для пошуку дескриптора без його запуску:
>>> 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}
Закриття думок¶
descriptor — це те, що ми називаємо будь-яким об’єктом, який визначає __get__()
, __set__()
або __delete__()
.
Додатково дескриптори можуть мати метод __set_name__()
. Це використовується лише у випадках, коли дескриптору потрібно знати або клас, у якому він був створений, або назву змінної класу, якій він був призначений. (Цей метод, якщо він присутній, викликається, навіть якщо клас не є дескриптором.)
Descriptors get invoked by the dot «operator» during attribute lookup. If a
descriptor is accessed indirectly with vars(some_class)[descriptor_name]
,
the descriptor instance is returned without invoking it.
Дескриптори працюють лише тоді, коли використовуються як змінні класу. Якщо їх поставити в інстанції, вони не мають ефекту.
Основна мотивація для дескрипторів полягає в тому, щоб надати гачок, який дозволяє об’єктам, що зберігаються у змінних класу, контролювати те, що відбувається під час пошуку атрибутів.
Традиційно викликаючий клас контролює те, що відбувається під час пошуку. Дескриптори інвертують цей зв’язок і дозволяють шуканим даним мати право голосу в цьому питанні.
Дескриптори використовуються в усій мові. Саме так функції перетворюються на зв’язані методи. Загальні інструменти, такі як classmethod()
, staticmethod()
, property()
і functools.cached_property()
, усі реалізовані як дескриптори.
Повний практичний приклад¶
У цьому прикладі ми створюємо практичний і потужний інструмент для виявлення помилок пошкодження даних, які, як відомо, важко знайти.
Клас валідатора¶
Валідатор — це дескриптор керованого доступу до атрибутів. Перш ніж зберігати будь-які дані, він перевіряє, чи нове значення відповідає різним обмеженням типу та діапазону. Якщо ці обмеження не виконуються, створюється виняток, щоб запобігти пошкодженню даних у їх джерелі.
Цей клас 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()
для перевірки різноманітних обмежень за потреби.
Спеціальні валідатори¶
Ось три практичні утиліти перевірки даних:
OneOf
перевіряє, чи значення є одним із обмеженого набору параметрів.Number
перевіряє, чи значення є абоint
, абоfloat
. За бажанням він перевіряє, чи значення знаходиться між заданим мінімумом або максимумом.String
перевіряє, що значення єstr
. За бажанням він перевіряє задану мінімальну або максимальну довжину. Він також може перевірити визначений користувачем предикат.
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.
Визначення та вступ¶
Загалом, дескриптор — це значення атрибута, яке має один із методів у протоколі дескриптора. Ці методи: __get__()
, __set__()
і __delete__()
. Якщо будь-який із цих методів визначено для атрибута, він називається 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) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
Це все. Визначте будь-який із цих методів, і об’єкт вважатиметься дескриптором і зможе замінити поведінку за замовчуванням, якщо його шукати як атрибут.
Якщо об’єкт визначає __set__()
або __delete__()
, він вважається дескриптором даних. Дескриптори, які визначають лише __get__()
, називаються дескрипторами не даних (вони часто використовуються для методів, але можливі й інші способи використання).
Дескриптори даних і не даних відрізняються тим, як обчислюються перевизначення щодо записів у словнику екземпляра. Якщо в словнику екземпляра є запис із таким самим іменем, як і дескриптор даних, дескриптор даних має пріоритет. Якщо в словнику екземпляра є запис із таким же ім’ям, що й дескриптор, що не є даними, пріоритет має словниковий запис.
Щоб створити дескриптор даних лише для читання, визначте __get__()
і __set__()
з __set__()
, що викликає AttributeError
під час виклику. Щоб зробити його дескриптором даних, достатньо визначити метод __set__()
із заповнювачем, що викликає винятки.
Огляд виклику дескриптора¶
Дескриптор можна викликати безпосередньо за допомогою desc.__get__(obj)
або desc.__get__(None, cls)
.
Але частіше дескриптор викликається автоматично з доступу до атрибутів.
Вираз obj.x
шукає атрибут x
у ланцюжку просторів імен для obj
. Якщо пошук знаходить дескриптор за межами екземпляра __dict__
, його метод __get__()
викликається згідно з правилами пріоритету, наведеними нижче.
Деталі виклику залежать від того, чи є obj
об’єктом, класом або екземпляром super.
Виклик із екземпляра¶
Пошук екземплярів сканує ланцюжок просторів імен, надаючи найвищий пріоритет дескрипторам даних, потім змінним екземплярів, потім дескрипторам, що не є даними, потім змінним класу, і нарешті __getattr__()
, якщо він надається.
Якщо для a.x
знайдено дескриптор, він викликається за допомогою: desc.__get__(a, type(a))
.
Логіка пошуку з пунктиром міститься в object.__getattribute__()
. Ось чистий еквівалент Python:
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)
Зауважте, що в коді __getattribute__()
немає хука __getattr__()
. Ось чому виклик __getattribute__()
безпосередньо або за допомогою super().__getattribute__
повністю обійде __getattr__()
.
Натомість це оператор крапки та функція getattr()
, які відповідають за виклик __getattr__()
щоразу, коли __getattribute__()
викликає AttributeError
. Їхня логіка інкапсульована в допоміжній функції:
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__
Виклик із класу¶
Логіка пошуку з пунктиром, наприклад A.x
, знаходиться в type.__getattribute__()
. Кроки подібні до кроків для object.__getattribute__()
, але пошук у словнику екземпляра замінюється пошуком у 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()
.
Важливо пам’ятати:
Дескриптори викликаються методом
__getattribute__()
.Класи успадковують цей механізм від
object
,type
абоsuper()
.Перевизначення
__getattribute__()
запобігає автоматичним викликам дескриптора, оскільки вся логіка дескриптора міститься в цьому методі.object.__getattribute__()
іtype.__getattribute__()
здійснюють різні виклики__get__()
. Перший включає екземпляр і може включати клас. Другий ставитьNone
для екземпляра та завжди включає клас.Дескриптори даних завжди перевизначають словники примірників.
Дескриптори, не пов’язані з даними, можуть бути перевизначені словниками примірників.
Автоматичне сповіщення про ім’я¶
Іноді дескриптору бажано знати, яке ім’я змінної класу було призначено. Коли створюється новий клас, метаклас type
сканує словник нового класу. Якщо будь-який із записів є дескрипторами та якщо вони визначають __set_name__()
, цей метод викликається з двома аргументами. Власник — це клас, де використовується дескриптор, а ім’я — це змінна класу, якій призначено дескриптор.
The implementation details are in type_new()
and
set_names()
in Objects/typeobject.c.
Оскільки логіка оновлення знаходиться в type.__new__()
, сповіщення надходять лише під час створення класу. Якщо дескриптори додаються до класу пізніше, __set_name__()
потрібно буде викликати вручну.
Приклад ORM¶
The following code is 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()
Ми можемо використовувати клас Field
, щоб визначити моделі, які описують схему для кожної таблиці в базі даних:
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
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
Вбудована функція property()
допомагає щоразу, коли інтерфейс користувача надає доступ до атрибутів, а подальші зміни вимагають втручання методу.
Наприклад, клас електронної таблиці може надати доступ до значення клітинки через Cell('b10').value
. Подальші вдосконалення програми вимагають перерахунку комірки при кожному доступі; однак програміст не хоче впливати на існуючий клієнтський код, який безпосередньо звертається до атрибута. Рішення полягає в тому, щоб загорнути доступ до атрибута значення в дескриптор даних властивості:
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
У цьому прикладі працюватиме або вбудований property()
, або наш еквівалент Property()
.
Функції та методи¶
Об’єктно-орієнтовані функції 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)
Щоб підтримувати автоматичне створення методів, функції включають метод __get__()
для зв’язування методів під час доступу до атрибутів. Це означає, що функції є дескрипторами, не пов’язаними з даними, які повертають пов’язані методи під час пошуку з пунктиром з екземпляра. Ось як це працює:
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'
Доступ до функції через словник класу не викликає __get__()
. Замість цього він просто повертає базовий об’єкт функції:
>>> D.__dict__['f']
<function D.f at 0x00C45070>
Пунктирний доступ із класу викликає __get__()
, який просто повертає базову функцію без змін:
>>> D.f
<function D.f at 0x00C45070>
Цікава поведінка відбувається під час доступу з екземпляра з пунктиром. Пошук із пунктиром викликає __get__()
, який повертає пов’язаний об’єкт методу:
>>> 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 0x1012e1f98>
Якщо ви коли-небудь задавалися питанням, звідки береться self у звичайних методах або звідки cls у методах класу, то це все!
Види методів¶
Дескриптори, не пов’язані з даними, надають простий механізм для варіацій звичайних шаблонів зв’язування функцій у методи.
Підсумовуючи, функції мають метод __get__()
, щоб їх можна було перетворити на метод, коли вони доступні як атрибути. Дескриптор без даних перетворює виклик obj.f(*args)
на f(obj, *args)
. Виклик cls.f(*args)
перетворюється на 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
.
Наприклад, пакет статистики може містити клас контейнера для експериментальних даних. Клас надає звичайні методи для обчислення середнього, середнього, медіани та іншої описової статистики, яка залежить від даних. Однак можуть існувати корисні функції, які концептуально пов’язані, але не залежать від даних. Наприклад, erf(x)
— це зручна процедура перетворення, яка виникає в статистичній роботі, але безпосередньо не залежить від конкретного набору даних. Його можна викликати або з об’єкта, або з класу: s.erf(1.5) --> .9332
або Sample.erf(1.5) --> .9332
.
Оскільки статичні методи повертають базову функцію без змін, приклади викликів нецікаві:
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
Використовуючи протокол дескриптора без даних, чиста версія staticmethod()
на Python виглядала б так:
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
Методи класу¶
На відміну від статичних методів, методи класу додають посилання на клас до списку аргументів перед викликом функції. Цей формат є однаковим для того, чи є викликаючий об’єкт чи клас:
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 виглядала б так:
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__'):
return self.f.__get__(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:
class G:
@classmethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"
Об’єкти-члени та __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: can't set attribute
>>> 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. 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
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__()
піклується про додавання об’єктів-членів до змінних класу:
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
def __new__(mcls, clsname, bases, mapping):
'Emuluate 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__()
піклується про створення екземплярів, які мають слоти замість словника екземплярів. Ось приблизне моделювання на чистому 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 на 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'