Zeichenfolge in Python-Klassenobjekt konvertieren?

187

Wenn eine Zeichenfolge als Benutzereingabe für eine Python-Funktion angegeben wird, möchte ich ein Klassenobjekt daraus entfernen, wenn sich im aktuell definierten Namespace eine Klasse mit diesem Namen befindet. Im Wesentlichen möchte ich die Implementierung für eine Funktion, die diese Art von Ergebnis erzeugt:

class Foo:
    pass

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

Ist das überhaupt möglich?


quelle
2
Es gibt einige nützliche Antworten hier, aber ich fand die Antwort auf diese Frage besonders nützlich: stackoverflow.com/questions/452969/…
Tom

Antworten:

115

Warnung : eval()Kann verwendet werden, um beliebigen Python-Code auszuführen. Sie sollten nie verwenden eval()mit nicht vertrauenswürdigen Strings. (Siehe Sicherheit von Pythons eval () für nicht vertrauenswürdige Zeichenfolgen? )

Das scheint am einfachsten zu sein.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
S.Lott
quelle
30
Achten Sie auf die Auswirkungen der Verwendung von eval. Stellen Sie besser sicher, dass die übergebene Zeichenfolge nicht vom Benutzer stammt.
Übertaktet
18
Die Verwendung von eval () lässt die Tür für die Ausführung von willkürlichem Code offen. Aus Sicherheitsgründen sollte dies vermieden werden.
m.kocikowski
54
Eval lässt die Tür für nichts offen. Wenn Sie böswillige, böse Benutzer haben, die böswillig und böse schlechte Werte an eval übergeben, können sie einfach die Python-Quelle bearbeiten. Da sie nur die Python-Quelle bearbeiten können, ist, war und bleibt die Tür offen.
S.Lott
34
Es sei denn, das betreffende Programm wird auf einem Server ausgeführt.
Vatsu1
12
@ s-lott Es tut mir leid, aber ich denke, Sie geben hier sehr schlechte Ratschläge. Bedenken Sie: Wenn Ihr Computer Sie zur Eingabe eines Kennworts auffordert, kann der böse, böswillige Benutzer genauso gut die entsprechende ausführbare Datei oder Bibliothek ändern und diese Prüfung umgehen. Daher könnte man argumentieren, dass es sinnlos ist, überhaupt Passwortprüfungen hinzuzufügen. Oder ist es?
Daniel Sk
148

Das könnte funktionieren:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
Sechster Gang
quelle
Was passiert, wenn die Klasse nicht existiert?
Riza
7
Es wird nur für die im aktuellen Modul definierte Klasse funktionieren
luc
1
Wirklich schöne Lösung: Hier verwenden Sie keinen Import, sondern ein allgemeineres Sys-Modul.
Stefano Falsetto
Wäre das anders als def str_to_class(str): return vars()[str]?
Arne
113

Sie könnten so etwas tun wie:

globals()[class_name]
Laurence Gonsalves
quelle
6
Ich mag dies besser als die 'eval'-Lösung, weil (1) es genauso einfach ist und (2) Sie nicht für die Ausführung von willkürlichem Code offen sind.
mjumbewu
2
aber Sie verwenden globals()... eine andere Sache, die vermieden werden sollte
Greg
5
@ Greg Vielleicht, ist aber globals()wohl weit weniger schlecht als evalund nicht wirklich schlechter als sys.modules[__name__]AFAIK.
Laurence Gonsalves
2
Ja, ich verstehe Ihren Standpunkt, weil Sie nur daran festhalten und nichts einstellen
Greg
Was ist, wenn die Variable in einer Klasse verwendet wird und mehrere Instanzen dieser Klasse gleichzeitig erstellt wurden? Wird es ein Problem verursachen, da dies eine globale Variable ist?
Alok Kumar Singh
98

Sie möchten die Klasse Baz, die im Modul lebt foo.bar. Mit Python 2.7 möchten Sie Folgendes verwenden importlib.import_module(), da dies den Übergang zu Python 3 erleichtert:

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

Mit 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

Verwenden:

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

Dies behandelt sowohl Klassen im alten als auch im neuen Stil genau.

Evan Fosmark
quelle
Sie benötigen keine types.TypeType; Es ist nur ein Alias ​​für den eingebauten "Typ".
Glenn Maynard
1
Ja, ich weiß, aber ich denke, es ist nur klarer zu lesen. Ich denke, es läuft nur auf die Präferenz hinaus.
Evan Fosmark
17

Ich habe mir angesehen, wie Django damit umgeht

django.utils.module_loading hat dies

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

Sie können es wie verwenden import_string("module_path.to.all.the.way.to.your_class")

Eugen
quelle
Dies ist eine großartige Antwort. Hier ist ein Link zu den neuesten Django-Dokumenten mit der aktuellen Implementierung .
Greg Kaleka
3

Ja, das kannst du machen. Angenommen, Ihre Klassen befinden sich im globalen Namespace.

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
quelle
1
In Python 3 gibt es keinen ClassType mehr.
Riza
1
Verwenden Sie isinstance (x, Typ).
Glenn Maynard
3

In Bezug auf die Ausführung von beliebigem Code oder unerwünschte vom Benutzer übergebene Namen können Sie eine Liste akzeptabler Funktions- / Klassennamen haben. Wenn die Eingabe mit einer in der Liste übereinstimmt, wird sie ausgewertet.

PS: Ich weiß ... ein bisschen spät ... aber es ist für alle anderen, die in Zukunft darüber stolpern.

JoryRFerrell
quelle
9
Wenn Sie die Liste pflegen möchten, können Sie stattdessen auch ein Diktat vom Namen zur Klasse beibehalten. Dann ist eval () nicht erforderlich.
Fasaxc
3

Die Verwendung von importlib hat für mich am besten funktioniert.

import importlib

importlib.import_module('accounting.views') 

Dies verwendet die Zeichenfolgenpunktnotation für das Python-Modul, das Sie importieren möchten.

Aaron Lelevier
quelle
3

Wenn Sie Klassen, die Sie mit einer Zeichenfolge erstellen, wirklich abrufen möchten, sollten Sie sie in einem Wörterbuch speichern (oder richtig formulieren, referenzieren ). Auf diese Weise können Sie Ihre Klassen auch auf einer höheren Ebene benennen und unerwünschte Klassen vermeiden.

Beispiel aus einem Spiel, in dem in Python Schauspielerklassen definiert sind und Sie vermeiden möchten, dass andere allgemeine Klassen durch Benutzereingaben erreicht werden.

Ein anderer Ansatz (wie im folgenden Beispiel) würde darin bestehen, eine völlig neue Klasse zu erstellen, die die dictoben genannten enthält. Das würde:

  • Ermöglichen Sie, dass mehrere Klasseninhaber für eine einfachere Organisation erstellt werden (z. B. einer für Schauspielerklassen und einer für Klangarten).
  • Erleichtern Sie Änderungen sowohl am Inhaber als auch an den Klassen, die abgehalten werden.
  • Und Sie können Klassenmethoden verwenden, um dem Diktat Klassen hinzuzufügen. (Obwohl die folgende Abstraktion nicht wirklich notwendig ist, dient sie nur der ... "Illustration" ).

Beispiel:

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

Das bringt mich zurück:

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

Ein weiteres lustiges Experiment, das damit zu tun hat, ist das Hinzufügen einer Methode, die das einbindet, ClassHolderdamit Sie nie alle Klassen verlieren, die Sie gemacht haben: ^)

Gustavo6046
quelle