__getattr__ auf einem Modul

127

Wie kann das Äquivalent von a __getattr__in einer Klasse, in einem Modul implementiert werden?

Beispiel

Wenn ich eine Funktion aufrufe, die in den statisch definierten Attributen eines Moduls nicht vorhanden ist, möchte ich eine Instanz einer Klasse in diesem Modul erstellen und die Methode mit demselben Namen aufrufen, der bei der Attributsuche im Modul fehlgeschlagen ist.

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

Welches gibt:

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
quelle
2
Ich werde wahrscheinlich mit der Antwort von Trauer gehen, da es unter allen Umständen funktioniert (obwohl es ein bisschen chaotisch ist und besser gemacht werden könnte). Harvard S und S Lott haben schöne, klare Antworten, aber sie sind keine praktischen Lösungen.
Matt Joiner
1
Sie machen in Ihrem Fall nicht einmal einen Attributzugriff, also fragen Sie nach zwei verschiedenen Dingen gleichzeitig. Die Hauptfrage ist also, welche Sie wollen. Möchten Sie salutationim globalen oder lokalen Namespace existieren (wie im obigen Code versucht) oder möchten Sie eine dynamische Suche nach Namen, wenn Sie einen Punktzugriff auf ein Modul vornehmen? Es sind zwei verschiedene Dinge.
Lennart Regebro
Interessante Frage, wie sind Sie darauf gekommen?
Chris Wesseling
1
mögliches Duplikat von Autoload in Python
Piotr Dobrogost
1
__getattr__on modules wird von Python 3.7
Fumito Hamamura

Antworten:

41

Vor einiger Zeit erklärte Guido, dass alle speziellen Methodensuchen für Klassen neuen Stils Bypass __getattr__und umgehen__getattribute__ . Dunder-Methoden hatten zuvor an Modulen gearbeitet - Sie konnten beispielsweise ein Modul als Kontextmanager verwenden, indem Sie einfach definieren __enter__und __exit__bevor diese Tricks brachen .

In letzter Zeit haben einige historische Features ein Comeback erlebt, __getattr__darunter auch das Modul. Daher sollte der vorhandene Hack (ein Modul, das sich sys.modulesbeim Import durch eine Klasse ersetzt ) nicht mehr erforderlich sein.

In Python 3.7+ verwenden Sie nur den einen offensichtlichen Weg. Um den Attributzugriff auf ein Modul anzupassen, definieren Sie eine __getattr__Funktion auf Modulebene, die ein Argument (Name des Attributs) akzeptieren soll, und geben Sie den berechneten Wert zurück oder lösen Sie Folgendes aus AttributeError:

# my_module.py

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

Dies ermöglicht auch Hooks in "von" -Importen, dh Sie können dynamisch generierte Objekte für Anweisungen wie zurückgeben from my_module import whatever.

In einem verwandten Hinweis können Sie zusammen mit dem Modul getattr auch eine __dir__Funktion auf Modulebene definieren, auf die reagiert werden soll dir(my_module). Siehe PEP 562 für Details.

wim
quelle
1
Wenn ich dynamisch ein Modul über m = ModuleType("mod")und setze m.__getattr__ = lambda attr: return "foo"; Wenn ich jedoch renne from mod import foo, bekomme ich TypeError: 'module' object is not iterable.
weberc2
@ weberc2: Machen Sie das m.__getattr__ = lambda attr: "foo", und Sie müssen einen Eintrag für das Modul mit definieren sys.modules['mod'] = m. Danach gibt es keinen Fehler mit from mod import foo.
Martineau
wim: Sie können auch dynamisch berechnete Werte abrufen, z. B. eine Eigenschaft auf Modulebene, sodass Sie schreiben können my_module.whatever, um sie aufzurufen (nach einem import my_module).
Martineau
115

Es gibt zwei grundlegende Probleme, auf die Sie hier stoßen:

  1. __xxx__ Methoden werden nur in der Klasse nachgeschlagen
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) bedeutet, dass jede Lösung auch verfolgen muss, welches Modul untersucht wird, andernfalls würde jedes Modul dann das Instanzersetzungsverhalten aufweisen; und (2) bedeutet, dass (1) nicht einmal möglich ist ... zumindest nicht direkt.

