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