ООП. Дескрипторы

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

Свойства

Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (property). Рассмотрим следующий пример: пусть у нас есть класс «Профиль пользователя», который включает следующие поля: имя, фамилия и дата рождения.

import typing as tp


class UserProfile:

    def __init__(self, user: "User", first_name: str = "", sur_name: str = "", bdate: tp.Optional[datetime.date] = None) -> None:
        self._user = user
        self.first_name = first_name
        self.sur_name = sur_name
        self.bdate = bdate

        self._age = None
        self._age_last_recalculated = None
        self._recalculate_age()

    def _recalculate_age(self) -> None:
        if self.bdate is None:
            return

        today = datetime.date.today()
        age = today.year - self.bdate.year

        if today < datetime.date(today.year, self.bdate.month, self.bdate.day):
            age -= 1

        self._age = age
        self._age_last_recalculated = today

    def age(self) -> tp.Optional[int]:
        if self._age is None:
            return None
        if datetime.date.today() > self._age_last_recalculated:
            self._recalculate_age()
        return self._age


class User:

    def __init__(self, username: str, email: str, password: str) -> None:
        # ...
        self.profile = UserProfile(self)
        # ...


>>> guido = User("guido", "guido@python.org", "python")
>>> guido.profile.age()
>>> guido.profile.bdate = datetime.date(1956, 1, 31)
>>> guido.profile.age()
65

Из примера видно, что, во-первых, возраст пользователя вычисляется при каждом обращении, во-вторых, мы только получаем значение и никогда его не изменяем. Было бы логично, чтобы клиентский код работал с возрастом как с обычным атрибутом (свойством) доступным только для чтения и python предоставляет нам для этого механизм свойств (propertes):

class UserProfile:
    # ...
    @property
    def age(self) -> Optional[int]:
        if self._age is None:
            return None
        if datetime.date.today() > self._age_last_recalculated:
            self._recalculate_age()
        return self._age


>>> guido = User("guido", "guido@python.org", "python")
>>> guido.profile.age
>>> guido.profile.bdate = datetime.date(1956, 1, 31)
>>> guido.profile.age
65
>>> guido.profile.age = 66
# ...
AttributeError: can't set attribute

Таким образом, свойства дают нам возможность создавать, аналогично другим языкам программирования (например, Java), сеттеры и геттеры, а также вычисляемые свойства (computed properties):

class UserProfile:
    # ...

    @property
    def fullname(self) -> str:
        return f"{self.first_name} {self.sur_name}".title()

    @fullname.setter
    def fullname(self, value: str) -> None:
        name, surname = value.split(" ", maxsplit=1)
        self.first_name = name
        self.sur_name = surname

    @fullname.deleter
    def fullname(self) -> None:
        self.first_name = ''
        self.sur_name = ''


>>> guido.profile.fullname = "Guido Van Rossum"
>>> guido.profile.first_name
'Guido'
>>> guido.profile.sur_name
'Van Rossum'
>>> del guido.profile.fullname
>>> guido.profile.first_name
''
>>> guido.profile.sur_name
''

Чтобы понять как работают свойства необходимо разобраться с дескрипторами.

Дескрипторы

В документации дано следующее определение дескрипторов:

Дескриптор это любой объект, у которого определены методы __get__(), __set__() или __delete__(). Если дескриптором является атрибут класса, то для него определено специальное поведение при разшенении имени атрибута.

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

descr.__get__(self, obj, owner=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None

Дескрипторы, которые реализуют только __get__ называются дескрипторами не данных (non-data descriptors), а дескрипторы, которые реализуют __set__ и/или __delete__ называются дескрипторами данных (data descriptors). Рассмотрим следующий пример:

class D:
  def __get__(self, obj, owner=None):
    print(f"__get__ был вызван с аргументами obj={obj} и owner={owner}")

  def __set__(self, obj, value):
    print(f"__set__ был вызван с аргументами obj={obj} и value={value}")


class Klass:
  d1 = D()

  def __init__(self):
    self.d2 = D()

>>> obj = Klass()
>>> obj.d1 # == type(obj).__dict__['d1'].__get__(obj, type(obj))
__get__ был вызван с аргументами obj=<__main__.Klass object at 0x1037d4c10> и owner=<class '__main__.Klass'>
>>> obj.d2 # == obj.__dict__['d2']
<__main__.D at 0x1052521c0>
>>> Klass.d1 # == Klass.__dict__['d1'].__get__(None, Klass)
__get__ был вызван с аргументами obj=None и owner=<class '__main__.Klass'>
>>> obj.d1 = None # == type(obj).__dict__['d1'].__set__(obj, None)
__set__ был вызван с аргументами obj=<__main__.Klass object at 0x1037d4c10> и value=None
>>> Klass.d1 = None # Klass.__dict__['d1'] = None

Из примера видно, что при обращении к d1 автоматически был вызван метод __get__ определенный на дескрипторе:

Поведением по умолчанию при доступе к атрибуту является обращение к словарю экземпляра, например, при обращении к a.x поиск начинается с a.__dict__['x'], затем type(a).__dict__['x'] и так далее в порядке разрешения методов (mro). Когда же атрибут (класса/метакласса) является дескриптором, то Python изменяет путь поиска, сначала вызывая методы определенные у дескриптора.

Поэтому теперь должно быть понятно, почему при обращении к d2 мы получили просто экземпляр класса. Порядка разрешения имен атрибутов и методов мы коснемся в следующих лекциях.

В Python дескрипторы используются достаточно часто, в том числе и в самом языке, например, функции это дескрипторы:

PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    sizeof(PyFunctionObject),
    0,
    // ...
    function_call,                              /* tp_call */
    // ...
    func_descr_get,                             /* tp_descr_get */
    0,                                          /* tp_descr_set */
    // ...
};

Это позволяет автоматически передавать экземпляр класса в качестве первого аргумента (self), давайте посмотрим на вызов func_descr_get:

/* Bind a function to an object */
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj);
}

Если obj не был передан, то мы имеем дело с обычной функцией, в противном случае это метод и мы «биндим» объект в качестве первого аргумента. На python реализацию функций можно было бы записать так:

class Function:

    def __call__(self, *args, **kwargs):
        # тело функции

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return functools.partial(self, instance)

А вот примеры реализаций декораторов @staticmethod и @classmethod:

import functools

class Classmethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return functools.partial(self.func, owner)


class Staticmethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return self.func

И наконец реализация @property:

class Property:

    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        if instance is None:
            return self
        elif self.fget is None:
            raise AttributeError("Unreadable attribute")
        else:
            return self.fget(instance)

    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("Cant't set attribute")
        else:
            self.fset(instance, value)

    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("Can't delete attribute")
        else:
            self.fdel(instance)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel)

Пример простой ORM можно найти в репозитории с лекциями.


Последнее обновление: 25 июня 2020 г.

Комментарии