ООП. Дескрипторы
В этой лекции мы рассмотрим такой важный механизм как дескрипторы, а также разберемся с тем как же устроены методы класса.
Свойства
Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (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 можно найти в репозитории с лекциями.