Quais são as diferenças entre um método `classmethod` e um método metaclasse?

12

No Python, posso criar um método de classe usando o @classmethoddecorador:

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

Como alternativa, posso usar um método normal (instância) em uma metaclasse:

>>> 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'>

Como mostra o resultado de C.f(), essas duas abordagens fornecem funcionalidade semelhante.

Quais são as diferenças entre usar @classmethode usar um método normal em uma metaclasse?

user200783
fonte

Respostas:

5

Como as classes são instâncias de uma metaclasse, não é inesperado que um "método de instância" na metaclasse se comporte como um método de classe.

No entanto, sim, existem diferenças - e algumas são mais que semânticas:

  1. A diferença mais importante é que um método na metaclasse não é "visível" de uma instância de classe . Isso acontece porque a pesquisa de atributo no Python (de maneira simplificada - os descritores podem ter precedência) procura por um atributo na instância - se não estiver presente na instância, o Python procurará na classe dessa instância e a pesquisa continuará. as superclasses da classe, mas não nas classes da classe. O stdlib do Python utiliza esse recurso no abc.ABCMeta.registermétodo Esse recurso pode ser usado para o bem, pois os métodos relacionados à própria classe são livres para serem reutilizados como atributos de instância sem nenhum conflito (mas um método ainda entraria em conflito).
  2. Outra diferença, embora óbvia, é que um método declarado na metaclasse pode estar disponível em várias classes, não relacionadas de outra forma - se você tiver hierarquias de classe diferentes, não relacionadas de maneira alguma com o que elas lidam, mas desejar alguma funcionalidade comum para todas as classes , você teria que criar uma classe mixin, que teria que ser incluída como base nas duas hierarquias (por exemplo, incluindo todas as classes no registro de um aplicativo). (Nota: o mixin às vezes pode ser uma chamada melhor do que uma metaclasse)
  3. Um método de classe é um objeto "método de classe" especializado, enquanto um método na metaclasse é uma função comum.

Então, acontece que o mecanismo usado pelos métodos de classe é o " protocolo descritor ". Enquanto as funções normais apresentam um __get__método que insere o selfargumento quando são recuperados de uma instância e deixa esse argumento vazio quando recuperado de uma classe, um classmethodobjeto tem um diferente __get__, que insere a própria classe (o "proprietário") como o primeiro parâmetro nas duas situações.

Isso não faz diferenças práticas na maioria das vezes, mas se você deseja acessar o método como uma função, para adicionar dinamicamente a adição de decorador, ou qualquer outro, para um método na metaclasse meta.methodrecupera a função, pronta para ser usada , enquanto você precisa usá cls.my_classmethod.__func__ -lo para recuperá-lo de um método de classe (e, em seguida, você deve criar outro classmethodobjeto e atribuí-lo de volta, se fizer alguma quebra).

Basicamente, estes são os 2 exemplos:


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)

Em outras palavras: além da diferença importante de que um método definido na metaclasse é visível a partir da instância e um classmethodobjeto não, as outras diferenças em tempo de execução parecerão obscuras e sem sentido - mas isso acontece porque o idioma não precisa ir fora do caminho com regras especiais para métodos de classe: As duas formas de declarar um método de classe são possíveis, como conseqüência do design da linguagem - uma, pelo fato de que uma classe é ela própria um objeto e outra, como uma possibilidade entre muitas, o uso do protocolo descritor que permite especializar o acesso ao atributo em uma instância e em uma classe:

O classmethodbuiltin é definido no código nativo, mas poderia ser codificado em python puro e funcionaria exatamente da mesma maneira. A classe de 5 linhas abaixo pode ser usada como classmethoddecorador, sem diferenças de tempo de execução, para a representação de @classmethod" at all (though distinguishable through introspection such as calls toinstância , and eveninterna, é claro):


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

E, além dos métodos, é interessante ter em mente que atributos especializados como a @propertyna metaclasse funcionarão como atributos de classe especializados, da mesma forma, sem nenhum comportamento surpreendente.

jsbueno
fonte
2

