На этом шаге мы рассмотрим особенности использования таких свойств.
Вы хотите определить доступный только для чтения атрибут как свойство, которое вычисляется при доступе к нему. Однако после того, как доступ произойдет,
значение должно кешироваться и не пересчитываться при следующих запросах.
Эффективный путь определения ленивых атрибутов - это использование класса-дескриптора:
>>> 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 шаге.
На следующем шаге мы рассмотрим упрощение инициализации структур данных.