__getattr__ en un módulo

127

¿Cómo se puede implementar el equivalente de a __getattr__en una clase, en un módulo?

Ejemplo

Al llamar a una función que no existe en los atributos estáticamente definidos de un módulo, deseo crear una instancia de una clase en ese módulo e invocar el método con el mismo nombre que falló en la búsqueda de atributos en el módulo.

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

Lo que da:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined
Matt Joiner
fuente
2
Probablemente iré con la respuesta de duelo, ya que funciona en todas las circunstancias (aunque es un poco desordenado y podría hacerse mejor). Harvard S y S Lott tienen buenas respuestas limpias pero no son soluciones prácticas.
Matt Joiner,
1
Ni siquiera está en su caso haciendo un acceso de atributo, por lo que está pidiendo dos cosas diferentes a la vez. Entonces, la pregunta principal es cuál quieres. ¿Desea salutationexistir en el espacio de nombres global o local (que es lo que intenta hacer el código anterior) o desea una búsqueda dinámica de nombres cuando realiza un acceso de punto en un módulo? Son dos cosas diferentes.
Lennart Regebro
Pregunta interesante, ¿cómo se te ocurrió esto?
Chris Wesseling
1
posible duplicado de Autoload en Python
Piotr Dobrogost
1
__getattr__en módulos es compatible con Python 3.7
Fumito Hamamura

Respuestas:

41

Hace un tiempo, Guido declaró que todas las búsquedas de métodos especiales en clases de estilo nuevo omiten __getattr__y__getattribute__ . Métodos dunder habían trabajado previamente en módulos - que podría, por ejemplo, utilizar un módulo como un gestor de contexto, simplemente mediante la definición __enter__y __exit__, antes de que esos trucos rompieron .

Recientemente, algunas características históricas han regresado, el módulo __getattr__entre ellas, por lo que el truco existente (un módulo que se reemplaza a sí mismo con una clase en el sys.modulesmomento de la importación) ya no debería ser necesario.

En Python 3.7+, solo usa la forma obvia. Para personalizar el acceso a los atributos en un módulo, defina una __getattr__función en el nivel del módulo que debería aceptar un argumento (nombre del atributo) y devolver el valor calculado o aumentar un AttributeError:

# my_module.py

def __getattr__(name: str) -> Any:
    ...

Esto también permitirá ganchos en las importaciones "desde", es decir, puede devolver objetos generados dinámicamente para declaraciones como from my_module import whatever.

En una nota relacionada, junto con el módulo getattr también puede definir una __dir__función a nivel de módulo para responder dir(my_module). Ver PEP 562 para más detalles.

wim
fuente
1
Si creo dinámicamente un módulo a través de m = ModuleType("mod")y establecer m.__getattr__ = lambda attr: return "foo"; sin embargo, cuando corro from mod import foo, me sale TypeError: 'module' object is not iterable.
weberc2
@ weberc2: Hacer que m.__getattr__ = lambda attr: "foo", además de que es necesario definir una entrada para el módulo con sys.modules['mod'] = m. Después, no hay error con from mod import foo.
Martineau
wim: También puede obtener valores calculados dinámicamente, como tener una propiedad de nivel de módulo, lo que permite escribir my_module.whateverpara invocarlo (después de un import my_module).
Martineau
115

Hay dos problemas básicos con los que se encuentra aquí:

  1. __xxx__ los métodos solo se buscan en la clase
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) significa que cualquier solución también tendría que hacer un seguimiento de qué módulo se estaba examinando, de lo contrario, cada módulo tendría el comportamiento de sustitución de instancia; y (2) significa que (1) ni siquiera es posible ... al menos no directamente.

Afortunadamente, sys.modules no es exigente con lo que va allí, por lo que un contenedor funcionará, pero solo para el acceso al módulo (es decir import somemodule; somemodule.salutation('world'); para el acceso al mismo módulo, prácticamente tiene que extraer los métodos de la clase de sustitución y agregarlos a cada uno globals()con un método personalizado en la clase (me gusta usar .export()) o con una función genérica (como las que ya figuran como respuestas). Una cosa a tener en cuenta: si el contenedor está creando una nueva instancia cada vez, y la solución global no lo es, terminas con un comportamiento sutilmente diferente Oh, y no puedes usar ambos al mismo tiempo, es uno o el otro.


Actualizar

De Guido van Rossum :

En realidad, hay un truco que se usa y recomienda ocasionalmente: un módulo puede definir una clase con la funcionalidad deseada y, al final, reemplazarse en sys.modules con una instancia de esa clase (o con la clase, si insiste , pero eso generalmente es menos útil). P.ej:

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

Esto funciona porque la maquinaria de importación está activando activamente este truco y, como paso final, extrae el módulo real de sys.modules, después de cargarlo. (Esto no es accidental. El hack fue propuesto hace mucho tiempo y decidimos que nos gustó lo suficiente como para apoyarlo en la maquinaria de importación).

Entonces, la forma establecida de lograr lo que desea es crear una sola clase en su módulo, y como el último acto del módulo, reemplace sys.modules[__name__]con una instancia de su clase, y ahora puede jugar con __getattr__/ __setattr__/ __getattribute__según sea necesario.


Nota 1 : Si usa esta funcionalidad, cualquier otra cosa en el módulo, como globales, otras funciones, etc., se perderá cuando sys.modulesse realice la asignación, así que asegúrese de que todo lo necesario esté dentro de la clase de reemplazo.

Nota 2 : Para apoyar from module import *debe haber __all__definido en la clase; por ejemplo:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

Dependiendo de su versión de Python, puede haber otros nombres para omitir __all__. Se set()puede omitir si no se necesita compatibilidad con Python 2.

Ethan Furman
fuente
3
Esto funciona porque la maquinaria de importación está activando activamente este truco, y como su paso final extrae el módulo real de sys.modules, después de cargarlo ¿Se menciona en algún lugar de los documentos?
Piotr Dobrogost
44
Ahora me siento más cómodo usando este truco, considerándolo "semi-sancionado" :)
Mark Nunberg
3
Esto es hacer cosas chifladas, como import sysdar Nonepor hecho sys. Supongo que este truco no está sancionado en Python 2.
asmeurer
3
@asmeurer: Para comprender la razón de eso (y una solución), vea la pregunta ¿Por qué cambia el valor de __name__ después de la asignación a sys.modules [__ name__]? .
Martineau
1
@ qarma: Parece recordar algunas mejoras de las que se habla que permitirían a los módulos de Python participar más directamente en el modelo de herencia de clases, pero aun así este método aún funciona y es compatible.
Ethan Furman
48

