Konwertuj ciąg znaków na obiekt klasy Python?

187

Biorąc pod uwagę ciąg znaków wprowadzany przez użytkownika do funkcji Python, chciałbym wyciągnąć z niego obiekt klasy, jeśli istnieje klasa o tej nazwie w aktualnie zdefiniowanej przestrzeni nazw. Zasadniczo chcę implementacji funkcji, która da taki wynik:

class Foo:
    pass

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

Czy to w ogóle jest możliwe?


źródło
2
Istnieje kilka przydatnych odpowiedzi tutaj, ale uważam, że odpowiedź na to pytanie jest szczególnie przydatna: stackoverflow.com/questions/452969/…
Tom

Odpowiedzi:

115

Ostrzeżenie : eval()może być użyte do wykonania dowolnego kodu Pythona. Nigdy nie powinieneś używać eval()z niezaufanymi łańcuchami. (Zobacz Bezpieczeństwo eval () Pythona na niezaufanych ciągach? )

To wydaje się najprostsze.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
S.Lott
źródło
30
Uważaj na konsekwencje używania eval. Lepiej upewnij się, że przekazany ciąg nie pochodzi od użytkownika.
Overclocked
18
Użycie eval () pozostawia otwarte drzwi do wykonania dowolnego kodu, ze względów bezpieczeństwa należy tego unikać.
m.kocikowski,
54
Eval nie pozostawia otwartych drzwi przed niczym. Jeśli masz złośliwych, złych użytkowników, którzy złośliwie i złośliwie przekazują złe wartości do ewaluacji, mogą po prostu edytować źródło Pythona. Ponieważ mogą po prostu edytować źródło Pythona, drzwi są, były i zawsze będą otwarte.
S.Lott,
34
Chyba że dany program działa na serwerze.
Vatsu1,
12
@ s-lott Przykro mi, ale myślę, że udzielasz tutaj bardzo złych rad. Zastanów się: kiedy komputer wyświetli monit o podanie hasła, zły, złośliwy użytkownik może równie dobrze zmodyfikować odpowiedni plik wykonywalny lub bibliotekę i pominąć tę kontrolę. Dlatego można argumentować, że dodawanie sprawdzania hasła nie ma sensu. Albo to jest?
Daniel Sk
148

To może działać:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
szósty strój
źródło
Co się stanie, jeśli klasa nie będzie istnieć?
riza
7
Będzie działać tylko dla klasy zdefiniowanej w bieżącym module
luc
1
Naprawdę fajne rozwiązanie: tutaj nie używasz importu, ale bardziej ogólny moduł sys.
Stefano Falsetto,
Czy byłoby inaczej def str_to_class(str): return vars()[str]?
Arne
113

Możesz zrobić coś takiego:

globals()[class_name]
Laurence Gonsalves
źródło
6
Podoba mi się to lepiej niż rozwiązanie „eval”, ponieważ (1) jest tak samo łatwe i (2) nie pozostawia cię otwartym na wykonanie dowolnego kodu.
mjumbewu
2
ale używasz globals()... innej rzeczy, której należy unikać
Greg
5
@Greg Być może, ale globals()jest prawdopodobnie znacznie mniej zły niż evali wcale nie gorszy niż sys.modules[__name__]AFAIK.
Laurence Gonsalves
2
Tak, rozumiem, o co ci chodzi, ponieważ tylko chwytasz się tego, nie ustawiając niczego
Greg
Co jeśli zmienna jest używana w klasie i wiele instancji dla tej klasy zostało utworzonych jednocześnie. Czy spowoduje to problemy, ponieważ jest to zmienna globalna.
Alok Kumar Singh
98

Chcesz klasy Baz, która mieszka w module foo.bar. Z Python 2.7 chcesz korzystać importlib.import_module(), ponieważ ułatwi to przejście do 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

Z Pythonem <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

Posługiwać się:

loaded_class = class_for_name('foo.bar', 'Baz')
m.kocikowski
źródło
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)

To dokładnie obsługuje zarówno klasy w starym, jak i nowym stylu.

Evan Fosmark
źródło
Nie potrzebujesz typów.TypeType; to tylko alias dla wbudowanego „typu”.
Glenn Maynard
1
Tak, wiem, ale uważam, że czytanie jest łatwiejsze. Myślę jednak, że sprowadza się to do preferencji.
Evan Fosmark
17

Patrzyłem, jak django sobie z tym radzi

django.utils.module_loading ma to

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

Możesz go używać jak import_string("module_path.to.all.the.way.to.your_class")

eugen
źródło
To świetna odpowiedź. Oto link do najnowszych dokumentów Django z bieżącą implementacją .
Greg Kaleka
3

Tak, możesz to zrobić. Zakładając, że twoje klasy istnieją w globalnej przestrzeni nazw, coś takiego to zrobi:

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
źródło
1
W Pythonie 3 nie ma już ClassType.
riza
1
Użyj isinstance (x, typ).
Glenn Maynard
3

Jeśli chodzi o wykonanie dowolnego kodu lub niepotrzebne nazwy użytkowników, możesz mieć listę akceptowalnych nazw funkcji / klas, a jeśli dane wejściowe pasują do jednej na liście, są ewaluowane.

PS: Wiem ... trochę późno ... ale to dla każdego, kto natknie się na to w przyszłości.

JoryRFerrell
źródło
9
Jeśli zamierzasz zachować listę, równie dobrze możesz zamiast tego prowadzić dyktat od nazwy do klasy. Zatem nie ma potrzeby eval ().
Fasaxc,
3

Korzystanie z importlib działało dla mnie najlepiej.

import importlib

importlib.import_module('accounting.views') 

Używa to notacji ciąg kropkowej dla modułu python, który chcesz zaimportować.

Aaron Lelevier
źródło
3

Jeśli naprawdę chcesz odzyskać klasy, które tworzysz za pomocą łańcucha, powinieneś przechowywać je (lub odpowiednio sformułować, odwoływać się ) w słowniku. W końcu pozwoli to także nazwać swoje klasy na wyższym poziomie i uniknąć ujawniania niepożądanych klas.

Przykład: z gry, w której klasy aktorskie są zdefiniowane w Pythonie i chcesz uniknąć innych klas ogólnych, do których dostęp miałby użytkownik.

Inne podejście (jak w poniższym przykładzie) stworzyłoby całą nową klasę, która zawiera dictpowyższe. To by:

  • Umożliwić tworzenie wielu posiadaczy klas w celu łatwiejszej organizacji (np. Jeden dla klas aktorów, a drugi dla rodzajów dźwięku);
  • Ułatwiaj modyfikowanie zarówno posiadacza, jak i prowadzonych klas;
  • I możesz użyć metod klas, aby dodać klasy do dykta. (Chociaż poniższa abstrakcja nie jest tak naprawdę konieczna, służy jedynie ... „ilustracji” ).

Przykład:

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

To zwraca mi:

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

Kolejnym zabawnym eksperymentem związanym z tymi jest dodanie metody, która pikluje ClassHolder, abyś nigdy nie stracił wszystkich zajęć: ^)

Gustavo6046
źródło