__getattr__ su un modulo


127

Come può implementare l'equivalente di a __getattr__su una classe, su un modulo?

Esempio

Quando si chiama una funzione che non esiste negli attributi definiti staticamente di un modulo, desidero creare un'istanza di una classe in quel modulo e invocare il metodo su di esso con lo stesso nome fallito nella ricerca degli attributi sul modulo.

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

Che dà:

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

2
Probabilmente andrò con la risposta del dolore, poiché funziona in tutte le circostanze (anche se è un po 'disordinato e potrebbe essere fatto meglio). Harvard S e S Lott hanno risposte carine e pulite ma non sono soluzioni pratiche.
Matt Joiner,

1
Nel tuo caso non stai nemmeno accedendo ad un attributo, quindi stai chiedendo due cose diverse contemporaneamente. Quindi la domanda principale è quale vuoi. Vuoi salutationesistere nello spazio dei nomi globale o locale (che è ciò che il codice sopra sta cercando di fare) o vuoi una ricerca dinamica dei nomi quando accedi a un punto su un modulo? Sono due cose diverse.
Lennart Regebro,

Domanda interessante, come ti è venuta questa idea?
Chris Wesseling,

1
possibile duplicato di Autoload in Python
Piotr Dobrogost,

1
__getattr__sui moduli è supportato da Python 3.7
Fumito Hamamura

Risposte:


41

Qualche tempo fa, Guido ha dichiarato che tutte le ricerche di metodi speciali su classi di nuovo stile bypassano __getattr__e__getattribute__ . I metodi Dunder avevano precedentemente funzionato su moduli: ad esempio, si poteva usare un modulo come gestore di contesto semplicemente definendo __enter__e __exit__, prima che quei trucchi si interrompessero .

Recentemente alcune funzionalità storiche sono tornate, __getattr__tra cui il modulo , e quindi l'hack esistente (un modulo che si sostituisce con una classe sys.modulesal momento dell'importazione) non dovrebbe più essere necessario.

In Python 3.7+, usi semplicemente un modo ovvio. Per personalizzare l'accesso agli attributi su un modulo, definire una __getattr__funzione a livello di modulo che dovrebbe accettare un argomento (nome dell'attributo) e restituire il valore calcolato o aumentare un AttributeError:

# my_module.py

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

Ciò consentirà inoltre di agganciare le importazioni "da", ovvero è possibile restituire oggetti generati dinamicamente per istruzioni come from my_module import whatever.

Su una nota correlata, insieme al modulo getattr è anche possibile definire una __dir__funzione a livello di modulo a cui rispondere dir(my_module). Vedi PEP 562 per i dettagli.


1
Se creo un modulo in modo dinamico tramite m = ModuleType("mod")e impostato m.__getattr__ = lambda attr: return "foo"; tuttavia, quando corro from mod import foo, ottengo TypeError: 'module' object is not iterable.
weberc2,

@ weberc2: crealo m.__getattr__ = lambda attr: "foo", in più devi definire una voce per il modulo con sys.modules['mod'] = m. Successivamente, non c'è nessun errore con from mod import foo.
martineau,

wim: puoi anche ottenere valori calcolati dinamicamente - come avere una proprietà a livello di modulo - che permette a uno di scrivere my_module.whateverdi invocarlo (dopo un import my_module).
martineau,

115

Esistono due problemi di base che si verificano qui:

  1. __xxx__ i metodi vengono cercati solo sulla classe
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) significa che qualsiasi soluzione dovrebbe anche tenere traccia di quale modulo è stato esaminato, altrimenti ogni modulo avrebbe quindi il comportamento di sostituzione dell'istanza; e (2) significa che (1) non è nemmeno possibile ... almeno non direttamente.

Fortunatamente, sys.modules non è schizzinoso su ciò che va lì, quindi un wrapper funzionerà, ma solo per l'accesso al modulo (cioè import somemodule; somemodule.salutation('world'); per l'accesso allo stesso modulo devi praticamente strappare i metodi dalla classe di sostituzione e aggiungerli a globals()eiher con un metodo personalizzato sulla classe (che mi piace usare .export()) o con una funzione generica (come quelle già elencate come risposte). Una cosa da tenere a mente: se il wrapper crea una nuova istanza ogni volta e la soluzione globale non lo è, finisci per comportarti in modo leggermente diverso Oh, e non riesci a usarli entrambi contemporaneamente - è l'uno o l'altro.


Aggiornare

Da Guido van Rossum :

Esiste effettivamente un hack che viene occasionalmente utilizzato e raccomandato: un modulo può definire una classe con la funzionalità desiderata, e quindi alla fine, sostituirsi in sys.modules con un'istanza di quella classe (o con la classe, se insisti , ma in genere è meno utile). Per esempio:

# module foo.py

import sys

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

sys.modules[__name__] = Foo()

Questo funziona perché il macchinario di importazione sta attivamente abilitando questo hack e mentre il suo passaggio finale estrae il modulo reale da sys.modules, dopo averlo caricato. (Questo non è un caso. L'hacking è stato proposto molto tempo fa e abbiamo deciso che ci piaceva abbastanza da supportarlo nei macchinari di importazione.)

Quindi il modo stabilito per ottenere ciò che vuoi è creare una singola classe nel tuo modulo, e come ultimo atto del modulo sostituire sys.modules[__name__]con un'istanza della tua classe - e ora puoi giocare con __getattr__/ __setattr__/ __getattribute__se necessario.


Nota 1 : se si utilizza questa funzionalità, qualsiasi altra cosa nel modulo, come globi, altre funzioni, ecc., Andrà persa al momento sys.modulesdell'assegnazione, quindi assicurarsi che tutto ciò che è necessario sia all'interno della classe di sostituzione.

Nota 2 : per supportare from module import *è necessario aver __all__definito nella classe; per esempio:

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

A seconda della versione di Python, potrebbero esserci altri nomi da omettere __all__. La set()può essere omesso se non è richiesta la compatibilità Python 2.


3
Questo perché il macchinario di importazione sta attivamente abilitando questo hack, e mentre il suo passaggio finale estrae il modulo reale da sys.modules, dopo averlo caricato Viene menzionato da qualche parte nei documenti?
Piotr Dobrogost,

4
Ora mi sento più a mio agio con questo trucco, considerato "semi-sanzionato" :)
Mark Nunberg,

