Können Module Eigenschaften genauso haben wie Objekte?

98

Mit Python-Eigenschaften kann ich es so machen, dass

obj.y 

ruft eine Funktion auf, anstatt nur einen Wert zurückzugeben.

Gibt es eine Möglichkeit, dies mit Modulen zu tun? Ich habe einen Fall, wo ich will

module.y 

eine Funktion aufrufen, anstatt nur den dort gespeicherten Wert zurückzugeben.

Josh Gibson
quelle
2
Eine modernere Lösung finden Sie __getattr__in einem Modul .
wim

Antworten:

56

Nur Instanzen von Klassen neuen Stils können Eigenschaften haben. Sie können Python glauben machen, dass eine solche Instanz ein Modul ist, indem Sie sie verstecken sys.modules[thename] = theinstance. So könnte Ihre m.py-Moduldatei beispielsweise sein:

import sys

class _M(object):
    def __init__(self):
        self.c = 0
    def afunction(self):
        self.c += 1
        return self.c
    y = property(afunction)

sys.modules[__name__] = _M()
Alex Martelli
quelle
2
Hat das noch jemand versucht? Wenn ich diesen Code in eine Datei x.py lege und aus einer anderen importiere, führt der Aufruf von xy zu AttributeError: Das Objekt 'NoneType' hat kein Attribut 'c', da _M irgendwie den Wert None hat ...
Stephan202
3
In der Tat funktioniert der Code auf dem Interpreter. Aber wenn ich es in eine Datei (z. B. bowwow.py) lege und es aus einer anderen Datei (otherfile.py) importiere, funktioniert es nicht mehr ...
Stephan202
4
F: Wäre es ein besonderer Vorteil, die Klasse der Instanz abzuleiten, types.ModuleTypewie in der ansonsten sehr ähnlichen Antwort von @ Unknown gezeigt?
Martineau
11
Nur Instanzen von Klassen neuen Stils können Eigenschaften haben. Dies ist nicht der Grund: Module sind Instanzen von Klassen neuen Stils, da sie Instanzen von sind builtins.module, von denen selbst eine Instanz ist type(was die Definition einer Klasse neuen Stils ist). Das Problem ist, dass Eigenschaften für die Klasse und nicht für die Instanz vorliegen müssen. Wenn Sie dies tun f = Foo(), f.some_property = property(...)schlägt dies genauso fehl, als ob Sie es naiv in ein Modul einfügen. Die Lösung besteht darin, es in die Klasse einzufügen. Da jedoch nicht alle Module die Eigenschaft haben sollen, wird eine Unterklasse erstellt (siehe Antwort von Unknown).
Thanatos
3
@Joe, die Änderung von globals()(die Schlüssel intakt halten, aber die Werte auf zurücksetzen None) beim erneuten Binden des Namens sys.modulesist ein Python 2-Problem - Python 3.4 funktioniert wie beabsichtigt. Wenn Sie Zugriff auf das Klassenobjekt in Py2 benötigen, fügen Sie z. B. _M._cls = _Mdirekt nach der classAnweisung hinzu (oder speichern Sie es äquivalent in einem anderen Namespace) und greifen Sie darauf zu, wie self._clsin den Methoden, die dies erfordern ( type(self)möglicherweise in Ordnung, aber nicht, wenn Sie auch eine Unterklasse von ausführen _M). .
Alex Martelli
54

Ich würde dies tun, um alle Attribute eines Moduls richtig zu erben und durch isinstance () korrekt identifiziert zu werden.

import types

class MyModule(types.ModuleType):
    @property
    def y(self):
        return 5


>>> a=MyModule("test")
>>> a
<module 'test' (built-in)>
>>> a.y
5

Und dann können Sie dies in sys.modules einfügen:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class
Unbekannt
quelle
Dies scheint nur für den einfachsten Fall zu funktionieren. Mögliche Probleme sind: (1) Einige Import-Helfer erwarten möglicherweise auch andere Attribute __file__, die manuell definiert werden müssen. (2) Im Modul, das die Klasse enthält, vorgenommene Importe werden zur Laufzeit nicht "sichtbar" usw.
tutuDajuju
1
Es ist nicht notwendig , eine Unterklasse von abzuleiten types.ModuleType, jede (neuem Stil) Klasse tun wird. Welche speziellen Modulattribute möchten Sie erben?
Martineau
Was ist, wenn das Originalmodul ein Paket ist und ich auf Module unterhalb des Originalmoduls zugreifen möchte?
Kawing-Chiu
2
@martineau Sie haben ein Modul repr, Sie können den Modulnamen bei __init__einer Instanz angeben und Sie erhalten ein korrektes Verhalten bei der Verwendung isinstance.
Wim
@wim: Punkte genommen, obwohl offen gesagt keiner so wichtig zu sein scheint IMO.
Martineau
33

