Spec-Zone.ru › Python 3.9

Руководство по описателям (HowTo)

Автор

Raymond Hettinger

Контакты

<python at rcn dot com>

  • Введение

    • Простой пример: описатель, возвращающий константу
    • Динамические запросы
    • Управляемые атрибуты
    • Настройка имён
    • Заключительные мысли
  • Полный практический пример

    • Класс валидатора
    • Настраиваемые валидаторы
    • Практическое применение
  • Техническое руководство

    • Аннотация
    • Определение и введение
    • Протокол описателя
    • Обзор вызова описателя
    • Вызов от экземпляра
    • Вызов от класса
    • Вызов от super
    • Резюме логики вызова
    • Автоматическое уведомление об именах
    • Пример ORM
  • Чисто-Python-эквиваленты

    • Свойства
    • Функции и методы
    • Типы методов
    • Статические методы
    • Методы класса
    • Объекты-члены и __slots__

Описатели позволяют объектам настраивать поиск, хранение и удаление атрибутов.

Это руководство состоит из четырёх основных разделов:

  1. Раздел «Введение» предоставляет общее описание, плавно переходя от простых примеров, добавляя по одному новому элементу. Начните здесь, если вы новичок в описателях.
  2. Во втором разделе показан полный практический пример описателя. Если вы уже знакомы с основами, начните с него.
  3. Третий раздел содержит более техническое руководство, которое углубляется в подробные механизмы работы описателей. Большинству людей такой уровень детализации не нужен.
  4. В последнем разделе представлены чисто-Python-эквиваленты встроенных описателей, написанных на C. Прочитайте его, если вас интересует, как функции превращаются в связанные методы или как реализуются такие общие инструменты, как classmethod(), staticmethod(), property(), и __slots__.

Учебник

В этом учебнике мы начнём с самого простого примера и поэтапно будем добавлять новые возможности.

Простой пример: Дескриптор, возвращающий константу

Класс Ten является дескриптором, который всегда возвращает константу 10 из своего метода __get__():

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

Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример также раскрывает назначение параметров метода __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'}

Теперь новый класс регистрирует доступ к 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}

Заключительные мысли

Дескриптор — это любой объект, который определяет __get__(), __set__() или __delete__().

Дескрипторы могут иметь метод __set_name__(). Он используется только в тех случаях, когда дескриптору необходимо знать либо класс, в котором он был создан, либо имя переменной класса, которой он был назначен. (Этот метод, если он присутствует, вызывается, даже если класс не является дескриптором.)

Дескрипторы вызываются оператором точки во время поиска атрибута. Если к дескриптору обращаются косвенно с помощью vars(some_class)[descriptor_name], экземпляр дескриптора возвращается без его вызова.

Дескрипторы работают только при использовании в качестве переменных класса. При размещении в экземплярах они не имеют эффекта.

Основная мотивация использования дескрипторов — предоставление возможности объектам, хранящимся в переменных класса, контролировать происходящее во время поиска атрибута.

Традиционно вызывающий класс контролирует происходящее во время поиска. Дескрипторы меняют эту взаимосвязь и позволяют данным, к которым осуществляется поиск, иметь голос в этом вопросе.

Дескрипторы используются во всём языке. Функции превращаются в связанные методы именно таким образом. Распространённые инструменты, такие как classmethod(), staticmethod(), property() и functools.cached_property(), реализованы как дескрипторы.

Полный практический пример

В этом примере мы создаём практичный и мощный инструмент для поиска известных ошибок искажения данных.

Класс валидатора

Валидатор — это дескриптор для доступа к управляемому атрибуту. Перед сохранением любых данных он проверяет, что новое значение удовлетворяет различным ограничениям типа и диапазона. Если эти ограничения не соблюдены, он поднимает исключение, чтобы предотвратить искажение данных в источнике.

Этот класс Validator является как абстрактным базовым классом, так и дескриптором управляемого атрибута:

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() для проверки различных ограничений по мере необходимости.

Пользовательские валидаторы

Вот три практических инструмента проверки данных:

  1. OneOf проверяет, является ли значение одним из ограниченного набора вариантов.
  2. Number проверяет, является ли значение либо int, либо float. Дополнительно проверяет, находится ли значение в заданном диапазоне между минимальным и максимальным значениями.
  3. 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__(). Если для атрибута определён любой из этих методов, он считается дескриптором.

По умолчанию доступ к атрибуту осуществляется получением, установкой или удалением атрибута из словаря объекта. Например, a.x имеет цепочку поиска, начинающуюся с a.__dict__['x'], затем type(a).__dict__['x'], и продолжающуюся через порядок разрешения методов type(a). Если найденное значение является объектом, определяющим один из методов дескриптора, Python может переопределить поведение по умолчанию и вызвать метод дескриптора вместо него. Где это произойдёт в цепочке приоритетов, зависит от определённых методов дескриптора.

Дескрипторы — мощный и универсальный протокол. Они являются механизмом, лежащим в основе свойств, методов, статических методов, методов класса и super(). Они используются во всём Python. Дескрипторы упрощают подлежащий C-код и предлагают гибкий набор новых инструментов для ежедневных программ Python.

