Mengonversi string ke objek kelas Python?

187

Diberikan string sebagai input pengguna ke fungsi Python, saya ingin mendapatkan objek kelas dari itu jika ada kelas dengan nama itu di namespace yang saat ini didefinisikan. Pada dasarnya, saya ingin implementasi untuk fungsi yang akan menghasilkan hasil seperti ini:

class Foo:
    pass

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

Apakah ini mungkin?


sumber
2
Ada beberapa jawaban yang berguna di sini, tetapi saya menemukan jawaban untuk pertanyaan ini sangat berguna: stackoverflow.com/questions/452969/…
Tom

Jawaban:

115

Peringatan : eval()dapat digunakan untuk mengeksekusi kode Python sewenang-wenang. Anda seharusnya tidak pernah menggunakan eval()string yang tidak terpercaya. (Lihat Keamanan eval Python () pada string yang tidak dipercaya? )

Ini tampaknya paling sederhana.

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>
S.Lott
sumber
30
Waspadai percabangan penggunaan eval. Anda lebih baik memastikan string yang Anda berikan bukan dari pengguna.
Overclocked
18
Menggunakan eval () membuat pintu terbuka untuk eksekusi kode arbitrer, demi keamanan itu harus dihindari.
m.kocikowski
55
Eval tidak membiarkan pintu terbuka untuk apa pun. Jika Anda memiliki pengguna jahat, jahat yang mungkin jahat dan jahat memberikan nilai buruk untuk eval, mereka bisa mengedit sumber python. Karena mereka hanya dapat mengedit sumber python, pintunya adalah, tadinya, dan akan selalu terbuka.
S.Lott
34
Kecuali jika program tersebut sedang berjalan di server.
Vatsu1
12
@ s-lott Maaf, tapi saya pikir Anda memberikan saran yang sangat buruk di sini. Pertimbangkan: Ketika komputer Anda meminta kata sandi, pengguna jahat, jahat mungkin juga memodifikasi executable atau library yang sesuai dan melewati pemeriksaan ini. Karena itu, orang mungkin berpendapat bahwa tidak ada gunanya menambahkan cek kata sandi sejak awal. Atau itu?
Daniel Sk
148

Ini bisa bekerja:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
keenam
sumber
Apa yang akan terjadi jika kelas tidak ada?
riza
7
Ini hanya akan bekerja untuk kelas yang ditentukan dalam modul saat ini
luc
1
Solusi yang sangat bagus: di sini Anda tidak menggunakan impor tetapi modul sys yang lebih umum.
Stefano Falsetto
Apakah ini berbeda def str_to_class(str): return vars()[str]?
Arne
113

Anda dapat melakukan sesuatu seperti:

globals()[class_name]
Laurence Gonsalves
sumber
6
Saya suka ini lebih baik daripada solusi 'eval' karena (1) sama mudahnya, dan (2) tidak membuat Anda terbuka untuk eksekusi kode arbitrer.
mjumbewu
2
tetapi Anda menggunakan globals()... hal lain yang harus dihindari
Greg
5
@Reg Mungkin, tapi globals()bisa dibilang jauh lebih buruk daripada eval, dan tidak benar-benar lebih buruk daripada sys.modules[__name__]AFAIK.
Laurence Gonsalves
2
Ya saya mengerti maksud Anda, karena Anda hanya mengambil darinya tanpa menetapkan apa pun
Greg
Bagaimana jika variabel digunakan dalam kelas dan beberapa instance untuk kelas yang dibuat secara bersamaan pada saat yang sama. Apakah ini akan menimbulkan masalah, karena ini adalah variabel global.
Alok Kumar Singh
98

Anda menginginkan kelas Baz, yang hidup dalam modul foo.bar. Dengan Python 2.7, Anda ingin menggunakan importlib.import_module(), karena ini akan membuat transisi ke Python 3 lebih mudah:

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

Dengan 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

Menggunakan:

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

Ini secara akurat menangani kelas gaya lama dan gaya baru.

Evan Fosmark
sumber
Anda tidak perlu mengetik. Jenis; itu hanya alias untuk "tipe" bawaan.
Glenn Maynard
1
Ya saya tahu, tetapi saya merasa itu lebih jelas untuk dibaca. Saya kira itu hanya bermuara pada preferensi.
Evan Fosmark
17

Saya telah melihat bagaimana django menangani ini

django.utils.module_loading memiliki ini

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

Anda bisa menggunakannya seperti import_string("module_path.to.all.the.way.to.your_class")

eugene
sumber
Ini jawaban yang bagus. Berikut ini tautan ke dokumen Django terbaru dengan implementasi saat ini .
Greg Kaleka
3

Ya, Anda bisa melakukan ini. Dengan asumsi kelas Anda ada di namespace global, sesuatu seperti ini akan melakukannya:

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
sumber
1
Di Python 3, tidak ada lagi ClassType.
riza
1
Gunakan isinstance (x, ketik).
Glenn Maynard
3

Dalam hal eksekusi kode arbitrer, atau nama yang diberikan pengguna yang tidak diinginkan, Anda dapat memiliki daftar nama fungsi / kelas yang dapat diterima, dan jika input cocok dengan satu dalam daftar, itu dievaluasi.

PS: Saya tahu .... agak terlambat .... tapi untuk siapa saja yang tersandung di masa depan.

JoryRFerrell
sumber
9
Jika Anda ingin mempertahankan daftar, lebih baik simpan dikt dari nama ke kelas saja. Maka tidak perlu untuk eval ().
Fasaxc
3

Menggunakan importlib bekerja paling baik untuk saya.

import importlib

importlib.import_module('accounting.views') 

Ini menggunakan notasi string dot untuk modul python yang ingin Anda impor.

Aaron Lelevier
sumber
3

Jika Anda benar-benar ingin mengambil kelas yang Anda buat dengan string, Anda harus menyimpan (atau menggunakan kata yang tepat, referensi ) dalam kamus. Setelah semua, itu juga akan memungkinkan untuk memberi nama kelas Anda di tingkat yang lebih tinggi dan menghindari mengekspos kelas yang tidak diinginkan.

Contoh, dari game di mana kelas aktor didefinisikan dengan Python dan Anda ingin menghindari kelas umum lainnya yang bisa dijangkau oleh input pengguna.

Pendekatan lain (seperti dalam contoh di bawah) akan membuat seluruh kelas baru, yang memegang di dictatas. Ini akan:

  • Izinkan beberapa pemegang kelas dibuat untuk organisasi yang lebih mudah (seperti, satu untuk kelas aktor dan satu lagi untuk jenis suara);
  • Buat modifikasi untuk pemegang dan kelas yang diadakan lebih mudah;
  • Dan Anda dapat menggunakan metode kelas untuk menambahkan kelas ke dikt. (Meskipun abstraksi di bawah ini tidak benar-benar diperlukan, itu hanya untuk ... "ilustrasi" ).

Contoh:

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

Ini mengembalikan saya:

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

Eksperimen menyenangkan lain yang harus dilakukan dengan itu adalah menambahkan metode yang mengasamkan ClassHoldersehingga Anda tidak pernah kehilangan semua kelas yang Anda lakukan: ^)

Gustavo6046
sumber