Quando você a formula como na pergunta, as @classmethodmetaclasses e podem parecer semelhantes, mas elas têm finalidades bastante diferentes. A classe que é injetada no @classmethodargumento do é geralmente usada para construir uma instância (ou seja, um construtor alternativo). Por outro lado, as metaclasses geralmente são usadas para modificar a própria classe (por exemplo, como o Django faz com seus modelos DSL).

Isso não quer dizer que você não possa modificar a classe dentro de um método de classe. Mas então a pergunta se torna: por que você não definiu a classe da maneira que deseja modificá-la? Caso contrário, pode sugerir um refator para usar várias classes.

Vamos expandir um pouco o primeiro exemplo.

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

Tomando emprestado os documentos do Python , o acima será expandido para algo como o seguinte:

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)

Observe como __get__pode ter uma instância ou a classe (ou ambas) e, portanto, você pode fazer as duas C.fe C().f. Isso é diferente do exemplo de metaclasse que você dá, que dará um AttributeErrorfor C().f.

Além disso, no exemplo da metaclasse, fnão existe em C.__dict__. Ao procurar o atributo fcom C.f, o intérprete olha C.__dict__e, depois de não encontrar, olha type(C).__dict__(o que é M.__dict__). Isso pode importa se você quer a flexibilidade de substituir fem C, embora eu duvido que isso nunca vai ser de uso prático.

Kent Shikama
fonte
0

No seu exemplo, a diferença estaria em algumas outras classes que terão M definido como sua metaclasse.

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'

Aqui está mais sobre metaclasses Quais são alguns casos de uso (concretos) para metaclasses?

andole
fonte
0

@Classmethod e Metaclass são diferentes.

Tudo em python é um objeto. Tudo significa tudo.

O que é Metaclasse?

Como dito, tudo é um objeto. Classes também são objetos, na verdade classes são instâncias de outros objetos misteriosos formalmente chamados de meta-classes. A metaclasse padrão em python é "type" se não for especificado

Por padrão, todas as classes definidas são instâncias do tipo.

Classes são instâncias de Meta-Classes

Poucos pontos importantes são para entender o comportamento mentiroso

  • As classes são instâncias de meta classes.
  • Como todo objeto instanciado, como objetos (instâncias) obtêm seus atributos da classe. A classe receberá seus atributos da Meta-classe

Considere o seguinte código

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()

Onde,

  • classe C é instância da classe Meta
  • "classe C" é um objeto de classe que é instância da "classe Meta"
  • Como qualquer outro objeto (instância), a "classe C" tem acesso aos atributos / métodos definidos na classe "classe Meta"
  • Então, decodificando "C.foo ()". "C" é a instância de "Meta" e "foo" é o método que chama pela instância de "Meta", que é "C".
  • O primeiro argumento do método "foo" é referência à instância que não é ao contrário de "classmethod"

Podemos verificar como se "classe C" é uma instância de "Classe Meta

  isinstance(C, Meta)

O que é método de classe?

Diz-se que os métodos Python estão vinculados. Como o python impõe a restrição, esse método deve ser chamado apenas com instância. Às vezes, podemos querer chamar métodos diretamente através da classe sem nenhuma instância (como membros estáticos em java) sem precisar criar nenhuma instância. Por instância padrão é necessária para chamar method. Como solução alternativa, o python fornece o método classmeth da função interna para vincular o método à classe em vez da instância.

Como métodos de classe são vinculados à classe. É necessário pelo menos um argumento que é referência à própria classe em vez de instância (auto)

se o método de função / decorador interno for usado. O primeiro argumento será referência à classe em vez de instância

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

Como usamos "classmethod", chamamos o método "foo" sem criar nenhuma instância, como a seguir

ClassMethodDemo.foo()

A chamada do método acima retornará True. Como o primeiro argumento cls é realmente uma referência a "ClassMethodDemo"

Resumo:

  • O primeiro método de recebimento de classes é "uma referência à própria classe (tradicionalmente denominada cls)"
  • Métodos de meta-classes não são métodos de classe. Os métodos das meta-classes recebem o primeiro argumento que é "uma referência à instância (tradicionalmente denominada self) e não à classe"
neotam
fonte