# Descriptor Guide

Автор:

  Raymond Hettinger

контакт:

  <python at rcn dot com>

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

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

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

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

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

4. Останній розділ містить чисті еквіваленти Python для вбудованих дескрипторів, написаних мовою C. Прочитайте це, якщо вам цікаво, як функції перетворюються на зв’язані методи, або про реалізацію звичайних інструментів, таких як `classmethod()`, `staticmethod()`, `property()` і __slots__.

## Буквар

У цьому посібнику ми починаємо з найпростішого можливого прикладу, а потім будемо додавати нові можливості одну за одною.

### Простий приклад: дескриптор, який повертає константу

Клас `Ten` — це дескриптор, чий метод `__get__()` завжди повертає константу `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

Крім показу того, як дескриптори можуть запускати обчислення, цей приклад також показує призначення параметрів для `__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__()`. Це використовується лише у випадках, коли дескриптору потрібно знати або клас, у якому він був створений, або назву змінної класу, якій він був призначений. (Цей метод, якщо він присутній, викликається, навіть якщо клас не є дескриптором.)

Дескриптори викликаються оператором крапки під час пошуку атрибутів. Якщо до дескриптора звертаються опосередковано за допомогою `vars(some_class)[descriptor_name]`, екземпляр дескриптора повертається без його виклику.

Дескриптори працюють лише тоді, коли використовуються як змінні класу. Якщо їх поставити в інстанції, вони не мають ефекту.

Основна мотивація для дескрипторів полягає в тому, щоб надати гачок, який дозволяє об’єктам, що зберігаються у змінних класу, контролювати те, що відбувається під час пошуку атрибутів.

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

Дескриптори використовуються в усій мові. Саме так функції перетворюються на зв’язані методи. Загальні інструменти, такі як `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()` для перевірки різноманітних обмежень за потреби.

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

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

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__()`. Якщо будь-який із цих методів визначено для атрибута, він називається 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)`

Це все. Визначте будь-який із цих методів, і об’єкт вважатиметься дескриптором і зможе замінити поведінку за замовчуванням, якщо його шукати як атрибут.

Якщо об’єкт визначає `__set__()` або `__delete__()`, він вважається дескриптором даних. Дескриптори, які визначають лише `__get__()`, називаються дескрипторами не даних (вони часто використовуються для методів, але можливі й інші способи використання).

Дескриптори даних і не даних відрізняються тим, як обчислюються перевизначення щодо записів у словнику екземпляра. Якщо в словнику екземпляра є запис із таким самим іменем, як і дескриптор даних, дескриптор даних має пріоритет. Якщо в словнику екземпляра є запис із таким же ім’ям, що й дескриптор, що не є даними, пріоритет має словниковий запис.

Щоб створити дескриптор даних лише для читання, визначте `__get__()` і `__set__()` з `__set__()`, що викликає `AttributeError` під час виклику. Щоб зробити його дескриптором даних, достатньо визначити метод `__set__()` із заповнювачем, що викликає винятки.

### Огляд виклику дескриптора

Дескриптор можна викликати безпосередньо за допомогою `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.

### Виклик із екземпляра

Пошук екземплярів сканує ланцюжок просторів імен, надаючи найвищий пріоритет дескрипторам даних, потім змінним екземплярів, потім дескрипторам, що не є даними, потім змінним класу, і нарешті `__getattr__()`, якщо він надається.

Якщо для `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)

Зауважте, що в коді `__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 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()

Ми можемо використовувати клас `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 that implements most of the core functionality:

    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 __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
            return self.fget(obj)

        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError
            self.fset(obj, value)

        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError
            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)

        def __getattribute__(self, name):
            "Emulate method_getset() in Objects/classobject.c"
            if name == '__doc__':
                return self.__func__.__doc__
            return object.__getattribute__(self, name)

        def __getattr__(self, name):
            "Emulate method_getattro() in Objects/classobject.c"
            return getattr(self.__func__, name)

        def __get__(self, obj, objtype=None):
            "Emulate method_descr_get() in Objects/classobject.c"
            return self

Щоб підтримувати автоматичне створення методів, функції включають метод `__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):
             return self

    class D2:
        pass

Функція має атрибут 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 0x00B18C90>

Якщо ви коли-небудь задавалися питанням, звідки береться 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`.

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) --> 0.9332` or `Sample.erf(1.5) --> 0.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)

        @property
        def __annotations__(self):
            return self.f.__annotations__

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, including `__name__`, `__qualname__`, and `__doc__`.

### Методи класу

На відміну від статичних методів, методи класу додають посилання на клас до списку аргументів перед викликом функції. Цей формат є однаковим для того, чи є викликаючий об’єкт чи клас:

    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)
            return MethodType(self.f, cls)

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}>'

Метод `type.__new__()` піклується про додавання об’єктів-членів до змінних класу:

    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)

Щоб використовувати симуляцію в реальному класі, просто успадкуйте від `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'