Convertir la chaîne en objet de classe Python?

196

Étant donné une chaîne en tant qu'entrée utilisateur dans une fonction Python, j'aimerais en extraire un objet de classe s'il y a une classe avec ce nom dans l'espace de noms actuellement défini. Essentiellement, je veux l'implémentation d'une fonction qui produira ce genre de résultat:

class Foo:
    pass

str_to_class("Foo")
==> <class __main__.Foo at 0x69ba0>

Est-ce possible?


la source
2
Il y a quelques réponses utiles ici, mais j'ai trouvé la réponse à cette question particulièrement utile: stackoverflow.com/questions/452969/…
Tom

Réponses:

121

Attention : eval()peut être utilisé pour exécuter du code Python arbitraire. Vous ne devez jamais utiliser eval()avec des chaînes non approuvées. (Voir Sécurité de eval () de Python sur des chaînes non approuvées? )

Cela semble le plus simple.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
S.Lott
la source
30
Attention à la ramification de l'utilisation d'eval. Vous feriez mieux de vous assurer que la chaîne que vous avez transmise ne provient pas de l'utilisateur.
Overclocké le
18
Utiliser eval () laisse la porte ouverte à l'exécution de code arbitraire, pour des raisons de sécurité, cela devrait être évité.
m.kocikowski
56
Eval ne laisse la porte ouverte à rien. Si vous avez des utilisateurs malveillants qui pourraient malicieusement et mal transmettre de mauvaises valeurs à eval, ils peuvent simplement modifier la source python. Puisqu'ils peuvent simplement modifier la source python, la porte est, était et sera toujours ouverte.
S.Lott
35
Sauf si le programme en question s'exécute sur un serveur.
Vatsu1
12
@ s-lott Je suis désolé, mais je pense que vous donnez de très mauvais conseils ici. Considérez ceci: lorsque votre ordinateur vous demande un mot de passe, l'utilisateur maléfique et malveillant pourrait tout aussi bien modifier l'exécutable ou la bibliothèque correspondante et contourner cette vérification. Par conséquent, on pourrait dire qu'il est inutile d'ajouter des vérifications de mot de passe en premier lieu. Ou est-ce?
Daniel Sk
151

Cela pourrait fonctionner:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
sixième
la source
Que se passera-t-il si la classe n'existe pas?
riza
7
Cela ne fonctionnera que pour la classe définie dans le module actuel
luc
1
Solution vraiment sympa: ici vous n'utilisez pas d' import mais un module sys plus générique.
Stefano Falsetto
Serait-ce différent de def str_to_class(str): return vars()[str]?
Arne
115

Vous pouvez faire quelque chose comme:

globals()[class_name]
Laurence Gonsalves
la source
6
J'aime mieux cela que la solution «eval» car (1) c'est tout aussi simple, et (2) cela ne vous laisse pas ouvert à l'exécution de code arbitraire.
mjumbewu
2
mais vous utilisez globals()... une autre chose à éviter
Greg
5
@Greg Peut-être, mais il globals()est sans doute beaucoup moins mauvais que eval, et pas vraiment pire que l' sys.modules[__name__]AFAIK.
Laurence Gonsalves
2
Ouais, je vois ton point de vue, parce que tu ne saisis que pour ne rien définir
Greg
Que faire si la variable est utilisée dans une classe et que plusieurs instances de cette classe ont été créées simultanément en même temps. Cela posera-t-il des problèmes, car il s'agit d'une variable globale.
Alok Kumar Singh le
99

Vous voulez la classe Baz, qui vit dans le module foo.bar. Avec Python 2.7, vous souhaitez utiliser importlib.import_module(), car cela facilitera la transition vers Python 3:

import importlib

def class_for_name(module_name, class_name):
    # load the module, will raise ImportError if module cannot be loaded
    m = importlib.import_module(module_name)
    # get the class, will raise AttributeError if class cannot be found
    c = getattr(m, class_name)
    return c