3
Questo sta facendo cose sbagliate, come fare per import sysdare . Immagino che questo hack non sia sanzionato in Python 2.Nonesys
asmeurer il

3
@asmeurer: per capire il motivo (e una soluzione) vedere la domanda Perché il valore di __name__ cambia dopo l'assegnazione a sys.modules [__ name__]? .
martineau,

1
@qarma: mi sembra di ricordare alcuni miglioramenti di cui si parla che consentirebbero ai moduli Python di partecipare più direttamente al modello di ereditarietà della classe, ma anche così questo metodo funziona ancora ed è supportato.
Ethan Furman,

48

Questo è un trucco, ma puoi avvolgere il modulo con una classe:

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__])

10
bello e sporco: D
Matt Joiner

Potrebbe funzionare ma probabilmente non è una soluzione al vero problema dell'autore.
DasIch

12
"Può funzionare" e "probabilmente no" non è molto utile. È un trucco / hack, ma funziona e risolve il problema posto dalla domanda.
Håvard S

6
Mentre questo funzionerà in altri moduli che importano il tuo modulo e accedono ad attributi inesistenti su di esso, non funzionerà qui per l'esempio di codice effettivo. L'accesso a globals () non passa attraverso sys.modules.
Marius Gedminas,

5
Sfortunatamente questo non funziona per il modulo corrente, o probabilmente per cose a cui si accede dopo un import *.
Matt Joiner,

19

Di solito non lo facciamo in questo modo.

Quello che facciamo è questo.

class A(object):
....

# The implicit global instance
a= A()

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

Perché? In modo che l'istanza globale implicita sia visibile.

Ad esempio, guarda il randommodulo, che crea un'istanza globale implicita per semplificare leggermente i casi d'uso in cui desideri un "semplice" generatore di numeri casuali.


Se sei davvero ambizioso, puoi creare la classe e scorrere tutti i suoi metodi e creare una funzione a livello di modulo per ciascun metodo.
Paul Fisher,

@Paul Fisher: per il problema, la classe esiste già. Esporre tutti i metodi della classe potrebbe non essere una buona idea. Di solito questi metodi esposti sono metodi di "convenienza". Non tutti sono appropriati per l'istanza globale implicita.
S.Lott

13

Simile a quanto proposto da @ Håvard S, nel caso in cui avessi bisogno di implementare un po 'di magia su un modulo (come __getattr__), definirei una nuova classe che eredita types.ModuleTypee inserirla sys.modules(probabilmente sostituendo il modulo in cui è ModuleTypestata definita la mia abitudine ).

Vedi il __init__.pyfile principale di Werkzeug per un'implementazione abbastanza solida di questo.


8

Questo è un trucco, ma ...

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

Questo funziona ripetendo tutti gli oggetti nello spazio dei nomi globale. Se l'elemento è una classe, scorre gli attributi della classe. Se l'attributo è richiamabile, lo aggiunge allo spazio dei nomi globale come funzione.

Ignora tutti gli attributi che contengono "__".

Non lo userei nel codice di produzione, ma dovrebbe iniziare.


2
Preferisco la risposta di Håvard S alla mia, poiché sembra molto più pulita, ma questo risponde direttamente alla domanda come è stata posta.
addolorarsi

Questo è molto più vicino a quello che alla fine sono andato con. È un po 'disordinato, ma funziona correttamente con globals () all'interno dello stesso modulo.
Matt Joiner,

1
Mi sembra che questa risposta non sia esattamente ciò che è stato chiesto, che era "Quando si chiama una funzione che non esiste negli attributi definiti staticamente di un modulo" perché sta facendo il suo lavoro incondizionatamente e aggiungendo ogni possibile metodo di classe. Ciò potrebbe essere risolto usando un wrapper di modulo che fa solo AddGlobalAttribute()quando c'è un livello di modulo AttributeError- una specie di rovescio della logica di @ Håvard S. Se ne avrò la possibilità, lo proverò e aggiungerò la mia risposta ibrida anche se l'OP ha già accettato questa risposta.
martineau,

Aggiornamento al mio commento precedente. Ora capisco che è molto difficile (impssoble?) Intercettare le NameErroreccezioni per lo spazio dei nomi globale (modulo) - il che spiega perché questa risposta aggiunge callable per ogni possibilità che trova allo spazio dei nomi globale corrente per coprire ogni possibile caso in anticipo.
martineau,

5

Ecco il mio modesto contributo - un leggero abbellimento della risposta molto apprezzata di @ Håvard S, ma un po 'più esplicita (quindi potrebbe essere accettabile per @ S.Lott, anche se probabilmente non abbastanza per l'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")

-3

Crea il tuo file del modulo che ha le tue classi. Importa il modulo. Esegui getattrsul modulo appena importato. È possibile eseguire un'importazione dinamica usando __import__ed estrarre il modulo da sys.modules.

Ecco il tuo modulo some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

E in un altro modulo:

import some_module

Foo = getattr(some_module, 'Foo')

Fare questo in modo dinamico:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')

1
Stai rispondendo a una domanda diversa qui.
Marius Gedminas,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.