web-dev-qa-db-fra.com

__getattr__ sur un module

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

Exemple

Lorsque j'appelle 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 avec le même nom que celle qui a échoué dans la recherche d'attribut 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 __== "__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
111
Matt Joiner

Il y a quelque temps, Guido a déclaré que toutes les recherches de méthodes spéciales sur les classes de nouveau style contournent __getattr__ Et __getattribute__ . Les méthodes Dunder avaient précédemment fonctionné sur les modules - vous pouvez, par exemple, utiliser un module comme gestionnaire de contexte en définissant simplement __enter__ Et __exit__, Avant ces astuces cassé .

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

Dans Python 3.7+, vous utilisez simplement la seule façon évidente. Pour personnaliser l'accès aux attributs sur un module, définissez une fonction __getattr__ Au niveau du module qui devrait accepter un argument (nom d'attribut), et retourner la valeur calculée ou augmenter un AttributeError:

# my_module.py

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

Cela autorisera également les connexions dans les importations "de", 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 fonction __dir__ Au niveau du module pour répondre à dir(my_module) . Voir PEP 562 pour plus de détails.

24
wim

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

  1. Les méthodes __xxx__ Ne sont consulté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 qui a été examiné, sinon tous les 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 tirer les méthodes de la classe de substitution et les ajouter à globals() eiher avec une méthode personnalisée sur la classe (j'aime utiliser .export()) ou avec une fonction générique (comme celles déjà listé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 autre.


Mise à 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 machinerie d'importation active activement ce piratage et que sa dernière étape extrait le module réel de sys.modules, après le chargement. (Ce n'est pas un hasard. Le hack a été proposé il y a longtemps et nous avons décidé que nous en aimions assez pour le soutenir dans les machines d'importation.)

Donc, la manière établie d'accomplir ce que vous voulez est de créer une seule classe dans votre module, et comme dernier acte du module, remplacez sys.modules[__name__] Par une instance de votre classe - et maintenant vous pouvez jouer avec __getattr__/__setattr__/__getattribute__ Au besoin.

Notez que si vous utilisez cette fonctionnalité, tout autre élément du module, comme les globaux, les autres fonctions, etc., sera perdu lorsque l'affectation sys.modules Sera effectuée - assurez-vous donc que tout le nécessaire se trouve dans la classe de remplacement.

107
Ethan Furman

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__])
45
Håvard S

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 module random, 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".

19
S.Lott

Semblable à ce que @ Håvard S a proposé, dans un cas où j'avais besoin d'implémenter de la magie sur un module (comme __getattr__), Je définirais une nouvelle classe qui hérite de types.ModuleType et mettez ça dans sys.modules (remplaçant probablement le module où mon ModuleType personnalisé a été défini).

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

13
Matt Anderson

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 __== "__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 peut être appelé, il l'ajoute à l'espace de noms global en tant que fonction.

Il ignore tous les attributs qui contiennent "__".

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

7
grieve

Voici ma propre humble contribution - 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 bon 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 __== "__main__":
    _globals.salutation("world")
4
martineau