__getattr__ w module


127

Jak można zaimplementować odpowiednik a __getattr__w klasie, w module?

Przykład

Podczas wywoływania funkcji, która nie istnieje w statycznie zdefiniowanych atrybutach modułu, chcę utworzyć wystąpienie klasy w tym module i wywołać na niej metodę o tej samej nazwie, która nie powiodła się w wyszukiwaniu atrybutów w module.

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

Co daje:

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
Prawdopodobnie pójdę z odpowiedzią żalu, ponieważ działa w każdych okolicznościach (choć jest trochę niechlujny i można to zrobić lepiej). Harvard S i S Lott mają ładne, jasne odpowiedzi, ale nie są to praktyczne rozwiązania.
— Matt Joiner

1
Nie jesteś w swoim przypadku, nawet jeśli chodzi o dostęp do atrybutów, więc prosisz o dwie różne rzeczy naraz. Więc głównym pytaniem jest, który z nich chcesz. Czy chcesz salutationistnieć w globalnej lub lokalnej przestrzeni nazw (co próbuje zrobić powyższy kod), czy chcesz dynamicznego wyszukiwania nazw podczas wykonywania dostępu z kropką w module? To dwie różne rzeczy.
— Lennart Regebro

Ciekawe pytanie, jak na to wpadłeś?
— Chris Wesseling,

1
możliwy duplikat Autoloada w Pythonie
— Piotr Dobrogost

1
__getattr__na modułach jest obsługiwany od Pythona 3.7
— Fumito Hamamura

Odpowiedzi:


42

Jakiś czas temu Guido zadeklarował, że wszystkie wyszukiwania metod specjalnych w klasach nowego stylu pomijają __getattr__i__getattribute__ . Metody Dunder pracowały wcześniej na modułach - można na przykład użyć modułu jako menedżera kontekstu, po prostu definiując __enter__i __exit__zanim te sztuczki się zepsuły .

Ostatnio niektóre funkcje historyczne powróciły, a __getattr__wśród nich moduł , więc istniejący hack (moduł zastępujący się klasą w sys.modulesczasie importu) nie powinien być już potrzebny.

W Pythonie 3.7+ używasz tylko jednego oczywistego sposobu. Aby dostosować dostęp do atrybutów w module, zdefiniuj __getattr__funkcję na poziomie modułu, która powinna akceptować jeden argument (nazwę atrybutu) i zwrócić obliczoną wartość lub podnieść AttributeError:

# my_module.py

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

Pozwoli to również na przechwycenie do importu „z”, tj. Możesz zwracać dynamicznie generowane obiekty dla instrukcji, takich jak from my_module import whatever.

W podobnej notatce, wraz z modułem getattr, możesz także zdefiniować __dir__funkcję na poziomie modułu, na którą ma odpowiadać dir(my_module). Szczegóły w PEP 562 .


1
Jeśli dynamicznie utworzę moduł za pośrednictwem m = ModuleType("mod")i ustawię m.__getattr__ = lambda attr: return "foo"; jednak kiedy biegnę from mod import foo, dostaję TypeError: 'module' object is not iterable.
— weberc2

@ weberc2: Zrób to m.__getattr__ = lambda attr: "foo", dodatkowo musisz zdefiniować wpis dla modułu za pomocą sys.modules['mod'] = m. Później nie ma błędu w from mod import foo.
— martineau

wim: Możesz także pobierać wartości obliczane dynamicznie - na przykład posiadanie właściwości na poziomie modułu - pozwalając na napisanie w my_module.whatevercelu wywołania (po import my_module).
— martineau

115

Występują tutaj dwa podstawowe problemy:

  1. __xxx__ metody są wyszukiwane tylko w klasie
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) oznacza, że ​​każde rozwiązanie musiałoby również śledzić, który moduł był badany, w przeciwnym razie każdy moduł miałby wówczas zachowanie polegające na zastępowaniu instancji; a (2) oznacza, że ​​(1) nie jest nawet możliwe ... przynajmniej nie bezpośrednio.