Da PEP 562 in Python> = 3.7 implementiert wurde, können wir dies jetzt tun

Datei: module.py

def __getattr__(name):
    if name == 'y':
        return 3
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

other = 4

Verwendung:

>>> import module
>>> module.y
3
>>> module.other
4
>>> module.nosuch
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "module.py", line 4, in __getattr__
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
AttributeError: module 'module' has no attribute 'nosuch'

Beachten Sie, dass, wenn Sie das raise AttributeErrorin der __getattr__Funktion weglassen , dies bedeutet, dass die Funktion mit endet return Noneund das module.nosucheinen Wert von erhält None.

John Lin
quelle
2
Auf dieser Grundlage habe ich eine weitere Antwort hinzugefügt: stackoverflow.com/a/58526852/2124834
3
Dies ist nur die Hälfte einer Immobilie. Kein Setter.
wim
Leider scheint es nicht schwer zu sein, Werkzeuge auf solche Attribute aufmerksam zu machen (?) ( Getattr wird nur aufgerufen, wenn kein reguläres Mitglied gefunden wird)
olejorgenb
9

Basierend auf John Lins Antwort :

def module_property(func):
    """Decorator to turn module functions into properties.
    Function names must be prefixed with an underscore."""
    module = sys.modules[func.__module__]

    def base_getattr(name):
        raise AttributeError(
            f"module '{module.__name__}' has no attribute '{name}'")

    old_getattr = getattr(module, '__getattr__', base_getattr)

    def new_getattr(name):
        if f'_{name}' == func.__name__:
            return func()
        else:
            return old_getattr(name)

    module.__getattr__ = new_getattr
    return func

Verwendung (beachten Sie den führenden Unterstrich), in the_module.py:

@module_property
def _thing():
    return 'hello'

Dann:

import the_module

print(the_module.thing)  # prints 'hello'

Der führende Unterstrich ist erforderlich, um die eigenschaftsbezogene Funktion von der ursprünglichen Funktion zu unterscheiden. Ich konnte mir keine Möglichkeit vorstellen, die Kennung neu zuzuweisen, da sie während der Ausführung des Dekorateurs noch nicht zugewiesen wurde.

Beachten Sie, dass IDEs nicht wissen, dass die Eigenschaft vorhanden ist, und rote Wellen anzeigen.


quelle
Toll! Im Vergleich zu Klasseneigenschaften @property def x(self): return self._xhalte ich es def thing()ohne Unterstrich für konventioneller. Und können Sie in Ihrer Antwort auch einen Dekorator für "Module Property Setter" erstellen?
John Lin
2
@ JohnLin, ich habe versucht, Ihren def thing()Vorschlag umzusetzen . Problem ist, dass __getattr__nur für fehlende Attribute aufgerufen wird . Aber nach @module_property def thing(): …Läufen the_module.thingist definiert, so dass getattr nie aufgerufen wird. Wir müssen uns irgendwie thingim Dekorator registrieren und ihn dann aus dem Namespace des Moduls löschen. Ich habe versucht, Nonevom Dekorateur zurückzukehren, aber dann thingist definiert als None. Man könnte es tun, @module_property def thing(): … del thingaber ich finde das schlimmer als thing()als Funktion zu verwenden
Ben Mares
OK Ich sehe, es gibt weder einen "Module Property Setter" noch ein "Modul __getattribute__". Danke dir.
John Lin
5

Ein typischer Anwendungsfall ist: Anreichern eines (riesigen) vorhandenen Moduls mit einigen (wenigen) dynamischen Attributen - ohne das gesamte Modulmaterial in ein Klassenlayout umzuwandeln. Leider sys.modules[__name__].__class__ = MyPropertyModuleschlägt ein einfachster Modulklassen-Patch wie fehl TypeError: __class__ assignment: only for heap types. Daher muss die Modulerstellung neu verkabelt werden.

Dieser Ansatz macht es ohne Python-Import-Hooks, nur indem ein Prolog über dem Modulcode steht:

# propertymodule.py
""" Module property example """

if '__orgmod__' not in globals():

    # constant prolog for having module properties / supports reload()

    print "PropertyModule stub execution", __name__
    import sys, types
    class PropertyModule(types.ModuleType):
        def __str__(self):
            return "<PropertyModule %r from %r>" % (self.__name__, self.__file__)
    modnew = PropertyModule(__name__, __doc__)
    modnew.__modclass__ = PropertyModule        
    modnew.__file__ = __file__
    modnew.__orgmod__ = sys.modules[__name__]
    sys.modules[__name__] = modnew
    exec sys._getframe().f_code in modnew.__dict__

else:

    # normal module code (usually vast) ..

    print "regular module execution"
    a = 7

    def get_dynval(module):
        return "property function returns %s in module %r" % (a * 4, module.__name__)    
    __modclass__.dynval = property(get_dynval)

