Руководство по отложенной загрузке атрибутов класса в Python

Ленивая загрузка — это шаблон проектирования, при котором мы откладываем загрузку объекта до тех пор, пока он не понадобится. Это чаще всего используется для повышения производительности.

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

Ленивая загрузка атрибутов класса в Python

Вычисляемые атрибуты или атрибуты, которые загружаются с помощью дорогостоящей операции, такой как вызов API, могут быть загружены лениво. Мы можем выполнить отложенную загрузку, используя @property в Python. В следующем примере определяется класс с именем Circle, который имеет атрибут radius и область как свойство.

import math
class Circle:
   def __init__(self, radius):
      self.radius = radius
   
   @property
   def area(self):
      print("Making expensive calculation...")
      return math.pi * self.radius ** 2

c = Circle(10)
print(c.__dict__)
print(c.area) 

Вызов __dict__ для объекта возвращает только radius в качестве атрибута.

Мы видим, что площадь вычисляется только при доступе к свойству. Это если пользователь загружает объект и выбирает не использовать этот атрибут, он не загружается без необходимости.

Допустим, мы обращаемся к свойству площади несколько раз, оно каждый раз пересчитывается — это очень неэффективно.

print(c.area)
print(c.area)

Кэшированное свойство

Мы можем кэшировать область и избежать этого ненужного пересчета:

import math
from functools import cached_property
class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @cached_property
    def area(self):
         print("Making expensive calculation...")
         return math.pi * self.radius ** 2

c = Circle(10)
print(c.area)
print(c.area)

Здесь мы видим, что расчет выполняется только один раз. Когда мы снова обращаемся к area, значение возвращается из кеша.

Проблемы:

  1. При изменении радиуса площадь не меняется, так как она уже кэширована.
  2. cached_property доступен для записи. В нашем случае мы не хотим, чтобы пользователь редактировал область, поскольку она должна рассчитываться по радиусу. Пользователь должен иметь возможность изменять только радиус.

Кэширование свойств только для чтения с помощью декоратора @property

Шаги:

  1. Сделайте radius свойством и определите геттер и сеттер. Сеттер должен сбросить область до None при изменении радиуса. Таким образом мы очищаем кеш.
  2. Сделайте площадь собственностью. Вычисляйте площадь, только если она равна None. Таким образом, мы вычислим площадь только один раз. Таким образом, избегается повторный расчет при каждом доступе к области.
import math
class Circle:
     def __init__(self, radius):
        self._radius = radius
        self._area = None
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value != self._radius:
            self._radius = value
            self._area = None
    @property
    def area(self):
        if self._area is None:
            print("Making expensive calculation...")
            self._area = math.pi * self.radius ** 2
        return self._area
c = Circle(10)
print(c.area)
c.radius = 20
print(c.area)
print(c.area)

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

Кроме того, поскольку мы использовали декоратор @property, область теперь доступна только для чтения.

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

  1. Некоторые фреймворки и ORM используют ленивую загрузку при выборке данных таблицы, содержащих отношения внешнего ключа. Вот — пример от Джанго.
  2. Бесконечная прокрутка, новый контент, когда читатель достигает нижней части страницы или близко к ней.
  3. Ленивая загрузка изображений в окне просмотра браузера. Изображение-заполнитель отображается в окне просмотра до тех пор, пока пользователь не прокрутит изображение до него.

Использованная литература:

  1. https://realpython.com/python-property/#managing-attributes-in-your-classes
  2. https://www.pythontutorial.net/python-oop/python-readonly-property/
  3. https://www.sourcecodeexamples.net/2018/05/lazy-loading-pattern.html