Agregue dinámicamente métodos a una clase en Python 3.0

Estoy tratando de escribir una Capa de abstracción de base de datos en Python que te permite construir sentencias de SQL usando llamadas a funciones encadenadas como:

results = db.search("book") .author("JK Rowling") .price("<40.00") .title("Harry") .execute() 

pero tengo problemas cuando bash agregar dinámicamente los métodos necesarios a la clase db.

Aquí están las partes importantes de mi código:

 import inspect def myName(): return inspect.stack()[1][3] class Search(): def __init__(self, family): self.family = family self.options = ['price', 'name', 'author', 'genre'] #self.options is generated based on family, but this is an example for opt in self.options: self.__dict__[opt] = self.__Set__ self.conditions = {} def __Set__(self, value): self.conditions[myName()] = value return self def execute(self): return self.conditions 

Sin embargo, cuando ejecuto el ejemplo como:

 print(db.search("book").price(">4.00").execute()) 

salidas:

 {'__Set__': 'harry'} 

¿Voy por esto de la manera incorrecta? ¿Hay una mejor manera de obtener el nombre de la función que se está llamando o de alguna manera hacer una “copia impresa” de la función?

Simplemente puede agregar las funciones de búsqueda (métodos) después de crear la clase :

 class Search: # The class does not include the search methods, at first def __init__(self): self.conditions = {} def make_set_condition(option): # Factory function that generates a "condition setter" for "option" def set_cond(self, value): self.conditions[option] = value return self return set_cond for option in ('price', 'name'): # The class is extended with additional condition setters setattr(Search, option, make_set_condition(option)) Search().name("Nice name").price('$3').conditions # Example {'price': '$3', 'name': 'Nice name'} 

PD : esta clase tiene un __init__() que no tiene el parámetro de family (los definidores de condición se agregan dinámicamente en tiempo de ejecución, pero se agregan a la clase, no a cada instancia por separado). Si es necesario crear objetos de Search con diferentes definidores de condición , entonces funciona la siguiente variación del método anterior (el método __init__() tiene un parámetro de family ):

 import types class Search: # The class does not include the search methods, at first def __init__(self, family): self.conditions = {} for option in family: # The class is extended with additional condition setters # The new 'option' attributes must be methods, not regular functions: setattr(self, option, types.MethodType(make_set_condition(option), self)) def make_set_condition(option): # Factory function that generates a "condition setter" for "option" def set_cond(self, value): self.conditions[option] = value return self return set_cond >>> o0 = Search(('price', 'name')) # Example >>> o0.name("Nice name").price('$3').conditions {'price': '$3', 'name': 'Nice name'} >>> dir(o0) # Each Search object has its own condition setters (here: name and price) ['__doc__', '__init__', '__module__', 'conditions', 'name', 'price'] >>> o1 = Search(('director', 'style')) >>> o1.director("Louis L").conditions # New method name {'director': 'Louis L'} >>> dir(o1) # Each Search object has its own condition setters (here: director and style) ['__doc__', '__init__', '__module__', 'conditions', 'director', 'style'] 

Referencia: http://docs.python.org/howto/descriptor.html#functions-and-methods


Si realmente necesita métodos de búsqueda que conozcan el nombre del atributo en el que están almacenados, simplemente puede establecerlo en make_set_condition() con

  set_cond.__name__ = option # Sets the function name 

(justo antes de la return set_cond ). Antes de hacer esto, el método Search.name tiene el siguiente nombre:

 >>> Search.price  

después de establecer su atributo __name__ , obtienes un nombre diferente:

 >>> Search.price  

Establecer el nombre del método de esta manera hace que los posibles mensajes de error relacionados con el método sean más fáciles de entender.

En primer lugar, no está agregando nada a la clase, lo está agregando a la instancia.

En segundo lugar, no es necesario acceder a dict . El self.__dict__[opt] = self.__Set__ se hace mejor con setattr(self, opt, self.__Set__) .

En tercer lugar, no utilice __xxx__ como nombres de atributos. Estos están reservados para uso interno de Python.

En cuarto lugar, como habrás notado, Python no es fácil de engañar. El nombre interno del método al que llama sigue siendo __Set__ , aunque acceda a él con un nombre diferente. 🙂 El nombre se establece cuando define el método como parte de la instrucción def .

Probablemente desee crear y configurar los métodos de opciones con una metaclase. También es posible que desee crear realmente esos métodos en lugar de tratar de usar un método para todos ellos. Si realmente quieres usar solo un __getattr__ es el camino, pero puede ser un poco incómodo, generalmente lo recomiendo. Lambdas u otros métodos generados dinámicamente son probablemente mejores.

Aquí hay algunos códigos de trabajo para comenzar (no todo el progtwig que intentabas escribir, sino algo que muestra cómo las partes pueden encajar):

 class Assign: def __init__(self, searchobj, key): self.searchobj = searchobj self.key = key def __call__(self, value): self.searchobj.conditions[self.key] = value return self.searchobj class Book(): def __init__(self, family): self.family = family self.options = ['price', 'name', 'author', 'genre'] self.conditions = {} def __getattr__(self, key): if key in self.options: return Assign(self, key) raise RuntimeError('There is no option for: %s' % key) def execute(self): # XXX do something with the conditions. return self.conditions b = Book('book') print(b.price(">4.00").author('JK Rowling').execute())