__getattr__ sur un module

127

Comment implémenter l'équivalent d'un __getattr__sur une classe, sur un module?

Exemple

Lors de l'appel d'une fonction qui n'existe pas dans les attributs définis statiquement d'un module, je souhaite créer une instance d'une classe dans ce module et invoquer la méthode dessus avec le même nom que celui qui a échoué dans la recherche d'attributs sur le module.

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

Qui donne:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined
Matt Joiner
la source
2
J'irai probablement avec la réponse de Grieve, car cela fonctionne dans toutes les circonstances (même si c'est un peu compliqué et pourrait être mieux fait). Harvard S et S Lott ont de belles réponses claires mais ce ne sont pas des solutions pratiques.
Matt Joiner
1
Vous n'êtes même pas dans votre cas en train de créer un accès aux attributs, vous demandez donc deux choses différentes à la fois. La question principale est donc celle que vous voulez. Voulez-vous salutationexister dans l'espace de noms global ou local (ce que le code ci-dessus essaie de faire) ou voulez-vous une recherche dynamique de noms lorsque vous effectuez un accès par point sur un module? C'est deux choses différentes.
Lennart Regebro
Question intéressante, comment avez-vous trouvé cela?
Chris Wesseling
1
duplication possible d' Autoload en Python
Piotr Dobrogost
1
__getattr__sur les modules est pris en charge à partir de Python 3.7
Fumito Hamamura

Réponses:

42

Il y a quelque temps, Guido a déclaré que toutes les recherches de méthodes spéciales sur les classes de nouveau style contournaient __getattr__et__getattribute__ . Les méthodes Dunder avaient précédemment travaillé sur des modules - vous pouviez, par exemple, utiliser un module comme gestionnaire de contexte simplement en définissant __enter__et __exit__, avant que ces astuces ne se cassent .

Récemment, certaines fonctionnalités historiques ont fait leur retour, le module __getattr__parmi eux, et donc le hack existant (un module se remplaçant par une classe sys.modulesau moment de l'importation) ne devrait plus être nécessaire.

Dans Python 3.7+, vous utilisez simplement la seule manière évidente. Pour personnaliser l'accès aux attributs sur un module, définissez une __getattr__fonction au niveau du module qui doit accepter un argument (nom de l'attribut), et renvoyer la valeur calculée ou lever un AttributeError:

# my_module.py

def __getattr__(name: str) -> Any:
    ...

Cela permettra également de se connecter aux importations "from", c'est-à-dire que vous pouvez renvoyer des objets générés dynamiquement pour des instructions telles que from my_module import whatever.

Sur une note connexe, avec le module getattr, vous pouvez également définir une __dir__fonction au niveau du module à laquelle répondre dir(my_module). Voir PEP 562 pour plus de détails.

wim
la source
1
Si je crée dynamiquement un module via m = ModuleType("mod")et définissez m.__getattr__ = lambda attr: return "foo"; cependant, quand je cours from mod import foo, je reçois TypeError: 'module' object is not iterable.
weberc2 le
@ weberc2: Faites cela m.__getattr__ = lambda attr: "foo", plus vous devez définir une entrée pour le module avec sys.modules['mod'] = m. Ensuite, il n'y a pas d'erreur avec from mod import foo.
martineau le
wim: Vous pouvez également obtenir des valeurs calculées dynamiquement - comme avoir une propriété au niveau du module - permettant d'écrire my_module.whateverpour l'invoquer (après un import my_module).
martineau le
115

Il y a deux problèmes de base que vous rencontrez ici:

  1. __xxx__ les méthodes ne sont recherchées que sur la classe
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) signifie que toute solution devrait également garder une trace du module en cours d'examen, sinon chaque module aurait alors le comportement de substitution d'instance; et (2) signifie que (1) n'est même pas possible ... du moins pas directement.

