Convertir cadena a objeto de clase Python?

187

Dada una cadena como entrada del usuario a una función de Python, me gustaría obtener un objeto de clase si hay una clase con ese nombre en el espacio de nombres actualmente definido. Esencialmente, quiero la implementación de una función que produzca este tipo de resultado:

class Foo:
    pass

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

¿Es esto posible?


fuente
2
Aquí hay algunas respuestas útiles, pero la respuesta a esta pregunta me pareció particularmente útil: stackoverflow.com/questions/452969/…
Tom

Respuestas:

115

Advertencia : eval()se puede usar para ejecutar código arbitrario de Python. Usted debe nunca se utilice eval()con las cadenas no son de confianza. (Consulte Seguridad de eval () de Python en cadenas no confiables? )

Esto parece más simple.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
S.Lott
fuente
30
Tenga cuidado con la ramificación del uso de eval. Es mejor que te asegures de que la cadena que ingresaste no sea del usuario.
Overclockeado
18
El uso de eval () deja la puerta abierta para la ejecución de código arbitrario, por el bien de la seguridad, debe evitarse.
m.kocikowski
55
Eval no deja la puerta abierta a nada. Si tiene usuarios malintencionados y malvados que podrían malvadamente y malvadamente pasar valores malos para evaluar, simplemente pueden editar la fuente de Python. Como solo pueden editar la fuente de Python, la puerta está, estaba y siempre estará abierta.
S.Lott
34
A menos que el programa en cuestión se ejecute en un servidor.
Vatsu1
12
@ s-lott Lo siento, pero creo que estás dando muy malos consejos aquí. Considere: cuando su computadora le solicita una contraseña, el usuario malintencionado podría modificar el ejecutable o la biblioteca correspondiente y omitir esta verificación. Por lo tanto, uno podría argumentar que no tiene sentido agregar comprobaciones de contraseña en primer lugar. ¿O es eso?
Daniel Sk
148

Esto podría funcionar:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
sexta marcha
fuente
¿Qué pasará si la clase no existe?
riza
77
Solo funcionará para la clase definida en el módulo actual
luc
1
Muy buena solución: aquí no estás utilizando importación sino un módulo sys más genérico.
Stefano Falsetto
¿Sería esto diferente def str_to_class(str): return vars()[str]?
Arne
113

Podrías hacer algo como:

globals()[class_name]
Laurence Gonsalves
fuente
66
Me gusta más que la solución 'eval' porque (1) es igual de fácil y (2) no te deja abierto a la ejecución de código arbitrario.
mjumbewu
2
pero estás usando globals()... otra cosa que debería evitarse
Greg
55
@ Greg Tal vez, pero globals()podría decirse que es mucho menos malo que eval, y en realidad no es peor que sys.modules[__name__]AFAIK.
Laurence Gonsalves
2
Sí, entiendo tu punto, porque solo estás agarrándolo sin establecer nada
Greg
¿Qué sucede si la variable se usa en una clase y en varias instancias para esa clase se hicieron simultáneamente al mismo tiempo? Causará problemas, ya que esta es una variable global.
Alok Kumar Singh
98

Quieres la clase Baz, que vive en el módulo foo.bar. Con Python 2.7, desea usar importlib.import_module(), ya que esto hará que la transición a Python 3 sea más fácil:

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

Con 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

Utilizar:

loaded_class = class_for_name('foo.bar', 'Baz')
m.kocikowski
fuente
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)

Esto maneja con precisión las clases de estilo antiguo y nuevo.

Evan Fosmark
fuente
No necesitas tipos. TipoTipo; es solo un alias para el "tipo" incorporado.
Glenn Maynard
1
Sí, lo sé, pero siento que es más claro de leer. Sin embargo, supongo que todo se reduce a preferencias.
Evan Fosmark
17

He visto cómo django maneja esto

django.utils.module_loading tiene esto

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

Puedes usarlo como import_string("module_path.to.all.the.way.to.your_class")

eugene
fuente
Esta es una respuesta genial. Aquí hay un enlace a los últimos documentos de Django con la implementación actual .
Greg Kaleka
3

Sí, usted puede hacer esto. Suponiendo que sus clases existan en el espacio de nombres global, algo como esto lo hará:

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
fuente
1
En Python 3, no hay más ClassType.
riza
1
Use isinstance (x, type).
Glenn Maynard el
3

En términos de ejecución de código arbitrario, o nombres de usuario no deseados, podría tener una lista de nombres de función / clase aceptables, y si la entrada coincide con uno en la lista, se evalúa.

PD: Lo sé ... un poco tarde ... pero es para cualquiera que se encuentre con esto en el futuro.

JoryRFerrell
fuente
9
Si va a mantener la lista, también podría mantener un dict de nombre a clase en su lugar. Entonces no hay necesidad de eval ().
Fasaxc
3

Usar importlib funcionó mejor para mí.

import importlib

importlib.import_module('accounting.views') 

Esto utiliza la notación de puntos de cadena para el módulo de Python que desea importar.

Aaron Lelevier
fuente
3

Si realmente desea recuperar las clases que realiza con una cadena, debe almacenarlas (o redactarlas correctamente, hacer referencia ) en un diccionario. Después de todo, eso también permitirá nombrar sus clases en un nivel superior y evitar exponer clases no deseadas.

Ejemplo, de un juego en el que las clases de actores se definen en Python y desea evitar que el usuario ingrese a otras clases generales.

Otro enfoque (como en el ejemplo a continuación) sería hacer una clase completamente nueva, que contenga lo dictanterior. Esto sería:

  • Permitir que se hagan múltiples titulares de clase para una organización más fácil (como, una para clases de actores y otra para tipos de sonido);
  • Hacer modificaciones tanto al titular como a las clases que se imparten más fácilmente;
  • Y puede usar métodos de clase para agregar clases al dict. (Aunque la siguiente abstracción no es realmente necesaria, es meramente para ... "ilustración" ).

Ejemplo:

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

Esto me devuelve:

<__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!

Otro experimento divertido que hacer con ellos es agregar un método que conserve el encurtido ClassHolderpara que nunca pierdas todas las clases que hiciste: ^)

Gustavo6046
fuente