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

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

Задача

    При создании экземпляров класса вы хотите возвращать закешированную ссылку на предыдущий экземпляр, созданный с теми же аргументами (если они есть).

Решение

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

>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True
>>> 

    Чтобы реализовать такое поведение, вы должны использовать фабричную функцию, отделенную от самого класса. Например:

>>> # Опрашиваемый класс
>>> class Spam:
    def __init__(self, name):
        self.name = name

>>> # Поддержка кеширования
>>> import weakref
>>> _spam_cache = weakref.WeakValueDictionary()
>>> def get_spam(name):
    if name not in _spam_cache:
        s = Spam(name)
        _spam_cache[name] = s
    else:
        s = _spam_cache[name]
    return s

>>>

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

>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> print('a is b:', a is b)
a is b: False
>>> c = get_spam('foo')
>>> print('a is c:', a is c)
a is c: True
>>>


Обсуждение

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

    Например, вы можете задуматься о переопределении метода __new__() в классе:

>>> # Замечание: этот код не полностью рабочий
>>> import weakref
>>> class Spam:
	_spam_cache = weakref.WeakValueDictionary()
	def __new__(cls, name):
		if name in cls._spam_cache:
			return cls._spam_cache[name]
		else:
			self = super().__new__(cls)
			cls._spam_cache[name] = self
			return self
	def __init__(self, name):
		print('Initializing Spam')
		self.name = name

		

    На первый взгляд кажется, что этот код может заработать. Однако есть большая проблема: метод __init__() вызывается всегда - без оглядки на то, закеширован ли экземпляр. Например:

>>> s = Spam('Dave')
Initializing Spam
>>> t = Spam('Dave')
Initializing Spam
>>> s is t
True
>>>

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

    Использование слабых ссылок в этом рецепте служит важной цели, связанной со сборкой мусора, что было описано в 152 шаге. Логика такова: вы обычно хотите держать экземпляры в кеше только до тех пор, пока они используются где-то в программе. Экземпляр WeakValueDictionary содержит только элементы, которые существуют где-то еще. Ключи словаря исчезают, когда экземпляры выходят из употребления. Смотрите:

>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> c = get_spam('foo')
>>> list(_spam_cache)
['foo', 'bar']
>>> del a
>>> del c
>>> list(_spam_cache)
['bar']
>>> del b
>>> list(_spam_cache)
[]
>>> 

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

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

>>> import weakref
>>> class CachedSpamManager:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()
    def get_spam(self, name):
        if name not in self._cache:
            s = Spam(name)
            self._cache[name] = s
        else:
            s = self._cache[name]
        return s

>>> class Spam:
    def __init__(self, name):
        self.name = name

>>> Spam.manager = CachedSpamManager()
>>> def get_spam(name):
    return Spam.manager.get_spam(name)

>>> 
>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> print('a is b:', a is b)
a is b: False
>>> c = get_spam('foo')
>>> print('a is c:', a is c)
a is c: True
>>> 

    Преимущество такого подхода в его гибкости. Например, могут быть реализованы различные схемы управления (как отдельные классы) и прикреплены к классу Spam в качестве замены реализации кеширования по умолчанию. Никакой другой код (например, get_spam) не нужно будет менять, чтобы все продолжило работать.

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

>>> a = Spam('foo')
>>> b = Spam('foo')
>>> a is b 
False 
>>>

    Если для вас важно это предотвратить, то это возможно. Например, вы можете дать классу имя, начинающееся с нижнего подчеркивания, такое как _Spam. Это сообщит пользователю о нежелательности прямого доступа.

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

class Spam:
    def __init__(self, *args, **kwargs):
        raise RuntimeError("Can't instantiate directly")

    # Alternate constructor
    @classmethod
    def _new(cls, name):
        self = cls.__new__(cls)
        self.name = name

    Далее вы меняете кеширующий код, чтобы использовать Spam._new() для создания экземпляров вместо обычного вызова Spam(). Например:

import weakref

class CachedSpamManager:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()

    def get_spam(self, name):
        if name not in self._cache:
            s = Spam._new(name) # Измененное создание
            self._cache[name] = s
        else:
            s = self._cache[name]
        return s

    Хотя есть и более экстремальные меры, которые можно предпринять для сокрытия класса Spam, но, вероятно, лучше не зацикливаться на этой проблеме. Нижнего подчеркивания в имени или определения конструктора как метода класса обычно достаточно, чтобы программисты поняли намек.

    Кеширование и другие паттерны создания часто могут быть реализованы в более элегантной (хотя и более продвинутой) манере - через использование метаклассов.

    Со следующего шага мы начнем рассматривать метапрограммирование.




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