Протокол дескрипторов

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)

Обратите внимание, что нет крючка __getattr__() в коде __getattribute__(). Вот почему прямой вызов __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__(), но поиск в словаре экземпляра заменён поиском в порядке разрешения методов класса.

Если дескриптор найден, он вызывается с помощью desc.__get__(None, A).

Полную реализацию на C можно найти в type_getattro() и _PyType_Lookup() в Objects/typeobject.c.

Вызов из super

Логика точечного поиска super находится в методе __getattribute__() для объекта, возвращаемого super().

Точечный поиск, такой как super(A, obj).m ищет в obj.__class__.__mro__ базовый класс B непосредственно после A и затем возвращает B.__dict__['m'].__get__(obj, A). Если это не дескриптор, то возвращается m без изменений.

Полная реализация на C находится в super_getattro() в Objects/typeobject.c. Чистый эквивалент Python можно найти в Руководстве Гвидо.

Резюме логики вызова

Механизм дескрипторов встроен в методы __getattribute__() для object, type и super().

Важные моменты, которые следует запомнить:

  • Дескрипторы вызываются методом __getattribute__().
  • Классы наследуют эту механику от object, type или super().
  • Переопределение __getattribute__() предотвращает автоматические вызовы дескрипторов, поскольку вся логика дескриптора находится в этом методе.
  • object.__getattribute__() и type.__getattribute__() делают разные вызовы к __get__(). Первый включает экземпляр и может включать класс. Второй вставляет None для экземпляра и всегда включает класс.
  • Дескрипторы данных всегда переопределяют словари экземпляров.
  • Дескрипторы не данных могут быть переопределены словарями экземпляров.

Автоматическое уведомление о названии

Иногда желательно, чтобы описатель знал, к какому имени переменной класса он был назначен. Когда создается новый класс, метакласс type сканирует словарь нового класса. Если какие-либо записи являются описателями и если они определяют __set_name__(), этот метод вызывается с двумя аргументами. Owner — это класс, где используется описатель, а name — это переменная класса, к которой был назначен описатель.

Подробности реализации находятся в type_new() и set_names() в Objects/typeobject.c.

Поскольку логика обновления находится в type.__new__(), уведомления происходят только во время создания класса. Если описатели добавляются к классу позже, __set_name__() необходимо будет вызвать вручную.

Пример ORM

Следующий код — это упрощенный скелет, демонстрирующий, как описатели данных можно использовать для реализации отображения объекта-реляционной модели.

Основная идея заключается в том, что данные хранятся во внешней базе данных. Экземпляры 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

Протокол дескриптора прост и предоставляет захватывающие возможности. Несколько вариантов использования настолько распространены, что они были предварительно упакованы в встроенные инструменты. Свойства, связанные методы, статические методы, методы класса и __slots__ основаны на протоколе дескриптора.

Свойства

Вызов 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.")

Чтобы увидеть, как property() реализован в терминах протокола дескриптора, вот чистый эквивалент Python:

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. Последующие улучшения программы требуют перерасчета ячейки при каждом доступе; однако программист не хочет влиять на существующий клиентский код, обращающийся к атрибуту напрямую. Решение заключается в обертывании доступа к атрибуту 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

Функция имеет атрибут полное имя для поддержки интроспекции:

>>> 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)

staticmethod

f(*args)

f(*args)

classmethod

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

Используя протокол дескриптора, не связанный с данными, чистая версия 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

Методы класса

В отличие от статических методов, методы класса добавляют ссылку на класс в список аргументов перед вызовом функции. Этот формат одинаков для вызывающего объекта или класса:

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}

Используя протокол дескриптора, не связанный с данными, чистая версия 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__'):
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

Путь кода для hasattr(type(self.f), '__get__') был добавлен в Python 3.9 и позволяет classmethod() поддерживать цепочку декораторов. Например, метод класса и свойство могут быть объединены:

class G:
    @classmethod
    @property
    def __doc__(cls):
        return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"

Объекты членов и __slots__

Когда класс определяет __slots__, он заменяет словари экземпляров массивом фиксированной длины значений слотов. С точки зрения пользователя, это имеет несколько последствий:

1. Обеспечивает немедленное обнаружение ошибок из-за неправильно написанных присваиваний атрибутов. Разрешены только имена атрибутов, указанные в __slots__:

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. Помогает создавать неизменяемые объекты, где дескрипторы управляют доступом к приватным атрибутам, хранящимся в __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. Экономит память. В 64-битном Linux экземпляр с двумя атрибутами занимает 48 байт с __slots__ и 152 байта без него. Этот паттерн проектирования Flyweight вероятно, имеет значение только при создании большого количества экземпляров.

4. Блокирует инструменты, такие как functools.cached_property(), которые требуют словаря экземпляра для корректной работы:

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.

Невозможно создать точный чистый эквивалент Python __slots__, потому что это требует прямого доступа к структурам 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 и установите метакласс на 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'

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/howto/descriptor.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API