web-dev-qa-db-fra.com

Les modules peuvent-ils avoir des propriétés de la même manière que les objets?

Avec les propriétés python, je peux faire en sorte que

obj.y 

appelle une fonction plutôt que de simplement renvoyer une valeur.

Existe-t-il un moyen de le faire avec des modules? J'ai un cas où je veux

module.y 

pour appeler une fonction, plutôt que de simplement renvoyer la valeur qui y est stockée.

93
Josh Gibson

Seules les instances de classes de nouveau style peuvent avoir des propriétés. Vous pouvez faire croire à Python qu'une telle instance est un module en la stockant dans sys.modules[thename] = theinstance. Ainsi, par exemple, votre fichier de module m.py pourrait être:

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()
55
Alex Martelli

Je le ferais afin d'hériter correctement de tous les attributs d'un module et d'être correctement identifié par isinstance ()

import types

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


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

Et puis vous pouvez insérer ceci dans sys.modules:

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

Comme PEP 562 a été implémenté dans Python> = 3.7, nous pouvons maintenant le faire

fichier: module.py

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

other = 4

utilisation:

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

Notez que si vous omettez le raise AttributeError dans le __getattr__ fonction, cela signifie que la fonction se termine par return None, puis le module.nosuch obtiendra une valeur de None.

20
John Lin

Un cas d'utilisation typique est: l'enrichissement d'un (énorme) module existant avec quelques (quelques) attributs dynamiques - sans transformer tous les éléments du module en une disposition de classe. Malheureusement, un correctif de classe de module le plus simple comme sys.modules[__name__].__class__ = MyPropertyModule échoue avec TypeError: __class__ assignment: only for heap types. La création de modules doit donc être recâblée.

Cette approche le fait sans Python hooks d'importation, juste en ayant un prologue au-dessus du code du module:

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

Usage:

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

Remarque: quelque chose comme from propertymodule import dynval produira bien sûr une copie figée - correspondant à dynval = someobject.dynval

5
kxr

Basé sur réponse 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

Utilisation (notez le tiret bas), dans the_module.py:

@module_property
def _thing():
    return 'hello'

Ensuite:

import the_module

print(the_module.thing)  # prints 'hello'

Le trait de soulignement de tête est nécessaire pour différencier la fonction de propriété de la fonction d'origine. Je ne pouvais pas penser à un moyen de réaffecter l'identifiant, car pendant le temps de l'exécution du décorateur, il n'a pas encore été attribué.

Notez que les IDE ne sauront pas que la propriété existe et afficheront des ondulations rouges.

3
Marein

Une réponse courte: utilisez proxy_tools

Le proxy_tools package tente de fournir @module_property Fonctionnalité.

Il s'installe avec

pip install proxy_tools

En utilisant une légère modification de l'exemple de @ Marein, dans the_module.py nous mettons

from proxy_tools import module_property

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

Maintenant, à partir d'un autre script, je peux faire

import the_module

print(the_module.thing)
# . hello

Comportement inattendu

Cette solution n'est pas sans réserves. À savoir, the_module.thing n'est pas une chaîne ! C'est un proxy_tools.Proxy objet dont les méthodes spéciales ont été remplacées de façon à imiter une chaîne. Voici quelques tests de base qui illustrent le point:

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

En interne, la fonction d'origine est stockée dans the_module.thing._Proxy__local:

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

Réflexions supplémentaires

Honnêtement, je ne comprends pas pourquoi les modules n'ont pas cette fonctionnalité intégrée. Je pense que le nœud du problème est que the_module est une instance de types.ModuleType classe. Définir une "propriété de module" revient à définir une propriété sur une instance de cette classe, plutôt que sur la types.ModuleType classe elle-même. Pour plus de détails, voir cette réponse .

Nous pouvons réellement implémenter des propriétés sur types.ModuleType comme suit, bien que les résultats ne soient pas excellents. Nous ne pouvons pas modifier directement les types intégrés, mais nous pouvons maudire eux:

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

Cela nous donne une propriété qui existe sur tous les modules. C'est un peu compliqué, car nous cassons le comportement de réglage sur tous les modules:

import sys

print(sys.thing2)
# hi from sys

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