__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
Matt Joiner
źródło
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 .

wim
źródło
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.

Ethan Furman
źródło
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__])
Håvard S
źródło
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.

S.Lott
źródło
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.

Matt Anderson
źródło
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.

smucić
źródło
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")
martineau
źródło
-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')
drr
źródło
1
Odpowiadasz tutaj na inne pytanie.
Marius Gedminas