Czy moduły mogą mieć takie same właściwości, jak obiekty?

100

Dzięki właściwościom Pythona mogę to zrobić

obj.y 

wywołuje funkcję, a nie tylko zwraca wartość.

Czy można to zrobić za pomocą modułów? Mam przypadek, w którym chcę

module.y 

aby wywołać funkcję, zamiast zwracać przechowywaną tam wartość.

Josh Gibson
źródło
2
Zobacz __getattr__moduł dla bardziej nowoczesnego rozwiązania.
wim

Odpowiedzi:

56

Tylko wystąpienia klas w nowym stylu mogą mieć właściwości. Możesz przekonać Pythona, że ​​taka instancja jest modułem, przechowując ją w sys.modules[thename] = theinstance. Na przykład twój plik modułu m.py może wyglądać następująco:

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
źródło
2
Czy ktoś jeszcze tego próbował? Kiedy umieściłem ten kod w jednym pliku x.py i zaimportowałem go z innego, wywołanie xy daje w wyniku AttributeError: obiekt 'NoneType' nie ma atrybutu 'c', ponieważ _M ma jakoś wartość None ...
Stephan202
3
Rzeczywiście, kod działa na tłumaczu. Ale kiedy umieszczam go w pliku (powiedzmy, bowwow.py) i importuję go z innego pliku (otherfile.py), to już nie działa ...
Stephan202
4
P: Czy byłoby jakieś szczególne korzyści z wyprowadzenia klasy instancji z, types.ModuleTypejak pokazano w bardzo podobnej odpowiedzi @ Unknown?
martineau
11
Tylko wystąpienia klas w nowym stylu mogą mieć właściwości. To nie jest powód: moduły instancjami klas w nowym stylu, ponieważ są instancjami builtins.module, które same w sobie są instancjami type(co jest definicją klasy w nowym stylu). Problem polega na tym, że właściwości muszą znajdować się w klasie, a nie w instancji: jeśli to zrobisz f = Foo(), f.some_property = property(...)zakończy się niepowodzeniem w taki sam sposób, jak gdybyś naiwnie umieścił ją w module. Rozwiązaniem jest umieszczenie go w klasie, ale ponieważ nie chcesz, aby wszystkie moduły miały tę właściwość, tworzysz podklasę (zobacz odpowiedź Nieznane).
Thanatos
3
@Joe, zmiana globals()(utrzymywanie kluczy w stanie nienaruszonym, ale resetowanie wartości None) po ponownym powiązaniu nazwy w sys.modulesjest problemem Pythona 2 - Python 3.4 działa zgodnie z przeznaczeniem. Jeśli potrzebujesz dostępu do obiektu klasy w Py2, dodaj np. Bezpośrednio _M._cls = _Mza classinstrukcją (lub schowaj ją równoważnie w jakiejś innej przestrzeni nazw) i uzyskaj do niego dostęp tak jak self._clsw metodach, które tego wymagają ( type(self)może być OK, ale nie, jeśli wykonasz również jakąkolwiek podklasę _M) .
Alex Martelli
54

Zrobiłbym to, aby poprawnie odziedziczyć wszystkie atrybuty modułu i zostać poprawnie zidentyfikowanym przez isinstance ()

import types

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


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

Następnie możesz wstawić to do sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class
Nieznany
źródło
Wydaje się, że działa to tylko w najprostszych przypadkach. Możliwe problemy to: (1) niektórzy pomocnicy importu mogą również oczekiwać innych atrybutów, takich jak te, __file__które muszą być zdefiniowane ręcznie, (2) importy wykonane w module zawierającym klasę nie będą "widoczne" w czasie wykonywania itd ...
tutuDajuju
1
Nie jest konieczne tworzenie podklasy z types.ModuleType, każda klasa (w nowym stylu) wystarczy. Jakie dokładnie atrybuty modułu mają zostać odziedziczone?
martineau
Co się stanie, jeśli oryginalny moduł jest pakietem i chcę uzyskać dostęp do modułów znajdujących się pod oryginalnym modułem?
kawing-chiu
2
@martineau Będziesz mieć moduł repr, możesz określić nazwę modułu, gdy __init__wystąpi, i uzyskasz prawidłowe zachowanie podczas używania isinstance.
wim
@wim: Punkty zebrane, chociaż szczerze mówiąc, żaden nie wydaje się być aż tak ważnym IMO.
martineau
35

Ponieważ PEP 562 został zaimplementowany w Pythonie> = 3.7, teraz możemy to zrobić

plik: module.py

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

other = 4

stosowanie:

>>> 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'

Zauważ, że jeśli pominiesz raise AttributeErrorw __getattr__funkcji, oznacza to, że funkcja kończy się na return None, a następnie module.nosuchotrzyma wartość None.

John Lin
źródło
2
Na tej podstawie dodałem kolejną odpowiedź: stackoverflow.com/a/58526852/2124834
3
To tylko połowa posiadłości. Żadnego setera.
wim
Niestety nie wydaje się trudne uświadomienie narzędziom takich atrybutów (?) ( Getattr jest wywoływane tylko wtedy, gdy nie zostanie znaleziony żaden zwykły członek)
olejorgenb
9

Na podstawie odpowiedzi Johna Lina :

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

Użycie (zwróć uwagę na wiodące podkreślenie), w the_module.py:

@module_property
def _thing():
    return 'hello'

Następnie:

import the_module

print(the_module.thing)  # prints 'hello'

Główny znak podkreślenia jest niezbędny do odróżnienia funkcji przypisanej własności od funkcji oryginalnej. Nie mogłem wymyślić sposobu na ponowne przypisanie identyfikatora, ponieważ w czasie wykonywania dekoratora nie został on jeszcze przypisany.

