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

98

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 faire cela 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.

Josh Gibson
la source
2
Voir __getattr__sur un module pour une solution plus moderne.
wim

Réponses:

56

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 le cachant 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()
Alex Martelli
la source
2
Quelqu'un d'autre a-t-il essayé cela? Lorsque je mets ce code dans un fichier x.py et que je l'importe d'un autre, l'appel de xy aboutit à AttributeError: l'objet 'NoneType' n'a pas d'attribut 'c', puisque _M a en quelque sorte la valeur None ...
Stephan202
3
En effet le code fonctionne sur l'interpréteur. Mais quand je le mets dans un fichier (disons, bowwow.py) et que je l'importe à partir d'un autre fichier (otherfile.py), alors ça ne fonctionne plus ...
Stephan202
4
Q: Y aurait-il un (des) avantage (s) particulier (s) à dériver la classe de l'instance types.ModuleTypecomme indiqué dans la réponse par ailleurs très similaire de @ Unknown?
martineau
11
Seules les instances de classes de nouveau style peuvent avoir des propriétés. Ce n'est pas la raison: les modules sont des instances de classes de nouveau style, en ce sens qu'ils sont des instances de builtins.module, qui lui-même est une instance de type(qui est la définition de la classe de nouveau style). Le problème est que les propriétés doivent être de la classe, et non pas l'instance: si vous le faites f = Foo(), f.some_property = property(...)il échouera de la même manière que si vous le mettre dans un module ingénument. La solution est de le mettre dans la classe, mais comme vous ne voulez pas que tous les modules aient la propriété, vous sous-classez (voir la réponse de Unknown).
Thanatos le
3
@Joe, la modification de globals()(en gardant les clés intactes mais en réinitialisant les valeurs à None) lors de la réassociation du nom dans sys.modulesest un problème Python 2 - Python 3.4 fonctionne comme prévu. Si vous avez besoin d'accéder à l'objet de classe dans Py2, ajoutez par exemple _M._cls = _Mjuste après l' classinstruction (ou placez-le de manière équivalente dans un autre espace de noms) et accédez-y comme self._clsdans les méthodes qui le nécessitent (cela type(self)peut être OK mais pas si vous faites également un sous-classement de _M) .
Alex Martelli
54

Je ferais cela pour hériter correctement de tous les attributs d'un module, et ê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
Inconnue
la source
Cela semble ne fonctionner que pour les cas les plus simples. Les problèmes possibles sont: (1) certains assistants d'importation peuvent également s'attendre à d'autres attributs comme ceux __file__qui doivent être définis manuellement, (2) les importations effectuées dans le module contenant la classe ne seront pas "visibles" pendant l'exécution, etc ...
tutuDajuju
1
Il n'est pas nécessaire de dériver une sous-classe à partir de types.ModuleType, n'importe quelle classe (de style nouveau) fera l'affaire. Quels sont exactement les attributs spéciaux du module dont vous espérez hériter?
martineau
Que faire si le module d'origine est un package et que je souhaite accéder aux modules sous le module d'origine?
kawing-chiu
2
@martineau Vous aurez un module repr, vous pouvez spécifier le nom du module lors d' __init__une instance, et vous obtiendrez un comportement correct lors de l'utilisation isinstance.
wim
@wim: Points pris, bien que franchement aucun ne semble être si important pour l'OMI.
martineau le
34

Comme PEP 562 a été implémenté en 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

usage:

>>> 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 AttributeErrordans la __getattr__fonction, cela signifie que la fonction se termine par return None, alors le module.nosuchobtiendra une valeur de None.

John Lin
la source
2
Sur cette base, j'ai ajouté une autre réponse: stackoverflow.com/a/58526852/2124834
3
Ce n'est que la moitié d'une propriété. Pas de passeur.
wim le
Malheureusement, il ne semble pas difficile de rendre l'outillage conscient de ces attributs (?) ( Getattr n'est invoqué que si aucun membre régulier n'est trouvé)
olejorgenb
9

