Каковы различия между `classmethod` и метаклассом?

12

В Python я могу создать метод класса, используя @classmethodдекоратор:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

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

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

Как показывают выходные C.f()данные, эти два подхода обеспечивают схожую функциональность.

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

user200783
источник

Ответы:

5

Поскольку классы являются экземплярами метакласса, неудивительно, что «метод экземпляра» в метаклассе будет вести себя как метод класса.

Однако, да, есть различия - и некоторые из них более чем семантические:

  1. Самое важное отличие состоит в том, что метод в метаклассе не «виден» из экземпляра класса . Это происходит потому, что поиск атрибута в Python (в упрощенном виде - дескрипторы могут иметь приоритет) ищет атрибут в экземпляре - если он отсутствует в экземпляре, Python затем ищет в классе этого экземпляра, а затем поиск продолжается суперклассы класса, но не на классы класса. Stdlib Python использует эту функцию в abc.ABCMeta.registerметоде. Эта функция может быть использована навсегда, поскольку методы, связанные с самим классом, могут свободно использоваться в качестве атрибутов экземпляра без какого-либо конфликта (но метод все равно будет конфликтовать).
  2. Другое отличие, хотя и очевидное, состоит в том, что метод, объявленный в метаклассе, может быть доступен в нескольких классах, не связанных между собой - если у вас разные иерархии классов, они вообще не связаны в том, с чем они имеют дело, но вам нужна некоторая общая функциональность для всех классов. вам нужно будет создать класс mixin, который должен быть включен как базовый в обе иерархии (скажем, для включения всех классов в реестр приложений). (NB. Иногда миксин может быть лучше, чем метакласс)
  3. Classmethod - это специализированный объект "classmethod", а метод в метаклассе - обычная функция.

Таким образом, случается, что механизм, который используют методы класса, является « протоколом дескриптора ». В то время как обычные функции имеют __get__метод, который вставляет selfаргумент, когда они извлекаются из экземпляра, и оставляет этот аргумент пустым, когда извлекается из класса, classmethodобъект имеет другой объект __get__, который вставит сам класс («владельца») в качестве объекта. Первый параметр в обеих ситуациях.

В большинстве случаев это не имеет практических различий, но если вам нужен доступ к методу как функции, для целей добавления к нему динамического добавления декоратора или любого другого, для метода в метаклассе meta.methodизвлекается функция, готовая для использования , в то время как вы должны использовать его cls.my_classmethod.__func__ для извлечения из метода класса (а затем вам нужно создать другой classmethodобъект и назначить его обратно, если вы сделаете некоторую обертку).

По сути, это 2 примера:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

Другими словами: помимо важного отличия того, что метод, определенный в метаклассе, виден из экземпляра, а classmethodобъект нет, другие различия во время выполнения будут казаться неясными и бессмысленными - но это происходит потому, что языку не нужно идти по-своему с особыми правилами для методов классов: как следствие разработки языка возможны оба способа объявления метода классов - один, потому что класс сам является объектом, а другой, как возможность среди многих, использование протокола дескриптора, который позволяет специализировать доступ к атрибутам в экземпляре и в классе:

classmethodВстроенный определяются в машинном коде, но он может просто быть закодирован в чистом питоне и будет работать в точно так же. 5-строчный класс ниже может быть использован в качестве classmethodдекоратора (без различий во времени выполнения по @classmethod" at all (though distinguishable through introspection such as calls toсравнению со встроенным представлением , and evenisinstance):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

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

jsbueno
источник
2

Когда вы формулируете это так, как вы это делали в вопросе, @classmethodметаклассы и могут выглядеть одинаково, но у них довольно разные цели. Класс, который вводится в @classmethodаргументе 's', обычно используется для создания экземпляра (т.е. альтернативного конструктора). С другой стороны, метаклассы обычно используются для изменения самого класса (например, то, что Django делает со своими моделями DSL).

Это не значит, что вы не можете изменить класс внутри метода класса. Но тогда возникает вопрос, почему вы не определили класс так, как вы хотите изменить его? Если нет, он может предложить рефакторинг для использования нескольких классов.

Давайте немного расширим первый пример.

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Заимствуя из документации по Python , вышеприведенное расширится до чего-то вроде следующего:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

Обратите внимание, как __get__можно взять экземпляр или класс (или оба), и, таким образом, вы можете сделать оба C.fи C().f. Это не похоже на пример метакласса, который вы приведете AttributeErrorдля C().f.

Более того, в примере метакласса fне существует в C.__dict__. При поиске атрибута fс C.fпомощью интерпретатор смотрит, C.__dict__а затем, не найдя, смотрит type(C).__dict__(что есть M.__dict__). Это может иметь значение , если вы хотите гибкость , чтобы переопределить fв C, хотя я сомневаюсь , что это будет когда - либо иметь практическое применение.

Кент Шикама
источник
0

В вашем примере разница будет в некоторых других классах, для которых в качестве метакласса будет задано M.

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

Подробнее о метаклассах. Какие (конкретные) варианты использования метаклассов?

andole
источник
0

И @classmethod, и Metaclass разные.

Все в питоне является объектом. Каждая вещь означает каждую вещь.

Что такое метакласс?

Как сказано, каждая вещь является объектом. Классы также являются объектами, фактически классы являются экземплярами других загадочных объектов, формально называемых мета-классами. Метакласс по умолчанию в Python - это «тип», если он не указан

По умолчанию все определенные классы являются экземплярами типа.

Классы являются экземплярами метаклассов

Немного важных моментов, чтобы понять поведение

  • Как классы являются экземплярами мета классов.
  • Как и каждый экземпляр объекта, объекты (экземпляры) получают свои атрибуты от класса. Класс получит свои атрибуты от Meta-Class

Рассмотрим следующий код

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

Куда,

  • класс C является экземпляром класса Meta
  • «класс C» - это объект класса, который является экземпляром «класса Meta»
  • Как и любой другой объект (экземпляр), «класс C» имеет доступ к своим атрибутам / методам, определенным в его классе «класс Meta».
  • Итак, расшифровка "C.foo ()". «C» - это экземпляр «Meta», а «foo» - вызов метода через экземпляр «Meta», который является «C».
  • Первый аргумент метода "foo" - это ссылка на экземпляр, а не класс в отличие от "classmethod"

Мы можем проверить, как будто «класс C» является экземпляром «класса мета»

  isinstance(C, Meta)

Что такое классометод?

Методы Python называются связанными. Поскольку Python накладывает ограничение, метод должен вызываться только с экземпляром. Иногда нам может потребоваться вызывать методы напрямую через класс без какого-либо экземпляра (как статические члены в Java) без необходимости создавать какой-либо экземпляр. По умолчанию для вызова метода требуется экземпляр. В качестве обходного пути python предоставляет встроенную функцию classmethod для привязки данного метода к классу вместо экземпляра.

Как методы класса связаны с классом. Требуется как минимум один аргумент, который ссылается на сам класс вместо instance (self)

если используется метод класса встроенной функции / декоратора. Первый аргумент будет ссылкой на класс вместо экземпляра

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

Поскольку мы использовали «classmethod», мы вызываем метод «foo» без создания какого-либо экземпляра следующим образом

ClassMethodDemo.foo()

Вызов метода выше вернет True. Поскольку первый аргумент cls действительно является ссылкой на "ClassMethodDemo"

Резюме:

  • Classmethod получает первый аргумент, который является «ссылкой на сам класс (традиционно называемый cls)»
  • Методы мета-классов не являются методами класса. Методы Meta-классов получают первый аргумент, который является «ссылкой на экземпляр (традиционно называемый self), а не на класс»
neotam
источник