Zauważ, że IDE nie będą wiedzieć, że właściwość istnieje i pokażą czerwone fale.


źródło
Wspaniały! W porównaniu z właściwościami klas, @property def x(self): return self._xmyślę, że def thing()bez podkreślenia jest bardziej konwencjonalne. Czy możesz również utworzyć dekorator „ustawiający właściwości modułu” w swojej odpowiedzi?
John Lin
2
@JohnLin, próbowałem wdrożyć twoją def thing()sugestię. Problem polega na tym, że __getattr__jest wywoływany tylko w przypadku brakujących atrybutów . Ale po @module_property def thing(): …uruchomieniach the_module.thingjest zdefiniowane, więc getattr nigdy nie zostanie wywołane. Musimy jakoś zarejestrować się thingw dekoratorze, a następnie usunąć go z przestrzeni nazw modułu. Próbowałem wrócić Noneod dekoratora, ale potem thingjest zdefiniowany jako None. Można to zrobić, @module_property def thing(): … del thingale uważam, że jest to gorsze niż używanie thing()jako funkcji
Ben Mares
OK, widzę, że nie ma „modułu ustawiającego właściwości modułu” ani „modułu __getattribute__”. Dziękuję Ci.
John Lin
5

Typowym przypadkiem użycia jest: wzbogacenie (ogromnego) istniejącego modułu kilkoma (kilkoma) atrybutami dynamicznymi - bez przekształcania wszystkich elementów modułu w układ klasy. Niestety najprostsza łatka na klasę modułu, jak na przykład, sys.modules[__name__].__class__ = MyPropertyModulenie działa TypeError: __class__ assignment: only for heap types. Dlatego tworzenie modułów wymaga ponownego okablowania.

To podejście robi to bez haków importu Pythona, po prostu mając prolog na górze kodu modułu:

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

Stosowanie:

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

Uwaga: coś podobnego from propertymodule import dynvalspowoduje oczywiście zamrożoną kopię - odpowiadającądynval = someobject.dynval

kxr
źródło
1

Krótka odpowiedź: użyj proxy_tools

proxy_toolsPakiet stara się zapewnić @module_propertyfunkcjonalność.

Instaluje się z

pip install proxy_tools

Używając niewielkiej modyfikacji przykładu @ Marein, the_module.pyumieściliśmy

from proxy_tools import module_property

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

Teraz z innego skryptu mogę zrobić

import the_module

print(the_module.thing)
# . hello

Nieoczekiwane zachowanie

To rozwiązanie nie jest pozbawione zastrzeżeń. Mianowicie niethe_module.thing jest to ciąg ! Jest to proxy_tools.Proxyobiekt, którego specjalne metody zostały nadpisane, tak że naśladuje łańcuch. Oto kilka podstawowych testów, które ilustrują ten punkt:

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']

Wewnętrznie oryginalna funkcja jest przechowywana w the_module.thing._Proxy__local:

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

Dalsze przemyślenia

Szczerze mówiąc, zastanawiam się, dlaczego moduły nie mają wbudowanej tej funkcji. Myślę, że sedno sprawy polega na tym, że the_modulejest to instancja types.ModuleTypeklasy. Ustawienie „właściwości modułu” jest równoznaczne z ustawieniem właściwości instancji tej klasy, a nie types.ModuleTypesamej klasy. Aby uzyskać więcej informacji, zobacz tę odpowiedź .

Możemy faktycznie zaimplementować właściwości w types.ModuleTypenastępujący sposób, chociaż wyniki nie są świetne. Nie możemy bezpośrednio modyfikować typów wbudowanych, ale możemy je przeklinać :

# 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__}'))

Daje nam to właściwość, która istnieje we wszystkich modułach. Jest to trochę nieporęczne, ponieważ przerywamy zachowanie ustawień we wszystkich modułach:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
źródło
1
Jak to jest lepsze niż uczynienie modułu instancją prawdziwej klasy, jak pokazano w odpowiedzi @Alex Martelli?
martineau
1
Powiedziałeś jeszcze coś, co nie ma dla mnie sensu. Weźmy ten biznes na temat posiadania @module_propertydekoratora. Ogólnie rzecz biorąc, wbudowany @propertydekorator jest używany podczas definiowania klasy, a nie po utworzeniu jej instancji, więc zakładam, że to samo dotyczy właściwości modułu i tak jest z odpowiedzią Alexa - przypomnij sobie to pytanie „Czy moduły mogą mieć takie same właściwości, jak obiekty?”. Jednak możliwe jest ich późniejsze dodanie i zmodyfikowałem mój wcześniejszy fragment, aby zilustrować sposób, w jaki można to zrobić.
martineau
1
Ben: Po przejrzeniu kodu w twoim konkretnym przykładzie, myślę, że rozumiem, do czego teraz zmierzasz. Myślę też, że niedawno natknąłem się na technikę implementacji czegoś podobnego do właściwości modułu, która nie wymaga zastąpienia modułu instancją klasy, jak w odpowiedzi Alexa, chociaż w tym momencie nie jestem pewien, czy można to zrobić za pośrednictwem dekoratora - skontaktuję się z Tobą, jeśli zrobię jakiekolwiek postępy.
martineau
1
OK, oto link do odpowiedzi na inne pytanie, które zawiera podstawową ideę.
martineau
1
Cóż, przynajmniej w przypadku a cached_module_property, fakt, że __getattr__()nie będzie już wywoływany, jeśli atrybut zostanie zdefiniowany, jest pomocny. (podobnie do tego, co functools.cached_propertyosiąga).
martineau