Avec Python <2.7:

def class_for_name(module_name, class_name):
    # load the module, will raise ImportError if module cannot be loaded
    m = __import__(module_name, globals(), locals(), class_name)
    # get the class, will raise AttributeError if class cannot be found
    c = getattr(m, class_name)
    return c

Utilisation:

loaded_class = class_for_name('foo.bar', 'Baz')
m.kocikowski
la source
19
import sys
import types

def str_to_class(field):
    try:
        identifier = getattr(sys.modules[__name__], field)
    except AttributeError:
        raise NameError("%s doesn't exist." % field)
    if isinstance(identifier, (types.ClassType, types.TypeType)):
        return identifier
    raise TypeError("%s is not a class." % field)

Cela gère avec précision les classes de style ancien et nouveau.

Evan Fosmark
la source
Vous n'avez pas besoin de types.TypeType; c'est juste un alias pour le "type" intégré.
Glenn Maynard
1
Ouais je sais, mais je pense que c'est juste plus clair à lire. Je suppose que cela se résume à la préférence, cependant.
Evan Fosmark le
17

J'ai regardé comment django gère cela

django.utils.module_loading a ceci

def import_string(dotted_path):
    """
    Import a dotted module path and return the attribute/class designated by the
    last name in the path. Raise ImportError if the import failed.
    """
    try:
        module_path, class_name = dotted_path.rsplit('.', 1)
    except ValueError:
        msg = "%s doesn't look like a module path" % dotted_path
        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])

    module = import_module(module_path)

    try:
        return getattr(module, class_name)
    except AttributeError:
        msg = 'Module "%s" does not define a "%s" attribute/class' % (
            module_path, class_name)
        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])

Vous pouvez l'utiliser comme import_string("module_path.to.all.the.way.to.your_class")

Eugène
la source
C'est une excellente réponse. Voici un lien vers les derniers documents Django avec l'implémentation actuelle .
Greg Kaleka le
4

Si vous voulez vraiment récupérer les classes que vous créez avec une chaîne, vous devez les stocker (ou les référencer correctement ) dans un dictionnaire. Après tout, cela permettra également de nommer vos classes à un niveau supérieur et d'éviter d'exposer des classes indésirables.

Exemple, à partir d'un jeu où les classes d'acteurs sont définies en Python et que vous voulez éviter que d'autres classes générales soient atteintes par l'entrée de l'utilisateur.

Une autre approche (comme dans l'exemple ci-dessous) consisterait à créer une nouvelle classe entière, qui contient ce qui dictprécède. Ce serait:

  • Permettre à plusieurs détenteurs de classe d'être organisés pour une organisation plus facile (comme, un pour les classes d'acteurs et un autre pour les types de sons);
  • Facilitez les modifications du titulaire et des classes tenues;
  • Et vous pouvez utiliser des méthodes de classe pour ajouter des classes au dict. (Bien que l'abstraction ci-dessous ne soit pas vraiment nécessaire, c'est simplement pour ... "illustration" ).

Exemple:

class ClassHolder:
    def __init__(self):
        self.classes = {}

    def add_class(self, c):
        self.classes[c.__name__] = c

    def __getitem__(self, n):
        return self.classes[n]

class Foo:
    def __init__(self):
        self.a = 0

    def bar(self):
        return self.a + 1

class Spam(Foo):
    def __init__(self):
        self.a = 2

    def bar(self):
        return self.a + 4

class SomethingDifferent:
    def __init__(self):
        self.a = "Hello"

    def add_world(self):
        self.a += " World"

    def add_word(self, w):
        self.a += " " + w

    def finish(self):
        self.a += "!"
        return self.a

aclasses = ClassHolder()
dclasses = ClassHolder()
aclasses.add_class(Foo)
aclasses.add_class(Spam)
dclasses.add_class(SomethingDifferent)

