Шаг 168.
Python: сборник рецептов. Метапрограммирование. Использование метакласса для управления созданием экземпляров

    На этом шаге мы рассмотрим особенности создания такого метакласса.

Задача

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

Решение

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

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

a = Spam('Guido')
b = Spam('Diana')

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

>>> class NoInstances(type):
    def __call__(self, *args, **kwargs):
        raise TypeError("Can't instantiate directly")

>>> # Пример
>>> class Spam(metaclass=NoInstances):
    @staticmethod
    def grok(x):
        print('Spam.grok')

>>> 

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

>>> Spam.grok(42)
Spam.grok
>>> s = Spam()
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    s = Spam()
  File "<pyshell#0>", line 3, in __call__
    raise TypeError("Can't instantiate directly")
TypeError: Can't instantiate directly
>>> 

    А теперь предположим, что вы хотите реализовать синглтон (паттерн проектирования "Одиночка") - класс, из которого можно создать только один экземпляр. Это делается относительно прямолинейно:

>>> class Singleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)
            return self.__instance
        else:
            return self.__instance

>>> # Пример
>>> class Spam(metaclass=Singleton):
    def __init__(self):
        print('Creating Spam')

>>> 

    В этом случае можно будет создать только один экземпляр. Например:

>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True
>>> 

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

>>> import weakref
>>> class Cached(type):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__cache = weakref.WeakValueDictionary()

    def __call__(self, *args):
        if args in self.__cache:
            return self.__cache[args]
        else:
            obj = super().__call__(*args)
            self.__cache[args] = obj
            return obj

>>> # Пример
>>> class Spam(metaclass=Cached):
    def __init__(self, name):
        print('Creating Spam({!r})'.format(name))
        self.name = name

>>> 

    Вот как это работает:

>>> a = Spam('Guido')
Creating Spam('Guido')
>>> b = Spam('Diana')
Creating Spam('Diana')
>>> c = Spam('Guido') # Закеширован
>>> a is b
False
>>> a is c # Возвращается закешированное значение
True
>>> 


Обсуждение

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

class _Spam:
    def __init__(self):
        print('Creating Spam')

_spam_instance = None
def Spam():
    global _spam_instance
    if _spam_instance is not None :
        return _spam_instance
    else:
        _spam_instance = _Spam()
        return _spam_instance

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

    См. 154 шаг, где приведена информация о создании кешированных экземпляров, слабых ссылках и прочих деталях.

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




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