На этом шаге мы рассмотрим способы решения этой задачи.
Вы хотите добавить дополнительную обработку (например, проверку типов или валидацию) в получение или присваивание значения атрибуту экземпляра.
Простой способ кастомизировать доступ к атрибуту заключается в определении свойства (property). Например, этот код определяет свойство, которое добавляет простую проверку типов к атрибуту:
>>> class Person: def __init__(self, first_name): self.first_name = first_name # Функция-геттер @property def first_name(self): return self._first_name # Функция-сеттер @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value # Функция-делитер (необязательная) @first_name.deleter def first_name(self): raise AttributeError("Can't delete attribute") >>>
В представленном коде есть три относящихся друг к другу метода, которые должны иметь одинаковое имя. Первый метод - это функция геттер (getter), она делает first_name свойством. Два других метода прикрепляют необязательные функции сеттер (setter) и делитер (deleter) к свойству (property) first_name. Важно подчеркнуть, что декораторы @first_name.setter и @first_name.deleter не будут определены, если first_name не было превращено в свойство с помощью @property.
Важнейшая особенность свойства в том, что оно выглядит так же, как обычный атрибут, но при попытке доступа автоматически активируются геттер, сеттер и делитер. Например:
>>> a = Person('Guido') >>> a.first_name # Вызывает геттер 'Guido' >>> a.first_name = 42 # Вызывает сеттер Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> a.first_name = 42 # Вызывает сеттер File "<pyshell#1>", line 12, in first_name raise TypeError('Expected a string') TypeError: Expected a string >>> del a.first_name Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> del a.first_name File "<pyshell#1>", line 17, in first_name raise AttributeError("Can't delete attribute") AttributeError: Can't delete attribute >>>
Когда вы реализуете свойство, данные (если они имеются) нужно где-то сохранять. Поэтому в методах получения и присваивания вы видите прямую манипуляцию атрибутом _first_name, в котором и находятся данные. Вы также можете спросить, почему метод __init__() устанавливает self.first_name, а не self._first_name. В этом примере весь смысл свойства заключается в применении проверки типа при присваивании значения атрибуту. Поэтому есть вероятность, что вы также захотите провести такую проверку при инициализации. Присваивая значение self.first_name, операция присваивания тоже использует метод-сеттер (в противоположность обходному пути прямого доступа к self._first_name).
Свойства также могут быть определены для существующих методов получения и присваивания значения. Например:
>>> class Person: def __init__(self, first_name): self.set_first_name(first_name) # Функция-геттер def get_first_name(self): return self._first_name # Функция-сеттер def set_first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value # Функция-делитер (необязательная) def del_first_name(self): raise AttributeError("Can't delete attribute") # Создание свойства из существующих методов get/set name = property(get_first_name, set_first_name, del_first_name) >>>
Свойство - это на самом деле коллекция связанных вместе методов. Если вы изучите класс со свойством, то обнаружите сырые методы fget, fset и fdel в атрибутах самого свойства.
>>> Person.first_name.fget <function Person.first_name at 0x000001B9E6B2BC10> >>> Person.first_name.fset <function Person.first_name at 0x000001B9E6B2BCA0> >>> Person.first_name.fdel <function Person.first_name at 0x000001B9E6B2BD30> >>>
Обычно вы не будете вызывать fget и fset напрямую, но они автоматически вызываются, когда происходит доступ к свойству.
Свойства должны быть использованы только в случаях, когда вы на самом деле нуждаетесь в выполнении дополнительных операций при доступе к атрибутам. Иногда программисты, пришедшие из языков типа Java, считают, что любой доступ нужно осуществлять с помощью геттеров и сеттеров, и пишут такой код:
class Person: def __init__(self, first_name): self.first_name = name @property def first_name(self): return self._first_name @first_name.setter def first_name(self, value): self._first_name = value
Не определяйте свойства, которые ничего не добавляют (как в примере выше). Во-первых, они делают ваш код многословным и непонятным другим. Во-вторых, они сделают вашу программу намного медленнее. И последнее: с точки зрения проектирования в этом нет никакого преимущества. Если вы в будущем решите, что нужно добавить дополнительную обработку к доступу к обычному атрибуту, то просто превратите его в свойство, что не приведет к необходимости менять существующий код. Это возможно, потому что синтаксис кода, который осуществляет доступ к атрибуту, останется неизменным.
Свойства также могут быть способом определить вычисляемые атрибуты. Это атрибуты, которые не хранятся, а вычисляются по запросу. Например:
>>> import math >>> class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return math.pi * self.radius ** 2 @property def perimeter(self): return 2 * math.pi * self.radius
Здесь использование свойств позволяет создать единообразный интерфейс экземпляра, в котором к radius, area и perimeter доступ осуществляется как к простым атрибутам, в противоположность смеси простых атрибутов и вызовов методов. Например:
>>> c = Circle(4.0) >>> c.radius 4.0 >>> c.area # Заметьте отсутствие () 50.26548245743669 >>> c.perimeter # Заметьте отсутствие () 25.132741228718345 >>>
Хотя свойства дают вам элегантный интерфейс программирования, иногда вы можете захотеть использовать геттеры и сеттеры напрямую. Например:
>>> p = Person('Guido') >>> p.get_first_name() 'Guido' >>> p.set_first_name('Larry') >>>
Такое часто имеет место, когда код на Python интегрируется в более крупную инфраструктуру систем или программ. Например, предположим, что класс Python будет вставлен в большую распределенную систему, основанную на удаленных вызовах процедур или распределенных объектов. В таких условиях может оказаться намного легче работать с явными методами присваивания и получения значений (вызывая их как обычные методы) вместо использования свойств для скрытого совершения таких вызовов.
И последнее: не пишите код на Python, в котором много повторяющихся определений свойств. Например:
class Person: def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name @property def first_name(self): return self._first_name @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value # Повторение кода свойства, но под другим именем (плохо!) @property def last_name(self): return self._last_name @last_name.setter def last_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._last_name = value
Повторение кода ведет к раздутому, подверженному ошибкам и уродливому коду. Есть намного лучшие пути добиться того же, используя дескрипторы или замыкания.
На следующем шаге мы рассмотрим вызов метода родительского класса.