print aclasses
print dclasses

print "======="
print "o"
print aclasses["Foo"]
print aclasses["Spam"]
print "o"
print dclasses["SomethingDifferent"]

print "======="
g = dclasses["SomethingDifferent"]()
g.add_world()
print g.finish()

print "======="
s = []
s.append(aclasses["Foo"]())
s.append(aclasses["Spam"]())

for a in s:
    print a.a
    print a.bar()
    print "--"

print "Done experiment!"

Cela me renvoie:

<__main__.ClassHolder object at 0x02D9EEF0>
<__main__.ClassHolder object at 0x02D9EF30>
=======
o
<class '__main__.Foo'>
<class '__main__.Spam'>
o
<class '__main__.SomethingDifferent'>
=======
Hello World!
=======
0
1
--
2
6
--
Done experiment!

Une autre expérience amusante à faire avec ceux-ci consiste à ajouter une méthode qui décape le ClassHolderpour ne jamais perdre toutes les classes que vous avez faites: ^)

MISE À JOUR: Il est également possible d'utiliser un décorateur comme raccourci.

class ClassHolder:
    def __init__(self):
        self.classes = {}

    def add_class(self, c):
        self.classes[c.__name__] = c

    # -- the decorator
    def held(self, c):
        self.add_class(c)

        # Decorators have to return the function/class passed (or a modified variant thereof), however I'd rather do this separately than retroactively change add_class, so.
        # "held" is more succint, anyway.
        return c 

    def __getitem__(self, n):
        return self.classes[n]

food_types = ClassHolder()

@food_types.held
class bacon:
    taste = "salty"

@food_types.held
class chocolate:
    taste = "sweet"

@food_types.held
class tee:
    taste = "bitter" # coffee, ftw ;)

@food_types.held
class lemon:
    taste = "sour"

print(food_types['bacon'].taste) # No manual add_class needed! :D
Gustavo6046
la source
1
Gee j'avais écrit une réponse proposant cela, puis j'ai vu ceci. D'accord - je pense que c'est la meilleure approche. SIMPLE, en utilisant simplement le langage avec un minimum de code / additions.
logicOnAbstractions
Merci @logicOnAbstractions, je suis heureux que vous le pensiez! Je suis un peu flatté. Vous m'avez inspiré pour ajouter un exemple plus récent: DI a beaucoup appris depuis 2016. La plupart de mon code à l'époque était plutôt moche. Cela s'est clairement démarqué, cependant. Même si ce n'est qu'une réponse Stack Overflow!
Gustavo6046
3

Oui, vous pouvez le faire. En supposant que vos classes existent dans l'espace de noms global, quelque chose comme ceci le fera:

import types

class Foo:
    pass

def str_to_class(s):
    if s in globals() and isinstance(globals()[s], types.ClassType):
            return globals()[s]
    return None

str_to_class('Foo')

==> <class __main__.Foo at 0x340808cc>
ollyc
la source
1
Dans Python 3, il n'y a plus de ClassType.
riza
1
Utilisez isinstance (x, type).
Glenn Maynard
3

En termes d'exécution de code arbitraire ou de noms non désirés par l'utilisateur, vous pouvez avoir une liste de noms de fonctions / classes acceptables, et si l'entrée correspond à une dans la liste, elle est évaluée.

PS: Je sais ... un peu tard ... mais c'est pour tous ceux qui trébucheront là-dessus à l'avenir.

JoryRFerrell
la source
9
Si vous voulez maintenir la liste, vous pouvez aussi bien maintenir un dict du nom à la classe à la place. Ensuite, il n'est pas nécessaire d'utiliser eval ().
Fasaxc
3

Utiliser importlib a fonctionné le mieux pour moi.

import importlib

importlib.import_module('accounting.views') 

Cela utilise la notation de point de chaîne pour le module python que vous souhaitez importer.

Aaron Lelevier
la source