Basé sur la 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 trait de soulignement principal), 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 début 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.


la source
Génial! Par rapport à la propriété de classe, @property def x(self): return self._xje pense que def thing()sans soulignement, c'est plus conventionnel. Et pouvez-vous également créer un décorateur "module property setter" dans votre réponse?
John Lin
2
@JohnLin, j'ai tenté de mettre en œuvre votre def thing()suggestion. Le problème est qu'il __getattr__n'est appelé que pour les attributs manquants . Mais après l' @module_property def thing(): …exécution, the_module.thingest défini, donc getattr ne sera jamais appelé. Nous devons en quelque sorte nous enregistrer thingdans le décorateur, puis le supprimer de l'espace de noms du module. J'ai essayé de revenir Nonedu décorateur, mais thingest défini comme None. On pourrait faire @module_property def thing(): … del thingmais je trouve cela pire que d'utiliser thing()comme fonction
Ben Mares
OK, je vois qu'il n'y a pas de "module property setter", ni de "module __getattribute__". Je vous remercie.
John Lin le
5

Un cas d'utilisation typique est: enrichir 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 avecTypeError: __class__ assignment: only for heap types . La création du module doit donc être recâblée.

Cette approche le fait sans les hooks d'importation Python, simplement 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 dynvalproduira une copie figée bien sûr - correspondant àdynval = someobject.dynval

kxr
la source
1

Une réponse courte: utiliser proxy_tools

Le proxy_toolspackage tente de fournir@module_property fonctionnalités.

Il s'installe avec

pip install proxy_tools

En utilisant une légère modification de l'exemple de @ Marein, the_module.pynous 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. A savoir, the_module.thingn'est pas une chaîne ! C'est un proxy_tools.Proxyobjet dont les méthodes spéciales ont été surchargées pour imiter une chaîne. Voici quelques tests de base qui illustrent ce 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 sais pas pourquoi les modules n'ont pas cette fonctionnalité intégrée. Je pense que le nœud du problème est qu'il the_modules'agit d'une instance de la types.ModuleTypeclasse. 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.ModuleTypeclasse elle-même. Pour plus de détails, consultez cette réponse .

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

# 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 difficile à manier, car nous cassons le comportement des paramètres dans tous les modules:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
la source
1
En quoi est-ce mieux que de simplement faire du module une instance d'une classe réelle comme le montre la réponse de @Alex Martelli?
martineau le
1
Vous avez dit quelque chose d'autre qui n'a pas de sens pour moi. Prenez cette entreprise pour avoir un @module_propertydécorateur. De manière générale, le @propertydécorateur intégré est utilisé lorsqu'une classe est définie, pas après qu'une instance de celle-ci a été créée, donc je suppose que la même chose serait vraie pour une propriété de module et c'est avec la réponse d'Alex - rappelez-vous que cette question pose "Les modules peuvent-ils avoir des propriétés de la même manière que les objets?". Cependant, il est possible de les ajouter par la suite et j'ai modifié mon extrait de code précédent pour illustrer une façon de le faire.
martineau le
1
Ben: Après avoir regardé le code dans votre exemple concret, je pense que je comprends ce que vous voulez en venir. Je pense aussi que je suis récemment tombé sur une technique pour implémenter quelque chose qui ressemble aux propriétés du module qui ne nécessite pas le remplacement du module par une instance de classe comme dans la réponse d'Alex, même si je ne suis pas sûr à ce stade s'il existe un moyen de faire via un décorateur - je vous répondrai si je fais des progrès.
martineau le
1
OK, voici un lien vers une réponse à une autre question qui contient l'idée principale.
martineau le
1
Eh bien, au moins dans le cas de a cached_module_property, le fait qu'il __getattr__()ne sera plus appelé si l'attribut est défini est utile. (similaire à ce qui functools.cached_propertyaccomplit).
martineau le