Glücklicherweise ist sys.modules nicht wählerisch, was dort vor sich geht, damit ein Wrapper funktioniert, sondern nur für import somemodule; somemodule.salutation('world')den Modulzugriff (dh für den Zugriff auf dasselbe Modul müssen Sie die Methoden aus der Substitutionsklasse ziehen und sie globals()mit einem zu einem hinzufügen Benutzerdefinierte Methode für die Klasse (die ich gerne verwende .export()) oder mit einer generischen Funktion (wie die bereits als Antworten aufgeführten). Beachten Sie Folgendes: Wenn der Wrapper jedes Mal eine neue Instanz erstellt und die globale Lösung nicht. Sie haben am Ende ein subtil anderes Verhalten. Oh, und Sie können nicht beide gleichzeitig verwenden - es ist das eine oder andere.


Aktualisieren

Von Guido van Rossum :

Es gibt tatsächlich einen Hack, der gelegentlich verwendet und empfohlen wird: Ein Modul kann eine Klasse mit der gewünschten Funktionalität definieren und sich am Ende in sys.modules durch eine Instanz dieser Klasse ersetzen (oder durch die Klasse, wenn Sie darauf bestehen , aber das ist im Allgemeinen weniger nützlich). Z.B:

# module foo.py

import sys

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

sys.modules[__name__] = Foo()

Dies funktioniert, weil die Importmaschinerie diesen Hack aktiv aktiviert und als letzten Schritt das eigentliche Modul nach dem Laden aus sys.modules herauszieht. (Dies ist kein Zufall. Der Hack wurde vor langer Zeit vorgeschlagen und wir entschieden, dass wir genug mochten, um ihn in den Importmaschinen zu unterstützen.)

Der etablierte Weg, um das zu erreichen, was Sie wollen, besteht darin, eine einzelne Klasse in Ihrem Modul zu erstellen und als letzten Akt des Moduls durch sys.modules[__name__]eine Instanz Ihrer Klasse zu ersetzen - und jetzt können Sie nach Bedarf mit __getattr__/ __setattr__/ spielen __getattribute__.


Hinweis 1 : Wenn Sie diese Funktionalität verwenden, gehen alle anderen Elemente des Moduls, wie z. B. Globals, andere Funktionen usw., bei der sys.modulesZuweisung verloren. Stellen Sie daher sicher, dass sich alle erforderlichen Elemente in der Ersatzklasse befinden .

Hinweis 2 : Zur Unterstützung from module import *müssen Sie __all__in der Klasse definiert haben . beispielsweise:

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

Abhängig von Ihrer Python-Version können andere Namen weggelassen werden __all__. Dies set()kann weggelassen werden, wenn keine Python 2-Kompatibilität erforderlich ist.

Ethan Furman
quelle
3
Dies funktioniert, weil die Importmaschinerie diesen Hack aktiv aktiviert und als letzten Schritt das eigentliche Modul nach dem Laden aus sys.modules herauszieht. Wird es irgendwo in den Dokumenten erwähnt?
Piotr Dobrogost
4
Jetzt fühle ich mich wohler mit diesem Hack, wenn man bedenkt, dass er "halb sanktioniert" ist :)
Mark Nunberg
3
Dies macht verrückte Dinge, wie das import sysGeben Nonefür sys. Ich vermute, dieser Hack wird in Python 2 nicht sanktioniert.
Asmeurer
3
@asmeurer: Um den Grund dafür (und eine Lösung) zu verstehen, lesen Sie die Frage Warum ändert sich der Wert von __name__ nach der Zuweisung zu sys.modules [__ name__]? .
Martineau
1
@qarma: Ich erinnere mich an einige Verbesserungen, über die gesprochen wurde, die es Python-Modulen ermöglichen würden, direkter am Klassenvererbungsmodell teilzunehmen, aber trotzdem funktioniert diese Methode immer noch und wird unterstützt.
Ethan Furman
48

Dies ist ein Hack, aber Sie können das Modul mit einer Klasse umschließen:

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.
quelle
10
nett und dreckig: D
Matt Joiner
Das mag funktionieren, ist aber wahrscheinlich keine Lösung für das eigentliche Problem des Autors.
DasIch
12
"Kann funktionieren" und "wahrscheinlich nicht" ist nicht sehr hilfreich. Es ist ein Hack / Trick, aber es funktioniert und löst das Problem, das sich aus der Frage ergibt.
Håvard S
6
Während dies in anderen Modulen funktioniert , die Ihr Modul importieren und auf nicht vorhandene Attribute zugreifen, funktioniert es hier nicht für das eigentliche Codebeispiel. Der Zugriff auf globals () erfolgt nicht über sys.modules.
Marius Gedminas
5
Leider funktioniert dies nicht für das aktuelle Modul oder wahrscheinlich für Dinge, auf die nach einem zugegriffen wird import *.
Matt Joiner
19

