Преобразовать строку в объект класса Python?

187

Учитывая строку в качестве пользовательского ввода в функцию Python, я хотел бы извлечь из нее объект класса, если в текущем определенном пространстве имен есть класс с таким именем. По сути, я хочу реализацию для функции, которая даст такой результат:

class Foo:
    pass

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

Это вообще возможно?


источник
2
Здесь есть несколько полезных ответов, но я нашел ответ на этот вопрос особенно полезным: stackoverflow.com/questions/452969/…
Том

Ответы:

116

Предупреждение : eval()может использоваться для выполнения произвольного кода Python. Вы никогда не должны использовать eval()с ненадежными строками. (См. Безопасность eval () Python для ненадежных строк? )

Это кажется самым простым.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
С. Лотт
источник
30
Остерегайтесь последствий использования eval. Вам лучше убедиться, что строка, которую вы передали, не от пользователя.
разогнан
18
Использование eval () оставляет дверь открытой для выполнения произвольного кода, в целях безопасности этого следует избегать.
m.kocikowski
55
Эвал не оставляет дверь открытой ни к чему. Если у вас есть злые, злые пользователи, которые могут злонамеренно и злобно передавать плохие значения eval, они могут просто отредактировать исходный код python. Поскольку они могут просто редактировать исходный код Python, дверь есть, была и всегда будет открыта.
С.Лотт
34
Если рассматриваемая программа не запущена на сервере.
Vatsu1
12
@ s-lott Извините, но я думаю, что вы даете очень плохой совет здесь. Подумайте: когда ваш компьютер запрашивает пароль, злой злоумышленник может также изменить соответствующий исполняемый файл или библиотеку и обойти эту проверку. Поэтому можно утверждать, что бессмысленно добавлять проверки паролей в первую очередь. Либо это?
Даниэль Ск
148

Это может сработать:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
sixthgear
источник
Что будет, если класс не существует?
Риза
7
Это будет работать только для класса, определенного в текущем модуле
Люк
1
Действительно хорошее решение: здесь вы используете не импорт, а более общий модуль sys.
Стефано Фальсетто
Будет ли это чем-то отличаться от def str_to_class(str): return vars()[str]?
Арне
113

Вы могли бы сделать что-то вроде:

globals()[class_name]
Лоуренс Гонсалвес
источник
6
Мне это нравится больше, чем решение 'eval', потому что (1) оно так же просто, и (2) оно не оставляет вас открытым для выполнения произвольного кода.
Мюмбеву
2
но вы используете globals()... еще одну вещь, которую следует избегать
Грег
5
@ Грег Возможно, но globals(), возможно, гораздо менее плох, чем eval, и не намного хуже, чем sys.modules[__name__]AFAIK.
Лоуренс Гонсалвес
2
Да, я понимаю твою точку зрения, потому что ты только хватаешься за это и ничего не настраиваешь
Грег
Что если переменная используется в классе, и несколько экземпляров этого класса были созданы одновременно в одно и то же время. Это вызовет проблемы, так как это глобальная переменная.
Алок Кумар Сингх
98

Вы хотите класс Baz, который живет в модуле foo.bar. Вы хотите использовать Python 2.7, так importlib.import_module()как это облегчит переход на 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

С 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

Использование:

loaded_class = class_for_name('foo.bar', 'Baz')
m.kocikowski
источник
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)

Это точно обрабатывает классы как старого, так и нового стиля.

Эван Фосмарк
источник
Вам не нужны типы. TypeType; это просто псевдоним для встроенного типа.
Гленн Мейнард
1
Да, я знаю, но я чувствую, что читать легче. Я думаю, это просто сводится к предпочтениям, хотя.
Эван Фосмарк
17

Я смотрел на то, как Django справляется с этим

django.utils.module_loading имеет это

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

Вы можете использовать его как import_string("module_path.to.all.the.way.to.your_class")

Евгений
источник
Это отличный ответ. Вот ссылка на последние документы Django с текущей реализацией .
Грег Калека
3

Да, ты можешь это сделать. Предполагая, что ваши классы существуют в глобальном пространстве имен, что-то вроде этого сделает это:

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
источник
1
В Python 3 больше нет ClassType.
Риза
1
Используйте isinstance (x, тип).
Гленн Мейнард
3

С точки зрения выполнения произвольного кода или нежелательных имен, переданных пользователем, у вас может быть список допустимых имен функций / классов, и, если входные данные соответствуют одному в списке, это eval'd.

PS: я знаю ... немного поздно ... но это для всех, кто столкнется с этим в будущем.

JoryRFerrell
источник
9
Если вы собираетесь вести список, то вместо этого можете поддерживать диктат от имени к классу. Тогда нет необходимости в eval ().
Fasaxc
3

Использование importlib работало лучше для меня.

import importlib

importlib.import_module('accounting.views') 

При этом используется нотация с точкой для модуля Python, который вы хотите импортировать.

Аарон Лелевье
источник
3

Если вы действительно хотите получить классы, которые вы делаете со строкой, вы должны сохранить (или правильно сформулировать, ссылку ) их в словаре. В конце концов, это также позволит назвать ваши классы на более высоком уровне и избежать выявления нежелательных классов.

Например, из игры, в которой классы актеров определены в Python, и вы хотите избежать других общих классов, которые будут доступны при вводе пользователем.

Другой подход (как в примере ниже) состоит в создании целого нового класса, который содержит dictвышеупомянутое. Это будет:

  • Разрешить создание нескольких владельцев классов для упрощения организации (например, один для актерских классов и другой для типов звука);
  • Вносите изменения как в владельца, так и в классы, которые легче проводить;
  • И вы можете использовать методы класса, чтобы добавить классы к dict. (Хотя приведенная ниже абстракция на самом деле не нужна, она просто для ... "иллюстрации" ).

Пример:

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

Это возвращает меня:

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

Еще один забавный эксперимент с ними - добавить метод, который перебирает все, ClassHolderчтобы вы никогда не потеряли все классы, которые вы сделали: ^)

Gustavo6046
источник