Verificação de Parâmetros de métodos com metaclasses
Este exemplo, que acbaou ficnado menos simples do que eu gostaria, mostra uma forma de se verificar a compatibilidade de tipos de entrada de um método e dos tipos de saída, usando metaclasses, e eespernado a descrição desses tipos na docstring do método.
Em linhas gerais:
quando a classe é instanciada, o método new de sua meta classe é chamado,
- com os parâmetros nome (da classe), bases (as superclasses) e um dicionário contendo os atributos de classe. Esses atributos são não só os atributos de dados, mas todos os métodos definidos na classe (lembrando sempre que em python, um método ou uma função é um objeto como qualquer outro)
- Esse método então percorre esses atributos da classe e para cada atributo
- que for "chamável (callable) - querendo dizer que é um método -- e tem uma docstring defindida, faz uma leitura (parse) dessa docstring procurnado pelo seguinte padrão:
""" <nome_do_método> (tipo_parm_1, tipo_parm_2)
-> tipo_retorno
- ..
Sendo que múltiplas linhas começadas com o nome do método, ou com a sequencia "->" são possíveis para permitir o overloading de tipos de parâmetros, que python usa no lugar de polimorfismo. Por exemplo:
- def soma (self, a, b):
- """soma(int, int)
- soma(float, float)
-> int -> float
- soma(float, float)
- """soma(int, int)
"StrongTypedMethod". Esse tipo de objeto é "chamável",q uerendo dizer que ele pode entrar diretamente no lugar do método - python não vê distinção entre objetos chamáveis e métodos ou funções. Eu crio um novo objeto em vez de uma simples função por que o objeto que entra no lugar do método tem que saber de forma persistente: quais são os tipos de parâmetros que ele aceita, quais os tipos que ele pode retornar, e qual o objeto que é seu "dono" - por que o parâmetro que o python passa no "self" para o método, com esse esquema não é mais passado automaticamente. Adicionalmente então, crio uma lista dos métodos que foram substituidos, e uma nova função
"new" para a classe modificada. O objetivo desta função é registrar, em cada um dos métodos modificados, qual é seu "owner_object" - ou seja, o "self". Faço isso com uma instância da classe "NewMethodForStrongTyped". Pronto. O núcleo do funcionamento é que a cada chamada de um dos métodos verificados,
o método call do objeto StrongTypedMethod correspondente é chamado, e ele verifica os parâmetros de entrada contra a lista que tem armazenada, se estiver Ok, chama o método original, verifica os argumentos de saída (o exemplo dá suporte a retorno de múltiplos objetos numa tupla), e se estiver tudo ok, retorna esses valores. Qualquer tipo de objeto fora da linha causa um TypeError.
- que for "chamável (callable) - querendo dizer que é um método -- e tem uma docstring defindida, faz uma leitura (parse) dessa docstring procurnado pelo seguinte padrão:
# coding: utf-8 # AUTHOR: João S. O. Bueno (2009) # Copyright: João S. O. Bueno # License: LGPL V 3.0 """ the StrongTyped class is designed to worka s a metaclass for cases where one could want to raise a type error when methods are called with unexpected parameter types, or return unexpected typed results. It could provide some confort for testing during development for people coming from strong typed languages To use import this module: set StrongTyped as the metaclass for your desired class, and fill in the doc string for method as lined in the "soma" example bellow """ #TODO: implement support for keyword arguments or variable arugmetn lenght #TODO: Group expected parameter types and return types # cuyrrently, any match is good - def parse_parms(name, doc): lines = doc.split("\n") parm_types = [] return_types = [] for line in lines: line = line.strip() if line.startswith(name): #picks the parenthizesd argumetn type list, # allowing for "#" initiated comments types_expr = line.split(name)[1].split("#")[0] parm_types.append(eval(types_expr)) elif line.startswith("->"): #return_types types_expr = line.split("->")[1].split("#")[0] return_types.append(eval(types_expr)) elif line and not line.startswith("#"): # if not a blank or "comment" line, then it # is the end of the parameter checking session break return parm_types, return_types class StrongTypedMethod(object): def __init__(self, method, parm_types, return_types): self.method = method self.parm_types = parm_types self.return_types = return_types self.owner_object = None def verify_signatures(self, arguments, signatures): # verifies if given parametes or result tuple equals one # of the registered signatures for the method if not isinstance(arguments, tuple): arguments = (arguments,) for signature in signatures: if not isinstance(signature, tuple): signature = (signature,) if len(signature) != len(arguments): continue this_signature_ok = True for result, expected_type in zip(arguments, signature): if not isinstance(result, expected_type): this_signature_ok = False if this_signature_ok: return True return False # TODO: implement support for keyword arguments def __call__(self, *args): if self.owner_object is None: owner_object = args[0] args = args[1:] else: owner_object = self.owner_object if not self.verify_signatures(args, self.parm_types): raise TypeError ("""Method %s called with incorrect parameters types. Expected one of:\n %s\n\ngot: \n%s\n""" % (self.method.__name__, str(self.parm_types), str(args)) ) results = self.method(owner_object, *args) if not self.verify_signatures(results, self.return_types): raise TypeError ("""Method %s returned incorrect types. Expected one of:\n %s\n\ngot: \n%s\n""" % (self.method.__name__, str(self.return_types), str(results)) ) return results class NewMethodForStrongTyped(object): def __init__(self, method_list, original__new__): self.method_list = method_list self.original__new__ = original__new__ def __call__(self, cls, *args, **kwargs): if self.original__new__: obj = self.original__new__(cls, *args, **kwargs) else: #warning: "super" didnṫ work here - possible bug could issue from this construct: obj = cls.__bases__[0].__new__(cls, *args, **kwargs) for method in self.method_list: m = getattr(obj, method) m.owner_object = obj return obj class StrongTyped(type): def __new__(cls, name, bases, dct): changed_methods = [] for attr, method in dct.items(): if not hasattr(method, "__call__"): continue if not method.__doc__: continue if attr == "__new__": continue parm_types, return_types = parse_parms(attr, method.__doc__) if not parm_types and not return_types: continue dct[attr] = StrongTypedMethod(method, parm_types, return_types) changed_methods.append(attr) if changed_methods: dct["__new__"] = NewMethodForStrongTyped(changed_methods, dct.get("__new__", None)) return type.__new__(cls, name, bases, dct) class Testing(object): __metaclass__ = StrongTyped def soma(self, a, b): """ soma(int, int) soma(float, float) -> int -> float """ return a + b if __name__ == "__main__": t = Testing() print t.soma(5, 10) print t.soma(5.0 , 10.0) try: print t.soma("ban", "ana") except TypeError, error: print "Soma de strings falhou como esperado:\n %s" % error