¿Pueden los módulos tener propiedades de la misma manera que los objetos?

98

Con las propiedades de Python, puedo hacer que

obj.y 

llama a una función en lugar de simplemente devolver un valor.

¿Hay alguna forma de hacer esto con módulos? Tengo un caso donde quiero

module.y 

para llamar a una función, en lugar de simplemente devolver el valor almacenado allí.

Josh Gibson
fuente
2
Consulte __getattr__un módulo para obtener una solución más moderna.
wim

Respuestas:

56

Solo las instancias de clases de estilo nuevo pueden tener propiedades. Puede hacer que Python crea que una instancia de este tipo es un módulo guardándolo sys.modules[thename] = theinstance. Entonces, por ejemplo, su archivo de módulo m.py podría ser:

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
fuente
2
¿Alguien más intentó esto? Cuando pongo este código en un archivo x.py y lo importo desde otro, luego llamo a xy a AttributeError: el objeto 'NoneType' no tiene atributo 'c', ya que _M de alguna manera tiene valor None ...
Stephan202
3
De hecho, el código funciona con el intérprete. Pero cuando lo pongo en un archivo (digamos, bowwow.py) y lo importo desde otro archivo (otherfile.py), entonces ya no funciona ...
Stephan202
4
P: ¿Habría alguna ventaja en particular al derivar la clase de la instancia de types.ModuleTypecomo se muestra en la respuesta muy similar de @ Unknown?
martineau
11
Solo las instancias de clases de estilo nuevo pueden tener propiedades. Esta no es la razón: los módulos son instancias de clases de estilo nuevo, en el sentido de que son instancias de builtins.module, que en sí es una instancia de type(que es la definición de clase de estilo nuevo). El problema es que las propiedades deben estar en la clase, no en la instancia: si lo haces f = Foo(), f.some_property = property(...)fallará de la misma manera que si lo pusieras ingenuamente en un módulo. La solución es ponerlo en la clase, pero como no quiere que todos los módulos tengan la propiedad, su subclase (vea la respuesta de Desconocido).
Thanatos
3
@Joe, la alteración de globals()(mantener las claves intactas pero restablecer los valores a None) al volver a enlazar el nombre sys.moduleses un problema de Python 2: Python 3.4 funciona según lo previsto. Si necesita acceder al objeto de clase en Py2, agregue, por ejemplo, _M._cls = _Mjusto después de la classdeclaración (o guárdelo de manera equivalente en algún otro espacio de nombres) y acceda a él como self._clsen los métodos que lo requieran ( type(self)podría estar bien, pero no si también realiza una subclase de _M) .
Alex Martelli
54

Haría esto para heredar correctamente todos los atributos de un módulo y ser identificado correctamente por isinstance ()

import types

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


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

Y luego puede insertar esto en sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class
Desconocido
fuente
Esto parece funcionar solo para los casos más simples. Los posibles problemas son: (1) algunos ayudantes de importación también pueden esperar otros atributos como los __file__que deben definirse manualmente, (2) las importaciones realizadas en el módulo que contiene la clase no serán "visibles" durante el tiempo de ejecución, etc ...
tutuDajuju
1
No es necesario derivar una subclase de types.ModuleType, cualquier clase (estilo nuevo) servirá. ¿Exactamente qué atributos de módulo especiales esperas heredar?
Martineau
¿Qué pasa si el módulo original es un paquete y quiero acceder a los módulos debajo del módulo original?
kawing-chiu
2
@martineau Tendrá un módulo repr, puede especificar el nombre del módulo cuando __init__una instancia, y obtendrá el comportamiento correcto al usar isinstance.
wim
@wim: Puntos tomados, aunque francamente ninguno parece ser tan importante en mi opinión.
martineau
34

Como PEP 562 se ha implementado en Python> = 3.7, ahora podemos hacer esto

archivo: module.py

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

other = 4

uso:

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

Tenga en cuenta que si omite raise AttributeErroren la __getattr__función, significa que la función termina con return None, entonces module.nosuchobtendrá un valor de None.

John Lin
fuente
2
En base a esto, agregué otra respuesta: stackoverflow.com/a/58526852/2124834
3
Esto es solo la mitad de una propiedad. Sin setter.
wim
Desafortunadamente, no parece difícil hacer que las herramientas sean conscientes de tales atributos (?) ( Getattr solo se invoca si no se encuentra un miembro regular)
olejorgenb
9

Basado en la respuesta de 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

Uso (observe el subrayado inicial), en the_module.py:

@module_property
def _thing():
    return 'hello'

Luego:

import the_module

print(the_module.thing)  # prints 'hello'

El guión bajo inicial es necesario para diferenciar la función con propiedad de la función original. No pude pensar en una forma de reasignar el identificador, ya que durante el tiempo de ejecución del decorador, aún no se ha asignado.