Na szczęście sys.modules nie jest wybredny, jeśli chodzi o to, co tam jest, więc opakowanie będzie działać, ale tylko w przypadku dostępu do modułu (tj. Aby uzyskać dostęp import somemodule; somemodule.salutation('world')do tego samego modułu, musisz prawie globals()wyciągnąć metody z klasy podstawienia i dodać je do eiher za pomocą niestandardową metodę w klasie (lubię używać .export()) lub z funkcją ogólną (taką jak te już wymienione jako odpowiedzi). Należy pamiętać o jednej rzeczy: jeśli opakowanie tworzy za każdym razem nową instancję, a rozwiązanie globalne nie, kończysz z subtelnie innym zachowaniem. Och, i nie możesz używać obu jednocześnie - to jedno lub drugie.


Aktualizacja

Od Guido van Rossuma :

W rzeczywistości istnieje hack, który jest czasami używany i zalecany: moduł może zdefiniować klasę z pożądaną funkcjonalnością, a na koniec zamienić się w sys.modules na instancję tej klasy (lub na klasę, jeśli nalegasz , ale to generalnie mniej przydatne). Na przykład:

# module foo.py

import sys

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

sys.modules[__name__] = Foo()

Działa to, ponieważ maszyna importująca aktywnie włącza ten hack i jako ostatni krok wyciąga rzeczywisty moduł z sys.modules po załadowaniu go. (To nie przypadek. Hack został zaproponowany dawno temu i zdecydowaliśmy, że podoba nam się wystarczająco, aby wspierać go w imporcie maszyn).

Tak więc ustalonym sposobem osiągnięcia tego, co chcesz, jest utworzenie pojedynczej klasy w swoim module i jako ostatni akt modułu zastąp sys.modules[__name__]go instancją swojej klasy - a teraz możesz grać z __getattr__/ __setattr__/ __getattribute__w razie potrzeby.


Uwaga 1 : Jeśli użyjesz tej funkcji, wszystko inne w module, takie jak globalne, inne funkcje itp. Zostanie utracone po sys.modulesprzypisaniu - więc upewnij się, że wszystko, co potrzebne, znajduje się w klasie zastępczej.

Uwaga 2 : Aby wspierać from module import *, musisz __all__zdefiniować w klasie; na przykład:

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

W zależności od wersji Pythona mogą istnieć inne nazwy do pominięcia __all__. set()Można pominąć, jeżeli Python 2 kompatybilność nie jest potrzebne.


3
Działa to, ponieważ mechanizm importu aktywnie włącza ten hack, a gdy jego ostatni krok wyciąga rzeczywisty moduł z sys.modules, po załadowaniu go Czy jest wspomniany gdzieś w dokumentacji?
— Piotr Dobrogost

4
Teraz czuję się bardziej swobodnie używając tego hack, uznając ją „semi-usankcjonowane” :)
— Mark Nunberg

3
To robi screwy rzeczy, jak co import sysdać Nonena sys. Zgaduję, że ten hack nie jest usankcjonowany w Pythonie 2.
— asmeurer

3
@asmeurer: Aby zrozumieć przyczynę tego (i rozwiązanie), zobacz pytanie Dlaczego wartość __name__ zmienia się po przypisaniu do sys.modules [__ name__]? .
— martineau,

1
@qarma: Wydaje mi się, że przypominam sobie pewne ulepszenia, o których mówiono, które pozwoliłyby modułom Pythona na bardziej bezpośredni udział w modelu dziedziczenia klas, ale mimo to ta metoda nadal działa i jest obsługiwana.
— Ethan Furman

48

To hack, ale możesz opakować moduł klasą:

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
ładne i brudne: D
— Matt Joiner

To może działać, ale prawdopodobnie nie jest rozwiązaniem prawdziwego problemu autora.
— DasIch

12
„Może zadziałać” i „prawdopodobnie nie” nie są zbyt pomocne. To hack / trick, ale działa i rozwiązuje problem związany z pytaniem.
— Håvard S

6
Chociaż będzie to działać w innych modułach, które importują Twój moduł i uzyskują dostęp do nieistniejących atrybutów, nie będzie działać dla rzeczywistego przykładu kodu tutaj. Dostęp do globals () nie przechodzi przez sys.modules.
— Marius Gedminas

5
Niestety to nie działa dla bieżącego modułu lub prawdopodobnie dla rzeczy, do których uzyskano dostęp po pliku import *.
— Matt Joiner

19