Este es un truco, pero puedes envolver el módulo con una clase:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])
Håvard S
fuente
10
agradable y sucio: D
Matt Joiner
Eso puede funcionar, pero probablemente no sea una solución al problema real del autor.
DasIch
12
"Puede funcionar" y "probablemente no" no es muy útil. Es un truco / truco, pero funciona y resuelve el problema planteado por la pregunta.
Håvard S
66
Si bien esto funcionará en otros módulos que importen su módulo y accedan a atributos inexistentes en él, no funcionará para el ejemplo de código real aquí. Acceder a globals () no pasa por sys.modules.
Marius Gedminas
55
Desafortunadamente, esto no funciona para el módulo actual, o probablemente para cosas a las que se accede después de un import *.
Matt Joiner,
19

No solemos hacerlo así.

Lo que hacemos es esto.

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

¿Por qué? Para que la instancia global implícita sea visible.

Para ver ejemplos, mire el randommódulo, que crea una instancia global implícita para simplificar ligeramente los casos de uso en los que desea un generador de números aleatorios "simple".

S.Lott
fuente
Si eres realmente ambicioso, puedes crear la clase e iterar a través de todos sus métodos y crear una función de nivel de módulo para cada método.
Paul Fisher
@Paul Fisher: Según el problema, la clase ya existe. Exponer todos los métodos de la clase podría no ser una buena idea. Por lo general, estos métodos expuestos son métodos de "conveniencia". No todos son apropiados para la instancia global implícita.
S.Lott
13

Similar a lo que propuso @ Håvard S, en un caso en el que necesitaba implementar algo de magia en un módulo (como __getattr__), definiría una nueva clase que hereda types.ModuleTypey la pondría sys.modules(probablemente reemplazando el módulo donde ModuleTypese definió mi costumbre ).

Vea el __init__.pyarchivo principal de Werkzeug para una implementación bastante robusta de esto.

Matt Anderson
fuente
8

Esto es hack, pero ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

Esto funciona iterando sobre todos los objetos en el espacio de nombres global. Si el elemento es una clase, itera sobre los atributos de la clase. Si el atributo es invocable, lo agrega al espacio de nombres global como una función.

Ignora todos los atributos que contienen "__".

No usaría esto en el código de producción, pero debería ayudarlo a comenzar.

afligirse
fuente
2
Prefiero la respuesta de Håvard S a la mía, ya que parece mucho más limpia, pero esto responde directamente a la pregunta que se le hizo.
duelo
Esto está mucho más cerca de lo que finalmente elegí. Es un poco desordenado, pero funciona con globales () correctamente dentro del mismo módulo.
Matt Joiner
1
Me parece que esta respuesta no es exactamente lo que se pidió, que era "Al llamar a una función que no existe en los atributos estáticamente definidos de un módulo" porque está haciendo su trabajo incondicionalmente y agregando todos los métodos de clase posibles. Eso podría solucionarse mediante el uso de un contenedor de módulos que solo funciona AddGlobalAttribute()cuando hay un nivel de módulo AttributeError, algo al revés de la lógica de @ Håvard S. Si tengo la oportunidad, probaré esto y agregaré mi propia respuesta híbrida a pesar de que el OP ya ha aceptado esta respuesta.
Martineau
Actualización a mi comentario anterior. Ahora entiendo que es muy difícil (¿impssoble?) Interceptar NameErrorexcepciones para el espacio de nombres global (módulo), lo que explica por qué esta respuesta agrega callables para cada posibilidad que encuentra al espacio de nombres global actual para cubrir todos los casos posibles antes de tiempo.
Martineau
5

Aquí está mi humilde contribución: un ligero adorno de la respuesta altamente calificada de @ Håvard S, pero un poco más explícita (por lo que podría ser aceptable para @ S.Lott, aunque probablemente no sea lo suficientemente buena para el OP):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")
Martineau
fuente
-3

Cree su archivo de módulo que tenga sus clases. Importar el módulo. Ejecute getattren el módulo que acaba de importar. Puedes hacer una importación dinámica usando__import__ y extraer el módulo de sys.modules.

Aquí está tu módulo some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

Y en otro módulo:

import some_module

Foo = getattr(some_module, 'Foo')

Haciendo esto dinámicamente:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')
drr
fuente
1
Estás respondiendo una pregunta diferente aquí.
Marius Gedminas