Entendendo Metaclasses em Orientação a Objetos com Python
Aplicação do método type
1. Verificando tipos de dados
texto = 'olá mundo' # str()
lista = [11, 22, 33, 44] # list()
dicionario = {'nome': 'joao', 'senha': 123} # dict()
tupla = (11, 22, 33, 44) # tuple()
print(type(texto)) # <class>
print(type(lista)) # <class>
print(type(dicionario)) # <class>
print(type(tupla)) # <class>
</class></class></class></class>
2. Verificando qual classe produziu um objeto
class Aluno:
pass
objeto = Aluno()
print(type(objeto)) # <class>
</class>
3. Classes também são objetos, produzidos por quem?
class Aluno:
pass
objeto = Aluno()
print(type(objeto)) # <class>
print(type(Aluno)) # <class>
class A:pass
class B:pass
print(type(A), type(B)) # <class> <class>
</class></class></class></class>
Descobrimos que nossas classes são todas produzidas pelo type. E o type é o produtor de todas as classes (incluindo ele mesmo) e é conhecido como **metaclass**. ```
print(type(type)) #
Mecanismo de criação de classes na base
---------------------------------------
Normalmente usamos a palavra-chave class para criar classes: ```
class NomeDaClasse(classe_pai):
código_corpo_classe
Se examinarmos o código fonte do type, podemos ver que além de receber um objeto para determinar sua classe, ele tem outro uso: Receber o nome da classe, classes pai e o namespace do corpo para criar uma nova classe. ```
corpo_classe = """ nome = 'maria' """ namespace_classe = {}
exec executa o código e insere alguns nomes no namespace
exec(corpo_classe, {}, namespace_classe) cls = type('Estudante', (object,), namespace_classe) # nome da classe, classes pai, namespace obj = cls() print(obj.class) # print(obj.nome) # maria
Este processo é complicado e ainda tem diferenças em relação à implementação da palavra-chave class, então aqui apenas para conhecimento. Na verdade, nossas classes são todas produzidas por metaclasses, e na base é usado um método type. Controlando a criação de classes através de metaclasses
-------------------------------------------------------
Como as classes são produzidas por metaclasses, podemos entender que as classes são na verdade objetos instanciados pela metaclass, Analogia: - Objeto -- produzido por Classe() -- dispara o `\_\_init\_\_` no corpo da classe, obtendo atributos exclusivos - Classe -- produzida por metaclass -- dispara o `\_\_init\_\_` na metaclass, obtendo atributos de classe exclusivos para cada classe Portanto, podemos controlar o processo de criação da classe derivando o método `\_\_init\_\_` da metaclass. A derivação naturalmente usa herança e a palavra-chave super, tornando a classe que tem type como classe pai também uma metaclass, pois tem o método de criar classes. ```
class MinhaMetaClasse(type):
def __init__(cls, nome, bases=None, namespace=None):
if not nome.istitle(): # Se o nome da classe não começar com maiúscula
raise TypeError(f'Nome de classe deve começar com maiúscula, você escreveu {nome}') # Erro para parar o programa
# Se seguir o estilo de nome que começa com maiúscula, continue executando
super().__init__(nome, bases, namespace) # Herdar métodos da classe pai
class A(metaclass=MinhaMetaClasse): # Modifica a metaclass padrão para nossa metaclass, também modifica a forma de criar esta classe
pass
class bbb(metaclass=MinhaMetaClasse): # Erro aqui, TypeError: Nome de classe deve começar com maiúscula, você escreveu bbb
pass
Se escolhermos modificar a metaclass de criação de classe (através da modificação do parâmetro palavra-chave metaclass), essa metaclass pode ser derivada de type, e através da derivação do duplo init de type podemos controlar o processo de criação da classe. ### Controlando o processo de criação de classes através do método \_\_new\_\_
Na verdade, quando uma classe é criada, a sintaxe acionada é: type(nome_classe, bases, dicionário) Similarmente, quando um objeto é criado, também é chamando a classe, o que dispara o \_\_call\_\_() na metaclass. A criação da classe passa por três etapas: 1. Pelo método \_\_new\_\_ da metaclass, cria um novo objeto (a classe também é um objeto) 2. Chama o método \_\_init\_\_ da metaclass para adicionar atributos exclusivos ao objeto 3. Retorna a classe criada Então a função type passa por um código semelhante ao seguinte: ```
O código a seguir apenas descreve brevemente o type, tem diferenças com o código fonte, lista apenas algumas características.
def type(nome_classe, bases, dicionario): # 1. Chama o new do type para produzir uma nova classe cls = type.new(mcs, nome_classe, bases, dicionario) # Passa metaclass, nome, classes pai, namespace # 2. Chama o duplo init para dar à nova classe alguns atributos específicos type.init(cls, nome_classe, bases=None, dicionario=None) # Passa a nova classe, nome, classes pai, namespace # 3. Retorna a classe return cls
Portanto, além de controlar através do duplo init, podemos controlar o processo de criação da classe derivando o duplo new. (Qualquer método deve especificar a metaclass como a que escrevemos herdada de type) Além disso, modificar o processo de criação da classe derivando o duplo new tem um impacto mais profundo, porque o método new passará o mcs metaclass, este atributo de metaclass irá procurar nas classes pai, então o método new pode não só controlar a classe produzida, mas também controlar o processo das subclasses da classe produzida. Em comparação, o duplo init só pode modificar o processo de classes criadas por esta metaclass, porque o método init só passará a nova classe e alguns atributos de classe, não possuindo o atributo de metaclass. Para lembrar de forma figurativa, o duplo new é a modificação genética, o duplo init é a cirurgia plástica pós-natal. #### Controlando que todas as propriedades de string de subclasses de uma classe sejam agrupadas no dicionário words
class MinhaMetaClasse(type): def new(mcs, nome_classe, classes_pai, atributos_classe): if nome_classe == 'Modelos': return type.new(mcs, nome_classe, classes_pai, atributos_classe) palavras = {} for k, v in atributos_classe.items(): # Pega todos os atributos de campo da tabela if isinstance(v, str): # Se o valor de um atributo for do tipo string, agrupa em palavras palavras[k] = v for k in palavras: atributos_classe.pop(k) # Remove os atributos do tipo string atributos_classe['palavras'] = palavras return type.new(mcs, nome_classe, classes_pai, atributos_classe)
class Modelos(metaclass=MinhaMetaClasse): # Classe pass
class Joao(Modelos): # Subclasse nome = 'joao' idade = 25 hobby = 'programar'
print(Joao.idade) # 25 print(Joao.palavras)
{'module': 'main', 'qualname': 'Joao', 'nome': 'joao', 'hobby': 'programar'}
print(Joao.nome) # Erro, porque nome não é mais um atributo desta classe
Controlando a criação de objetos por metaclasses
------------------------------------------------
O título parece semelhante, mas note a diferença. A seção anterior se refere ao controle do processo de criação da classe, esta seção explica o processo de criação de objetos. - No post anterior, mencionamos que o método mágico `\_\_call\_\_` da classe é disparado automaticamente quando um objeto é chamado com parênteses. - Então, considerando a classe como um objeto, o `\_\_call\_\_` em sua metaclass também será disparado automaticamente quando o nome da classe é chamado com parênteses. Ou seja, o processo de criação de objetos que estamos acostumados, `obj = NomeDaClasse()`, dispara automaticamente o `\_\_call\_\_` na metaclass. Entendendo este ponto, podemos tentar modificar o `\_\_call\_\_` da metaclass para controlar a criação de objetos. Podemos modificar diretamente por derivação ou sobrescrever um call. Geralmente, a metaclass de uma classe é type, e seu `\_\_call\_\_` faz principalmente as seguintes coisas: ```
class MinhaMetaClasse(type):
def __call__(cls, *args, **kwargs):
# 1. Cria um objeto vazio
obj = cls.__new__(cls)
# 2. Chama o init da classe passando o objeto e os argumentos
cls.__init__(obj, *args, **kwargs)
# 3. Retorna o objeto criado
return obj
# O __call__ acima é apenas uma imitação simples do type, é melhor usar super para derivar
class A(metaclass=MinhaMetaClasse): # Modifica a metaclass padrão para nossa metaclass, também modifica a forma de criar esta classe
pass
obj = A() # Dispara o __call__ do MinhaMetaClasse
print(obj) # <__main__.A object at 0x0000025DBB436610>
Permitindo apenas argumentos de palavra-chave na criação de objetos
# Método de implementação 1
class MinhaMetaClasse(type):
def __call__(cls, *args, **kwargs):
if args:
raise TypeError('Você só pode passar argumentos de palavra-chave') # Controla aqui para permitir apenas argumentos de palavra-chave
return super().__call__(*args, **kwargs) # Passa automaticamente o objeto chamado, usando o método da classe pai
# E retorna o valor, mantendo a estrutura consistente com o call original
class A(metaclass=MinhaMetaClasse): # Modifica a metaclass padrão para nossa metaclass, também modifica a forma de criar esta classe
def __init__(self, nome):
self.nome = nome
obj = A('joao')
print(obj.nome)
# Método de implementação 2 (não recomendado)
class MinhaMetaClasse(type):
def __call__(cls, *args, **kwargs):
# 1. Cria um objeto vazio
obj = cls.__new__(cls)
# 2. Chama o init da classe passando o objeto e os argumentos
if args:
raise TypeError('Você só pode passar argumentos de palavra-chave') # Controla aqui para permitir apenas argumentos de palavra-chave
cls.__init__(obj, *args, **kwargs)
# 3. Retorna o objeto criado
return obj