# Descriptor HowTo Guide

Автор:

  Raymond Hettinger

контакт:

  <python at rcn dot com>

Дескриптори дозволяють об’єктам налаштовувати пошук атрибутів, зберігання та видалення.

Цей посібник складається з чотирьох основних розділів:

1. «Буквар» дає базовий огляд, обережно переходячи від простих прикладів, додаючи одну функцію за раз. Почніть тут, якщо ви новачок у дескрипторах.

2. Другий розділ показує повний, практичний приклад дескриптора. Якщо ви вже знаєте основи, почніть з цього.

3. Третій розділ містить більш технічний підручник, який детально описує механізм роботи дескрипторів. Більшості людей не потрібен такий рівень деталізації.

4. Останній розділ містить чисті еквіваленти 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.

### Спеціальні валідатори

Ось три практичні утиліти перевірки даних:

1. `OneOf` verifies that a value is one of a restricted set of options.

2. `Number` verifies that a value is either an `int` or `float`. Optionally, it verifies that a value is between a given minimum or maximum.

3. `String` verifies that a value is a `str`. 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__()` and `type.__getattribute__()` make different calls to `__get__()`. The first includes the instance and may include the class. The second puts in `None` 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}' has no getter")
            return self.fget(obj)

        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError(f"property '{self._name}' has no setter")
            self.fset(obj, value)

        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError(f"property '{self._name}' has no deleter")
            self.fdel(obj)

        def getter(self, fget):
            prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
            prop._name = self._name
            return prop

        def setter(self, fset):
            prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
            prop._name = self._name
            return prop

        def deleter(self, fdel):
            prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
            prop._name = self._name
            return prop

Вбудована функція `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'