2009-07-24 8 views
125

Étant donné une chaîne comme entrée d'utilisateur pour une fonction python, j'aimerais en extraire un objet de classe s'il existe une classe portant ce nom dans l'espace de nommage actuellement défini. Essentiellement, je veux l'implémentation pour une fonction qui produira ce genre de résultat:Convertir une chaîne en objet de classe Python?

class Foo: 
    pass 

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

Est-ce possible?

+2

Il y a quelques réponses utiles ici, mais j'ai trouvé la réponse à cette question particulièrement utile: http://stackoverflow.com/q uestions/452969/does-python-avoir-un-équivalent-à-java-class-forname – Tom

Répondre

79

Cela semble plus simple.

>>> class Foo(object): 
...  pass 
... 
>>> eval("Foo") 
<class '__main__.Foo'> 
+24

Beaware 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. – Overclocked

+16

L'utilisation de eval() laisse la porte ouverte à l'exécution de code arbitraire, pour des raisons de sécurité, elle devrait être évitée. –

+41

Eval ne laisse pas la porte ouverte à quoi que ce soit. Si vous avez des utilisateurs malveillants et mal intentionnés qui pourraient mal intentionnellement transmettre les mauvaises valeurs à Eval, ils peuvent simplement modifier la source Python. Puisqu'ils peuvent juste éditer la source de python, la porte est, était, et sera toujours ouverte. –

87

Vous pouvez faire quelque chose comme:

globals()[class_name] 
+5

J'aime mieux que la solution 'eval' parce que (1) c'est aussi facile, et (2) il ne part pas vous ouvrez à l'exécution de code arbitraire. – mjumbewu

+0

mais vous utilisez 'globals()' ... autre chose à éviter – Greg

+3

@Greg Peut-être, mais 'globals()' est sans doute bien moins mauvais que 'eval', et pas vraiment pire que' sys. modules [__ nom __] 'AFAIK. –

106

Cela pourrait fonctionner:

import sys 

def str_to_class(str): 
    return getattr(sys.modules[__name__], str) 
+0

Que se passera-t-il si la classe n'existe pas? – riza

+1

Cela ne fonctionnera que pour la classe définie dans le module en cours – luc

+33

Désolé de soulever cette réponse du défunt. @luc a raison, mais nous pouvons remplacer la dernière ligne par 'return reduce (getattr, str.split (". "), sys.modules [__ nom __])'. Cela donnera le même effet que 'eval()' mais ne permettra pas d'exécuter du code arbitraire. – jollyroger

2

Oui, vous pouvez le faire. En supposant que vos classes existent dans l'espace de noms global, quelque chose comme ça 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> 
+1

En Python 3, il n'y a plus de ClassType. – riza

+0

Utilisez isinstance (x, type). –

18
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) 

Cette poignées précisément à la fois de style ancien et les classes de nouveau style.

+0

Vous n'avez pas besoin de types.TypeType; c'est juste un alias pour le "type" intégré. –

+1

Oui, je sais, mais je pense que c'est plus clair à lire. Je suppose que cela se résume à la préférence, cependant. –

71

Vous voulez la classe "Baz", qui réside dans le module "foo.bar". Avec Python 2.7, que vous souhaitez utiliser importlib.import_module(), car cela fera la transition vers python 3 plus facile:

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 

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') 
+1

sympa, cela fonctionne même quand la classe n'est pas importée à l'origine dans ce module – parxier

2

En termes d'exécution de code arbitraire ou de noms d'utilisateur non autorisés, vous pouvez avoir une liste de noms de classe/fonction acceptables, et si l'entrée correspond à un dans la liste, elle est eval'd. PS: Je sais ... un peu tard ... mais c'est pour quelqu'un d'autre qui trébuche à travers cela à l'avenir.

+6

Si vous voulez maintenir la liste, vous pourriez aussi bien conserver une dict de nom en classe. Ensuite, il n'y a pas besoin d'eval(). – Fasaxc

1

En utilisant importlib a travaillé le meilleur pour moi.

import importlib 

importlib.import_module('accounting.views') 

Cette chaîne utilise dot notation pour le module python que vous souhaitez importer.

6

J'ai regardé comment poignées django cette

django.utils.module_loading a cette

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")

1

Si vous voulez vraiment récupérer les classes que vous faites avec une chaîne, vous devriez stocker (ou correctement libellé, référence) eux dans un dictionnaire. Après tout, cela permettra également de nommer vos classes dans un niveau supérieur et d'éviter d'exposer des classes non désirées.Exemple, à partir d'un jeu où les classes d'acteur sont définies en Python et 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) permettrait de créer une nouvelle classe entière, qui contient le dict ci-dessus. Cela permettrait:

  • Permettre à plusieurs détenteurs de classes d'être faites pour une organisation plus facile (comme, une pour les classes d'acteur et une autre pour les types de sons);
  • Modifiez à la fois le titulaire et les classes qui sont retenues;
  • Et vous pouvez utiliser des méthodes de classe pour ajouter des classes à la dict. (Bien que l'abstraction ci-dessous ne soit pas vraiment nécessaire, c'est simplement pour ... "illustration").

Exemple:

class ClassHolder(object): 
    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(object): 
    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(object): 
    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 retourne:

<__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 à voir avec celles-ci est d'ajouter une méthode qui Pickles la ClassHolder vous ne perdez jamais toutes les classes que vous fait: ^)

Questions connexes