Verwendung:

>>> import propertymodule
PropertyModule stub execution propertymodule
regular module execution
>>> propertymodule.dynval
"property function returns 28 in module 'propertymodule'"
>>> reload(propertymodule)   # AFTER EDITS
regular module execution
<module 'propertymodule' from 'propertymodule.pyc'>
>>> propertymodule.dynval
"property function returns 36 in module 'propertymodule'"

Hinweis: So etwas from propertymodule import dynvalerzeugt natürlich eine eingefrorene Kopie - entsprechenddynval = someobject.dynval

kxr
quelle
1

Eine kurze Antwort: verwenden proxy_tools

Das proxy_toolsPaket versucht, @module_propertyFunktionen bereitzustellen .

Es wird mit installiert

pip install proxy_tools

Mit einer geringfügigen Modifikation von @ Mareins Beispiel the_module.pysetzen wir ein

from proxy_tools import module_property

@module_property
def thing():
    print(". ", end='')  # Prints ". " on each invocation
    return 'hello'

Jetzt aus einem anderen Skript kann ich tun

import the_module

print(the_module.thing)
# . hello

Unerwartetes Verhalten

Diese Lösung ist nicht ohne Einschränkungen. Das heißt, the_module.thingist kein String ! Es ist ein proxy_tools.ProxyObjekt, dessen spezielle Methoden überschrieben wurden, damit es eine Zeichenfolge nachahmt. Hier sind einige grundlegende Tests, die den Punkt veranschaulichen:

res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]

print(type(res))
# <class 'proxy_tools.Proxy'>

print(isinstance(res, str))
# False

print(res)
# . hello

print(res + " there")
# . hello there

print(isinstance(res + "", str))
# . True

print(res.split('e'))
# . ['h', 'llo']

Intern wird die ursprüngliche Funktion gespeichert in the_module.thing._Proxy__local:

print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>

Weitere Gedanken

Ehrlich gesagt bin ich verblüfft darüber, warum Module diese Funktionalität nicht eingebaut haben. Ich denke, der springende Punkt ist, dass the_modulees sich um eine Instanz der types.ModuleTypeKlasse handelt. Das Festlegen einer "Moduleigenschaft" entspricht dem Festlegen einer Eigenschaft für eine Instanz dieser Klasse und nicht für die types.ModuleTypeKlasse selbst. Weitere Einzelheiten finden Sie in dieser Antwort .

Wir können Eigenschaften types.ModuleTypewie folgt implementieren , obwohl die Ergebnisse nicht großartig sind. Wir können eingebaute Typen nicht direkt ändern, aber wir können sie verfluchen :

# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))

Dies gibt uns eine Eigenschaft, die über alle Module existiert. Es ist etwas unhandlich, da wir das Einstellungsverhalten über alle Module hinweg aufteilen:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
quelle
1
Wie ist das besser, als das Modul nur zu einer Instanz einer echten Klasse zu machen, wie in der Antwort von @Alex Martelli gezeigt?
Martineau
1
Sie haben etwas anderes gesagt, das für mich keinen Sinn ergibt. Nehmen Sie dieses Geschäft über einen @module_propertyDekorateur. Im Allgemeinen wird der integrierte @propertyDekorator verwendet, wenn eine Klasse definiert wird, nicht nachdem eine Instanz erstellt wurde. Daher würde ich davon ausgehen, dass dies auch für eine Moduleigenschaft gilt, und dies gilt für Alex 'Antwort - erinnern Sie sich an diese Frage "Können Module Eigenschaften haben, die Objekte haben können?" Es ist jedoch möglich, sie später hinzuzufügen, und ich habe mein früheres Snippet geändert, um zu veranschaulichen, wie dies getan werden kann.
Martineau
1
Ben: Nachdem ich mir den Code in Ihrem konkreten Beispiel angesehen habe, glaube ich zu verstehen, worauf Sie gerade hinaus wollen. Ich denke auch, dass ich kürzlich auf eine Technik gestoßen bin, um etwas zu implementieren, das Moduleigenschaften ähnelt, bei denen das Modul nicht durch eine Klasseninstanz wie in Alex 'Antwort ersetzt werden muss, obwohl ich mir zu diesem Zeitpunkt nicht sicher bin, ob es eine Möglichkeit gibt es über einen Dekorateur - wird sich bei Ihnen melden, wenn ich Fortschritte mache.
Martineau
1
OK, hier ist ein Link zu einer Antwort auf eine andere Frage, die die Kernidee enthält.
Martineau
1
Nun, zumindest im Fall von a cached_module_propertyist die Tatsache __getattr__()hilfreich , dass nicht mehr aufgerufen wird, wenn das Attribut definiert wird. (ähnlich dem, was functools.cached_propertyerreicht wird).
Martineau