Comment implémenter l'équivalent d'un __getattr__
sur une classe, sur un module?
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
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.
Il y a deux problèmes de base que vous rencontrez ici:
__xxx__
Ne sont consultées que sur la classeTypeError: 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.
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__])
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".
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.
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.
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")