Шаг 135.
Python: сборник рецептов.
Классы и объекты. Создание управляемых атрибутов

    На этом шаге мы рассмотрим способы решения этой задачи.

Задача

    Вы хотите добавить дополнительную обработку (например, проверку типов или валидацию) в получение или присваивание значения атрибуту экземпляра.

Решение

    Простой способ кастомизировать доступ к атрибуту заключается в определении свойства (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

    Повторение кода ведет к раздутому, подверженному ошибкам и уродливому коду. Есть намного лучшие пути добиться того же, используя дескрипторы или замыкания.

    На следующем шаге мы рассмотрим вызов метода родительского класса.




Предыдущий шаг Содержание Следующий шаг