Zwykle nie robimy tego w ten sposób.

To, co robimy, to to.

class A(object):
....

# The implicit global instance
a= A()

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

Czemu? Aby niejawna instancja globalna była widoczna.

Na przykład spójrz na randommoduł, który tworzy niejawną instancję globalną, aby nieco uprościć przypadki użycia, w których potrzebny jest „prosty” generator liczb losowych.


Jeśli jesteś naprawdę ambitny, możesz stworzyć klasę i powtórzyć wszystkie jej metody i utworzyć funkcję na poziomie modułu dla każdej metody.
— Paul Fisher

@Paul Fisher: Zgodnie z problemem, klasa już istnieje. Ujawnienie wszystkich metod klasy może nie być dobrym pomysłem. Zwykle te ujawnione metody są metodami „wygodnymi”. Nie wszystkie są odpowiednie dla niejawnej instancji globalnej.
— S.Lott

13

Podobnie do tego, co zaproponował @ Håvard S, w przypadku, gdy potrzebowałem zaimplementować jakąś magię na module (np. __getattr__), Zdefiniowałbym nową klasę, która dziedziczy types.ModuleTypei umieściłbym ją sys.modules(prawdopodobnie zastępując moduł, w którym ModuleTypezdefiniowano mój niestandardowy ).

Zobacz główny __init__.pyplik Werkzeug, aby uzyskać dość solidną implementację tego.


8

To jest hacking, ale ...

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

Działa to poprzez iterację po wszystkich obiektach w globalnej przestrzeni nazw. Jeśli element jest klasą, iteruje po atrybutach klasy. Jeśli atrybut jest wywoływalny, dodaje go do globalnej przestrzeni nazw jako funkcję.

Ignoruje wszystkie atrybuty zawierające „__”.

Nie użyłbym tego w kodzie produkcyjnym, ale powinno ci to pomóc.


2
Wolę odpowiedź Håvarda S. od mojej, ponieważ wydaje się ona znacznie czystsza, ale to bezpośrednio odpowiada na zadane pytanie.
— opłakiwać

To jest dużo bliższe temu, z czym w końcu poszedłem. Jest trochę bałaganiarski, ale działa poprawnie z globals () w tym samym module.
— Matt Joiner

1
Wydaje mi się, że ta odpowiedź nie jest dokładnie tym, o co pytano, a które brzmiało: „Podczas wywoływania funkcji, która nie istnieje w statycznie zdefiniowanych atrybutach modułu”, ponieważ wykonuje swoją pracę bezwarunkowo i dodaje każdą możliwą metodę klasy. Można to naprawić, używając otoki modułu, która robi to tylko AddGlobalAttribute()wtedy, gdy istnieje poziom modułu AttributeError- coś w rodzaju odwrotności logiki @ Håvard S. Jeśli będę miał okazję, przetestuję to i dodam własną odpowiedź hybrydową, mimo że OP zaakceptował już tę odpowiedź.
— martineau

Zaktualizuj do mojego poprzedniego komentarza. Rozumiem teraz, że przechwycenie NameErrorwyjątków dla globalnej (modułu) przestrzeni nazw jest bardzo trudne (nieskomplikowane?) - co wyjaśnia, dlaczego ta odpowiedź dodaje wywołania dla każdej możliwości, jaką znajdzie w bieżącej globalnej przestrzeni nazw, aby uwzględnić każdy możliwy przypadek z wyprzedzeniem.
— martineau

5

Oto mój własny skromny wkład - niewielkie upiększenie wysoko ocenianej odpowiedzi @ Håvard S, ale nieco bardziej wyraźne (więc może być do zaakceptowania dla @ S.Lott, nawet jeśli prawdopodobnie nie jest wystarczająco dobre dla 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

Utwórz plik modułu zawierający Twoje klasy. Zaimportuj moduł. Uruchom getattrna module, który właśnie zaimportowałeś. Możesz wykonać dynamiczny import za pomocą __import__i wyciągnąć moduł z sys.modules.

Oto Twój moduł some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

A w innym module:

import some_module

Foo = getattr(some_module, 'Foo')

Robiąc to dynamicznie:

import sys

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

1
Odpowiadasz tutaj na inne pytanie.
— Marius Gedminas
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.