Heureusement, sys.modules n'est pas pointilleux sur ce qui s'y passe, donc un wrapper fonctionnera, mais uniquement pour l'accès au module (c'est-à-dire import somemodule; somemodule.salutation('world'); pour l'accès au même module, vous devez à peu près extraire les méthodes de la classe de substitution et les ajouter à globals()eiher avec un méthode personnalisée sur la classe (j'aime utiliser .export()) ou avec une fonction générique (comme celles déjà répertoriées comme réponses). Une chose à garder à l'esprit: si le wrapper crée une nouvelle instance à chaque fois, et que la solution globale ne l'est pas, vous vous retrouvez avec un comportement subtilement différent Oh, et vous ne pouvez pas utiliser les deux en même temps - c'est l'un ou l'autre.


Mettre à jour

De Guido van Rossum :

Il y a en fait un hack qui est parfois utilisé et recommandé: un module peut définir une classe avec la fonctionnalité souhaitée, puis à la fin, se remplacer dans sys.modules par une instance de cette classe (ou par la classe, si vous insistez , mais c'est généralement moins utile). Par exemple:

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

Cela fonctionne parce que la machine d'importation active activement ce hack et que sa dernière étape extrait le module réel de sys.modules, après l'avoir chargé. (Ce n'est pas un hasard. Le hack a été proposé il y a longtemps et nous avons décidé que nous aimions suffisamment pour le supporter dans la machinerie d'importation.)

Donc, la façon établie d'accomplir ce que vous voulez est de créer une seule classe dans votre module, et comme dernier acte du module, remplacez-la sys.modules[__name__]par une instance de votre classe - et maintenant vous pouvez jouer avec __getattr__/ __setattr__/ __getattribute__si nécessaire.


Remarque 1 : Si vous utilisez cette fonctionnalité, tout le reste du module, comme les globaux, les autres fonctions, etc., sera perdu lors de l' sys.modulesaffectation - assurez-vous donc que tout ce qui est nécessaire se trouve dans la classe de remplacement.

Note 2 : Pour soutenir, from module import *vous devez avoir __all__défini dans la classe; par exemple:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

Selon votre version de Python, il peut y avoir d'autres noms à omettre __all__. Le set()peut être omis si la compatibilité Python 2 n'est pas nécessaire.

Ethan Furman
la source
3
Cela fonctionne parce que la machinerie d'importation active activement ce hack et que sa dernière étape extrait le module réel de sys.modules, après son chargement. Est-ce mentionné quelque part dans la documentation?
Piotr Dobrogost
4
Maintenant, je me sens plus à l'aise en utilisant ce hack, le considérant "semi-sanctionné" :)
Mark Nunberg
3
Cela fait des choses screwy, comme faire import sysdonner Nonepour sys. Je suppose que ce hack n'est pas autorisé en Python 2.
asmeurer
3
@asmeurer: Pour comprendre la raison de cela (et une solution) voir la question Pourquoi la valeur de __name__ change-t-elle après l'affectation à sys.modules [__ name__]? .
martineau
1
@qarma: Je semble me souvenir de certaines améliorations dont on parle qui permettraient aux modules python de participer plus directement au modèle d'héritage de classe, mais malgré cela, cette méthode fonctionne toujours et est prise en charge.
Ethan Furman
48

C'est un hack, mais vous pouvez envelopper le module avec une classe:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])
Håvard S
la source
10
nice and dirty: D
Matt Joiner
Cela peut fonctionner mais ce n'est probablement pas une solution au vrai problème de l'auteur.
DasIch
12
«Peut fonctionner» et «probablement pas» n'est pas très utile. C'est un hack / astuce, mais cela fonctionne et résout le problème posé par la question.
Håvard S
6
Bien que cela fonctionne dans d' autres modules qui importent votre module et accèdent à des attributs inexistants, cela ne fonctionnera pas pour l'exemple de code réel ici. L'accès à globals () ne passe pas par sys.modules.
Marius Gedminas
5
Malheureusement, cela ne fonctionne pas pour le module actuel, ou probablement pour les éléments accessibles après un import *.
Matt Joiner
19

Nous ne le faisons généralement pas de cette façon.

Voici ce que nous faisons.

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

Pourquoi? Pour que l'instance globale implicite soit visible.

Pour des exemples, regardez le randommodule, qui crée une instance globale implicite pour simplifier légèrement les cas d'utilisation où vous voulez un générateur de nombres aléatoires "simple".

S.Lott
la source
Si vous êtes vraiment ambitieux, vous pouvez créer la classe, parcourir toutes ses méthodes et créer une fonction au niveau du module pour chaque méthode.
Paul Fisher
@Paul Fisher: Selon le problème, la classe existe déjà. Exposer toutes les méthodes de la classe n'est peut-être pas une bonne idée. Habituellement, ces méthodes exposées sont des méthodes «pratiques». Tous ne sont pas appropriés pour l'instance globale implicite.
S.Lott
13

Semblable à ce que @ Håvard S a proposé, dans un cas où je devais implémenter un peu de magie sur un module (comme __getattr__), je définirais une nouvelle classe qui hérite types.ModuleTypeet la mettrait sys.modules(en remplaçant probablement le module où ma coutume a ModuleTypeété définie).

Voir le __init__.pyfichier principal de Werkzeug pour une implémentation assez robuste de ceci.

Matt Anderson
la source
8

C'est hackish, mais ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

Cela fonctionne en itérant sur tous les objets de l'espace de noms global. Si l'élément est une classe, il itère sur les attributs de classe. Si l'attribut est appelable, il l'ajoute à l'espace de noms global en tant que fonction.

Il ignore tous les attributs qui contiennent "__".

Je n'utiliserais pas cela dans le code de production, mais cela devrait vous aider à démarrer.

pleurer
la source
2
Je préfère la réponse de Håvard S à la mienne, car elle semble beaucoup plus propre, mais cela répond directement à la question posée.
pleurer
C'est beaucoup plus proche de ce que j'ai finalement choisi. C'est un peu compliqué, mais fonctionne correctement avec globals () dans le même module.
Matt Joiner
1
Il me semble que cette réponse n'est pas tout à fait ce qui a été demandé, à savoir "Lors de l'appel d'une fonction qui n'existe pas dans les attributs définis statiquement d'un module" parce qu'elle fait son travail sans condition et ajoute toutes les méthodes de classe possibles. Cela pourrait être corrigé en utilisant un wrapper de module qui ne fait le que AddGlobalAttribute()lorsqu'il y a un niveau de module AttributeError- un peu l'inverse de la logique de @ Håvard S. Si j'ai une chance, je vais tester ceci et ajouter ma propre réponse hybride même si l'OP a déjà accepté cette réponse.
martineau
Mettre à jour mon commentaire précédent. Je comprends maintenant qu'il est très difficile (impssoble?) D'intercepter les NameErrorexceptions pour l'espace de noms global (module) - ce qui explique pourquoi cette réponse ajoute des appelables pour chaque possibilité qu'elle trouve à l'espace de noms global actuel pour couvrir tous les cas possibles à l'avance.
martineau le
5

Voici ma propre contribution humble - un léger embellissement de la réponse très appréciée de @ Håvard S, mais un peu plus explicite (donc cela pourrait être acceptable pour @ S.Lott, même si probablement pas assez bien pour l'OP):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")
Martineau
la source
-3

Créez votre fichier de module contenant vos classes. Importez le module. Exécutez getattrsur le module que vous venez d'importer. Vous pouvez effectuer une importation dynamique en utilisant __import__et extraire le module de sys.modules.

Voici votre module some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

Et dans un autre module:

import some_module

Foo = getattr(some_module, 'Foo')

Faire cela dynamiquement:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')
drr
la source
1
Vous répondez à une question différente ici.
Marius Gedminas