Wir machen das normalerweise nicht so.

Was wir tun, ist dies.

class A(object):
....

# The implicit global instance
a= A()

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

Warum? Damit ist die implizite globale Instanz sichtbar.

Schauen Sie sich zum Beispiel das randomModul an, das eine implizite globale Instanz erstellt, um die Anwendungsfälle, in denen Sie einen "einfachen" Zufallszahlengenerator benötigen, leicht zu vereinfachen.

S.Lott
quelle
Wenn Sie wirklich ehrgeizig sind, können Sie die Klasse erstellen, alle Methoden durchlaufen und für jede Methode eine Funktion auf Modulebene erstellen.
Paul Fisher
@ Paul Fisher: Gemäß dem Problem existiert die Klasse bereits. Es ist möglicherweise keine gute Idee, alle Methoden der Klasse offenzulegen. Normalerweise sind diese exponierten Methoden "Convenience" -Methoden. Nicht alle sind für die implizite globale Instanz geeignet.
S.Lott
13

Ähnlich wie bei @ Håvard S vorgeschlagen, würde ich in einem Fall, in dem ich etwas Magie auf einem Modul (wie __getattr__) implementieren musste , eine neue Klasse definieren, die von dieser erbt types.ModuleTypeund diese einfügt sys.modules(wahrscheinlich das Modul ersetzen, in dem meine Gewohnheit ModuleTypedefiniert wurde).

Eine ziemlich robuste Implementierung finden Sie in der Hauptdatei __init__.pyvon Werkzeug .

Matt Anderson
quelle
8

Das ist hackisch, aber ...

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

Dies funktioniert, indem alle Objekte im globalen Namespace durchlaufen werden. Wenn das Element eine Klasse ist, durchläuft es die Klassenattribute. Wenn das Attribut aufrufbar ist, wird es als Funktion zum globalen Namespace hinzugefügt.

Es werden alle Attribute ignoriert, die "__" enthalten.

Ich würde dies nicht im Produktionscode verwenden, aber es sollte Ihnen den Einstieg erleichtern.

trauern
quelle
2
Ich ziehe die Antwort von Håvard S meiner vor, da sie viel sauberer erscheint, aber dies beantwortet die gestellte Frage direkt.
Trauer
Dies ist viel näher an dem, was ich letztendlich gemacht habe. Es ist ein wenig chaotisch, funktioniert aber korrekt mit globals () innerhalb desselben Moduls.
Matt Joiner
1
Mir scheint, diese Antwort ist nicht ganz das, wonach gefragt wurde: "Wenn eine Funktion aufgerufen wird, die in den statisch definierten Attributen eines Moduls nicht vorhanden ist", weil sie bedingungslos funktioniert und jede mögliche Klassenmethode hinzufügt. Dies könnte behoben werden, indem ein Modul-Wrapper verwendet wird, der nur dann ausgeführt wird, AddGlobalAttribute()wenn eine Modulebene vorhanden ist AttributeError- eine Art Umkehrung der Logik von @ Håvard S. Wenn ich eine Chance habe, werde ich dies testen und meine eigene hybride Antwort hinzufügen, obwohl das OP diese Antwort bereits akzeptiert hat.
Martineau
Update auf meinen vorherigen Kommentar. Ich verstehe jetzt, dass es sehr schwierig (unmöglich) ist, NameErrorAusnahmen für den globalen (Modul-) Namespace abzufangen - was erklärt, warum diese Antwort dem aktuellen globalen Namespace Callables für jede Möglichkeit hinzufügt, die er findet, um jeden möglichen Fall im Voraus abzudecken.
Martineau
5

Hier ist mein bescheidener Beitrag - eine leichte Verschönerung der hoch bewerteten Antwort von @ Håvard S, aber etwas expliziter (so könnte es für @ S.Lott akzeptabel sein, obwohl es für das OP wahrscheinlich nicht gut genug ist):

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
quelle
-3

Erstellen Sie Ihre Moduldatei mit Ihren Klassen. Importieren Sie das Modul. Führen getattrSie das gerade importierte Modul aus. Sie können einen dynamischen Import mit durchführen__import__ und das Modul aus sys.modules ziehen.

Hier ist dein Modul some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

Und in einem anderen Modul:

import some_module

Foo = getattr(some_module, 'Foo')

Dynamisch machen:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')
drr
quelle
1
Sie beantworten hier eine andere Frage.
Marius Gedminas