Шаг 139.
Python: сборник рецептов.
Классы и объекты. Использование лениво вычисляемых свойств

    На этом шаге мы рассмотрим особенности использования таких свойств.

Задача

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

Решение

    Эффективный путь определения ленивых атрибутов - это использование класса-дескриптора:

>>> class lazyproperty:
	def __init__(self, func):
		self.func = func
	def __get__(self, instance, cls):
		if instance is None:
			return self
		else:
			value = self.func(instance)
			setattr(instance, self.func.__name__, value)
			return value

		
>>>

    Чтобы использовать этот код, вы можете применить его в классе:

>>> import math
>>> class Circle:
	def __init__(self, radius):
		self.radius = radius
	@lazyproperty
	def area(self):
		print('Computing area')
		return math.pi * self.radius ** 2
	@lazyproperty
	def perimeter(self):
		print('Computing perimeter')
		return 2 * math.pi * self.radius

	
>>> 

    Вот пример использования в интерактивном сеансе:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
>>> 

    Обратите внимание, что сообщения "Computing area" и "Computing perimeter" появляются только один раз.

Обсуждение

    Во многих случаях цель применения лениво вычисляемых атрибутов заключается в увеличении производительности. Например, вы можете избежать вычисления значений, если только они действительно где-то не нужны. Показанное решение делает именно это, используя одну из особенностей дескриптора, чтобы реализовать функциональность самым эффективным способом.

    Как показано в других рецептах (например, на 138 шаге), когда дескриптор помещается в определение класса, его методы __get__(), __set__() и _delete__() задействуются при доступе к атрибуту. Но если дескриптор определяет только метод __get__(), то у него намного более слабое связывание, нежели обычно. В частности, метод __get__() срабатывает, только если атрибут, к которому осуществляется доступ, отсутствует в словаре экземпляра.

    Класс lazyproperty использует это так: он заставляет метод __get__() сохранять вычисленное значение в экземпляре, используя то же имя, что и само свойство. С помощью этого значение сохраняется в словаре экземпляра и отключает будущие вычисления свойства. Вы можете понаблюдать за этим в таком примере:

>>> c = Circle(4.0)
>>> # Получение переменных экземпляра
>>> vars(c)
{'radius': 4.0}
>>> # Вычисление площади и последующий просмотр переменных
>>> c.area
Computing area
50.26548245743669
>>> vars(c)
{'radius': 4.0, 'area': 50.26548245743669}
>>> # Обратите внимание, что доступ больше не вызывает свойство
>>> c.area
50.26548245743669
>>> # Удалить переменную и увидеть, что свойство снова задействуется
>>> del c.area
>>> vars(c)
{'radius': 4.0}
>>> c.area
Computing area
50.26548245743669
>>>

    Возможный недостаток этого рецепта в том, что вычисленное значение становится изменяемым после создания. Например:

>>> c.area
Computing area
50.26548245743669
>>> c.area = 25
>>> c.area
25

    Если это проблема, вы можете использовать немного менее эффективную реализацию:

>>> def lazyproperty(func):
	name = '_lazy_' + func.__name__
	@property
	def lazy(self):
		if hasattr(self, name):
			return getattr(self, name)
		else:
			value = func(self)
			setattr(self, name, value)
			return value
	return lazy

>>> 

    Если вы используете эту версию, то обнаружите, что операции присваивания недоступны. Например:

>>> c = Circle(4.0)
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.area = 25
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    c.area = 25
AttributeError: can't set attribute
>>> 

    Недостаток такого подхода в том, что все операции получения значения проводятся через функцию-геттер свойства. Это менее эффективно, чем простой поиск значения в словаре экземпляра, как сделано в первом решении.

    За сведениями о свойствах и управлении атрибутами обратитесь к 135 шагу. Дескрипторы описаны на 138 шаге.

    На следующем шаге мы рассмотрим упрощение инициализации структур данных.




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