I moduli possono avere proprietà allo stesso modo degli oggetti?


98

Con le proprietà Python, posso renderlo tale

obj.y 

chiama una funzione piuttosto che restituire solo un valore.

C'è un modo per farlo con i moduli? Ho un caso dove voglio

module.y 

per chiamare una funzione, piuttosto che restituire semplicemente il valore memorizzato lì.


2
Vedi __getattr__su un modulo per una soluzione più moderna.
wim

Risposte:


56

Solo le istanze di classi di nuovo stile possono avere proprietà. Puoi far credere a Python che un'istanza del genere sia un modulo nascondendola in sys.modules[thename] = theinstance. Quindi, ad esempio, il tuo file del modulo m.py potrebbe essere:

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

2
Qualcun altro l'ha provato? Quando metto questo codice in un file x.py e lo importi da un altro, la chiamata a xy risulta in AttributeError: l'oggetto 'NoneType' non ha attributo 'c', poiché _M in qualche modo ha valore None ...
Stephan202

3
In effetti il ​​codice funziona sull'interprete. Ma quando lo metto in un file (ad esempio, bowwow.py) e lo importi da un altro file (otherfile.py), allora non funziona più ...
Stephan202

4
D: Ci sarebbero dei vantaggi particolari nel derivare la classe dell'istanza da types.ModuleTypecome mostrato nella risposta altrimenti molto simile di @ Unknown?
martineau

11
Solo le istanze di classi di nuovo stile possono avere proprietà. Questo non è il motivo: i moduli sono istanze di classi di nuovo stile, in quanto sono istanze di builtins.module, che a sua volta è un'istanza di type(che è la definizione di classe di nuovo stile). Il problema è che le proprietà devono essere sulla classe, non sull'istanza: se lo fai f = Foo(), f.some_property = property(...)fallirà nello stesso modo come se lo mettessi ingenuamente in un modulo. La soluzione è metterlo nella classe, ma poiché non vuoi che tutti i moduli abbiano la proprietà, la sottoclasse (vedi la risposta di Unknown).
Thanatos

3
@ Joe, l'alterazione di globals()(mantenendo le chiavi intatte ma reimpostando i valori su None) al momento del rebinding del nome in sys.modulesè un problema di Python 2 - Python 3.4 funziona come previsto. Se hai bisogno di accedere all'oggetto classe in Py2, aggiungi ad es. Subito _M._cls = _Mdopo l' classistruzione (o riponilo in modo equivalente in qualche altro spazio dei nomi) e accedilo come self._clsnei metodi che lo richiedono ( type(self)potrebbe essere OK ma non se esegui anche sottoclassi di _M) .
Alex Martelli

54

Lo farei per ereditare correttamente tutti gli attributi di un modulo ed essere identificato correttamente da isinstance ()

import types

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


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

E poi puoi inserirlo in sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class

Questo sembra funzionare solo per i casi più semplici. I possibili problemi sono: (1) alcuni helper di importazione possono anche aspettarsi altri attributi come quelli __file__che devono essere definiti manualmente, (2) le importazioni fatte nel modulo contenente la classe non saranno "visibili" durante il runtime, ecc ...
tutuDajuju

1
Non è necessario derivare una sottoclasse da types.ModuleType, qualsiasi classe (nuovo stile) andrà bene. Quali attributi speciali del modulo speri di ereditare?
martineau

Cosa succede se il modulo originale è un pacchetto e desidero accedere ai moduli sotto il modulo originale?
kawing-chiu

2
@martineau Avrai un modulo repr, puoi specificare il nome del modulo quando __init__un'istanza e otterrai un comportamento corretto quando lo usi isinstance.
wim

@wim: Punti presi, anche se francamente nessuno sembra essere così importante IMO.
martineau

34

Poiché PEP 562 è stato implementato in Python> = 3.7, ora possiamo farlo

file: module.py

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

other = 4

utilizzo:

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

Nota che se ometti il raise AttributeErrornella __getattr__funzione, significa che la funzione finisce con return None, quindi module.nosuchotterrà un valore di None.


2
Sulla base di questo, ho aggiunto un'altra risposta: stackoverflow.com/a/58526852/2124834

3
Questa è solo la metà di una proprietà. Nessun setter.
wim

Sfortunatamente non sembra difficile rendere gli strumenti consapevoli di tali attributi (?) ( Getattr viene richiamato solo se non viene trovato alcun membro regolare)
olejorgenb

9

Basato sulla risposta di John Lin :

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

Utilizzo (notare il trattino basso iniziale), in the_module.py:

@module_property
def _thing():
    return 'hello'

Poi:

import the_module

print(the_module.thing)  # prints 'hello'

Il carattere di sottolineatura iniziale è necessario per differenziare la funzione con proprietà dalla funzione originale. Non riuscivo a pensare a un modo per riassegnare l'identificativo, poiché durante l'esecuzione del decoratore non è stato ancora assegnato.