Tenga en cuenta que los IDE no sabrán que la propiedad existe y mostrarán ondas rojas.


fuente
¡Excelente! En comparación con la propiedad de clase, @property def x(self): return self._xcreo que def thing()sin subrayado es más convencional. ¿Y puede crear un decorador de "establecedor de propiedades de módulo" en su respuesta también?
John Lin
2
@JohnLin, intenté implementar tu def thing()sugerencia. El problema es que __getattr__solo se llama por atributos faltantes . Pero después de @module_property def thing(): …ejecutar, the_module.thingse define, por lo que nunca se llamará a getattr . Necesitamos registrarnos de alguna manera thingen el decorador y luego eliminarlo del espacio de nombres del módulo. Intenté regresar Nonedel decorador, pero luego thingse define como None. Se podría hacer, @module_property def thing(): … del thingpero me parece peor que usarlo thing()como una función
Ben Mares
De acuerdo, veo que no hay "establecedor de propiedades del módulo", ni "módulo __getattribute__". Gracias.
John Lin
5

Un caso de uso típico es: enriquecer un módulo (enorme) existente con algunos (pocos) atributos dinámicos, sin convertir todo el material del módulo en un diseño de clase. Desafortunadamente, un parche de clase de módulo más simple como sys.modules[__name__].__class__ = MyPropertyModulefalla TypeError: __class__ assignment: only for heap types. Por tanto, es necesario volver a cablear la creación de módulos.

Este enfoque lo hace sin los ganchos de importación de Python, solo con tener un prólogo encima del código del módulo:

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

Uso:

>>> 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: Algo como from propertymodule import dynvalproducirá una copia congelada, por supuesto, correspondiente adynval = someobject.dynval

kxr
fuente
1

Una respuesta corta: usa proxy_tools

El proxy_toolspaquete intenta proporcionar @module_propertyfuncionalidad.

Se instala con

pip install proxy_tools

Utilizando una ligera modificación del ejemplo de @ Marein, the_module.pyponemos

from proxy_tools import module_property

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

Ahora de otro guión, puedo hacer

import the_module

print(the_module.thing)
# . hello

Comportamiento inesperado

Esta solución no está exenta de salvedades. Es decir, ¡ nothe_module.thing es una cadena ! Es un proxy_tools.Proxyobjeto cuyos métodos especiales se han anulado para que imite una cadena. Aquí hay algunas pruebas básicas que ilustran el 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 función original se almacena en the_module.thing._Proxy__local:

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

Pensamientos adicionales

Honestamente, estoy desconcertado acerca de por qué los módulos no tienen esta funcionalidad incorporada. Creo que el quid del asunto es que the_modulees una instancia de la types.ModuleTypeclase. Establecer una "propiedad de módulo" equivale a establecer una propiedad en una instancia de esta clase, en lugar de en la types.ModuleTypeclase misma. Para obtener más detalles, consulte esta respuesta .

De hecho, podemos implementar propiedades de la types.ModuleTypesiguiente manera, aunque los resultados no son excelentes. No podemos modificar directamente los tipos integrados, pero podemos maldecirlos :

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

Esto nos da una propiedad que existe en todos los módulos. Es un poco difícil de manejar, ya que rompemos el comportamiento de configuración en todos los módulos:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
fuente
1
¿Cómo es esto mejor que simplemente hacer del módulo una instancia de una clase real como se muestra en la respuesta de @Alex Martelli?
martineau
1
Has dicho algo más que no tiene sentido para mí. Tome este asunto de tener un @module_propertydecorador. En términos generales, el @propertydecorador incorporado se usa cuando se define una clase, no después de que se haya creado una instancia de ella, por lo que supongo que lo mismo sería cierto para una propiedad de módulo y es con la respuesta de Alex: recuerde que esta pregunta pide "¿Pueden los módulos tener propiedades de la misma manera que los objetos?". Sin embargo, es posible agregarlos después y modifiqué mi fragmento anterior para ilustrar una forma en que se puede hacer.
Martineau
1
Ben: Después de revisar el código en tu ejemplo concreto, creo que entiendo a qué te refieres ahora. También creo que recientemente me topé con una técnica para implementar algo similar a las propiedades del módulo que no requiere que el módulo sea reemplazado con una instancia de clase como en la respuesta de Alex, aunque no estoy seguro en este momento si hay una forma de hacerlo a través de un decorador - se pondrá en contacto con usted si hago algún progreso.
Martineau
1
Bien, aquí hay un enlace a la respuesta a otra pregunta que contiene la idea central.
Martineau
1
Bueno, al menos en el caso de a cached_module_property, el hecho de que __getattr__()ya no se llamará si el atributo se define es útil. (similar a lo que functools.cached_propertylogra).
martineau