Nota che gli IDE non sapranno che la proprietà esiste e mostrerà ondulazioni rosse.


Grande! Rispetto alla proprietà di classe @property def x(self): return self._xpenso che def thing()senza sottolineatura sia più convenzionale. E puoi anche creare un decoratore "module property setter" nella tua risposta?
John Lin

2
@JohnLin, ho tentato di attuare il tuo def thing()suggerimento. Il problema è che __getattr__viene chiamato solo per attributi mancanti . Ma dopo @module_property def thing(): …run, the_module.thingè definito, quindi getattr non verrà mai chiamato. Dobbiamo in qualche modo registrarci thingnel decoratore e quindi cancellarlo dallo spazio dei nomi del modulo. Ho provato a tornare Nonedal decoratore, ma poi thingè definito come None. Si potrebbe fare, @module_property def thing(): … del thingma trovo che sia peggio dell'uso thing()come funzione
Ben Mares

OK, vedo che non esiste un "setter delle proprietà del modulo", né un "modulo __getattribute__". Grazie.
John Lin,

5

Un tipico caso d'uso è: arricchire un (enorme) modulo esistente con alcuni (pochi) attributi dinamici - senza trasformare tutto il materiale del modulo in un layout di classe. Sfortunatamente una patch di classe modulo più semplice come sys.modules[__name__].__class__ = MyPropertyModulefallisce con TypeError: __class__ assignment: only for heap types. Quindi la creazione del modulo deve essere ricablata.

Questo approccio lo fa senza gli hook di importazione di Python, semplicemente avendo un prologo sopra il codice del modulo:

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

Utilizzo:

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

Nota: qualcosa di simile from propertymodule import dynvalprodurrà ovviamente una copia congelata, corrispondente adynval = someobject.dynval


1

Una risposta breve: usa proxy_tools

Il proxy_toolspacchetto tenta di fornire @module_propertyfunzionalità.

Si installa con

pip install proxy_tools

Usando una leggera modifica dell'esempio di @ Marein, the_module.pyabbiamo inserito

from proxy_tools import module_property

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

Ora da un altro script, posso farlo

import the_module

print(the_module.thing)
# . hello

Comportamento inaspettato

Questa soluzione non è priva di avvertimenti. Vale a dire, nonthe_module.thing è una stringa ! È un proxy_tools.Proxyoggetto i cui metodi speciali sono stati sovrascritti in modo che imiti una stringa. Ecco alcuni test di base che illustrano il punto:

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

Internamente, la funzione originale viene memorizzata in the_module.thing._Proxy__local:

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

Ulteriori pensieri

Onestamente, sono sconcertato dal motivo per cui i moduli non hanno questa funzionalità incorporata. Penso che il nocciolo della questione sia che the_moduleè un'istanza della types.ModuleTypeclasse. L'impostazione di una "proprietà del modulo" equivale a impostare una proprietà su un'istanza di questa classe, piuttosto che sulla types.ModuleTypeclasse stessa. Per maggiori dettagli, vedi questa risposta .

Possiamo effettivamente implementare le proprietà types.ModuleTypecome segue, sebbene i risultati non siano eccezionali. Non possiamo modificare direttamente i tipi incorporati, ma possiamo maledirli :

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

Questo ci dà una proprietà che esiste su tutti i moduli. È un po 'ingombrante, poiché interrompiamo il comportamento delle impostazioni in tutti i moduli:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute

1
Come è meglio che rendere il modulo un'istanza di una classe reale, come mostrato nella risposta di @Alex Martelli?
martineau

1
Hai detto qualcos'altro che non ha senso per me. Prendi questa faccenda di avere un @module_propertydecoratore. In generale, il @propertydecoratore integrato viene utilizzato quando una classe è definita, non dopo che è stata creata un'istanza di essa, quindi presumo che lo stesso sarebbe vero per una proprietà del modulo ed è con la risposta di Alex - ricorda che questa domanda chiede "I moduli possono avere proprietà allo stesso modo degli oggetti?". Tuttavia è possibile aggiungerli in seguito e ho modificato il mio frammento precedente per illustrare un modo che può essere fatto.
martineau

1
Ben: Dopo aver esaminato il codice nel tuo esempio concreto, penso di aver capito cosa stai ottenendo ora. Penso anche di essermi imbattuto di recente in una tecnica per implementare qualcosa di simile alle proprietà del modulo che non richiede la sostituzione del modulo con un'istanza di classe come nella risposta di Alex, anche se a questo punto non sono sicuro che ci sia un modo per farlo tramite un decoratore - ti ricontatterò se farò progressi.
martineau

1
OK, ecco un collegamento a una risposta a un'altra domanda che contiene l'idea principale.
martineau

1
Bene, almeno nel caso di a cached_module_property, il fatto che __getattr__()non verrà più chiamato se l'attributo viene definito è utile. (simile a ciò che functools.cached_propertyrealizza).
martineau
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.