แปลงสตริงเป็นวัตถุคลาส Python หรือไม่


187

รับสตริงเป็นอินพุตของผู้ใช้ไปยังฟังก์ชั่น Python ฉันต้องการเอาคลาสอ็อบเจกต์ออกมาถ้ามีคลาสที่มีชื่อนั้นในเนมสเปซที่กำหนดในปัจจุบัน โดยพื้นฐานแล้วฉันต้องการการนำไปใช้งานสำหรับฟังก์ชั่นที่จะให้ผลลัพธ์แบบนี้:

class Foo:
    pass

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

เป็นไปได้ไหม?

python 

2
มีคำตอบที่เป็นประโยชน์อยู่ที่นี่ แต่ฉันพบคำตอบสำหรับคำถามนี้มีประโยชน์อย่างยิ่ง: stackoverflow.com/questions/452969/…
Tom

คำตอบ:


115

คำเตือน : eval()สามารถใช้เพื่อเรียกใช้งานโค้ด Python โดยพลการ คุณไม่ควรใช้eval()กับสตริงที่ไม่น่าเชื่อถือ (ดูSecurity of Python eval () สำหรับสตริงที่ไม่น่าเชื่อถือ? )

ดูเหมือนง่ายที่สุด

>>> class Foo(object):
...     pass
... 
>>> eval("Foo")
<class '__main__.Foo'>

30
จงระวังการแตกกิ่งก้านของการใช้ eval คุณควรตรวจสอบให้แน่ใจว่าสตริงที่คุณส่งนั้นไม่ได้มาจากผู้ใช้
โอเวอร์คล็อก

18
การใช้ eval () เปิดประตูไว้สำหรับการใช้รหัสโดยอำเภอใจเพื่อความปลอดภัยควรหลีกเลี่ยง
m.kocikowski

54
Eval ไม่ได้เปิดประตูทิ้งไว้เพื่ออะไร หากคุณมีผู้ใช้ที่ประสงค์ร้ายและชั่วร้ายซึ่งอาจเป็นอันตรายและมีเจตนาส่งผ่านค่าที่ไม่ดีให้เป็นหลักฐานพวกเขาก็สามารถแก้ไขแหล่งไพ ธ อนได้ เนื่องจากพวกเขาสามารถแก้ไขแหล่งไพ ธ อนได้ประตูจึงเป็นและจะเปิดตลอดเวลา
S.Lott

34
ยกเว้นว่าโปรแกรมที่เป็นปัญหากำลังทำงานอยู่บนเซิร์ฟเวอร์
Vatsu1

12
@ s-lott ฉันขอโทษ แต่ฉันคิดว่าคุณกำลังให้คำแนะนำที่ไม่ดีที่นี่ ควรพิจารณา: เมื่อคอมพิวเตอร์ของคุณให้คุณใส่รหัสผ่านผู้ใช้ที่ชั่วร้ายผู้ประสงค์ร้ายอาจปรับเปลี่ยนการปฏิบัติที่สอดคล้องกันหรือไลบรารีและหลีกเลี่ยงการตรวจสอบนี้ ดังนั้นหนึ่งอาจยืนยันว่าไม่มีประโยชน์ที่จะเพิ่มการตรวจสอบรหัสผ่านในสถานที่แรก หรือมันคืออะไร?
Daniel Sk

148

สิ่งนี้สามารถใช้งานได้:

import sys

def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)

จะเกิดอะไรขึ้นถ้าไม่มีคลาส?
riza

7
มันจะทำงานเฉพาะกับคลาสที่กำหนดไว้ในโมดูลปัจจุบัน
24490

1
ทางออกที่ดีจริงๆ: ที่นี่คุณไม่ได้ใช้ การนำเข้าแต่เป็นโมดูล sys ทั่วไป
Stefano Falsetto

สิ่งนี้จะแตกต่างจากนี้def str_to_class(str): return vars()[str]หรือไม่?
Arne

113

คุณสามารถทำสิ่งที่ชอบ:

globals()[class_name]

6
ฉันชอบสิ่งนี้ดีกว่าโซลูชัน 'Eval' เพราะ (1) มันง่ายเหมือนและ (2) มันไม่ได้ทำให้คุณเปิดการใช้รหัสโดยอำเภอใจ
mjumbewu

2
แต่คุณกำลังใช้globals()... สิ่งอื่นที่ควรหลีกเลี่ยง
Greg

5
@ Greg บางที แต่globals()เนื้อหานั้นแย่กว่าevalและไม่แย่ไปกว่าsys.modules[__name__]AFAIK
Laurence Gonsalves

2
ใช่ฉันเห็นจุดของคุณเพราะคุณเพียงแค่คว้าจากมันไม่ได้ตั้งค่าอะไร
เกร็ก

จะเกิดอะไรขึ้นถ้าตัวแปรถูกใช้ในคลาสและหลายอินสแตนซ์สำหรับคลาสนั้นถูกสร้างขึ้นพร้อมกันในเวลาเดียวกัน มันจะทำให้เกิดปัญหาเช่นนี้เป็นตัวแปรทั่วโลก
Alok Kumar Singh

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

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; มันเป็นเพียงนามแฝงสำหรับ "ชนิด" ในตัว
Glenn Maynard

1
ใช่ฉันรู้ แต่ฉันรู้สึกว่ามันชัดเจนกว่าที่จะอ่าน ฉันเดาว่ามันจะทำให้คุณพึงพอใจมากขึ้น
Evan Fosmark

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>

1
ใน Python 3 ไม่มี ClassType มากขึ้น
riza

1
ใช้ isinstance (x, type)
Glenn Maynard

3

ในแง่ของการใช้รหัสโดยอำเภอใจหรือชื่อผู้ใช้ที่ไม่ต้องการผ่านคุณอาจมีรายการชื่อฟังก์ชั่น / คลาสที่ยอมรับได้และถ้าอินพุตตรงกับหนึ่งในรายการมันจะถูกประเมิน

PS: ฉันรู้ว่า .... สายเกินไป .... แต่สำหรับคนอื่นที่เจอสิ่งนี้ในอนาคต


9
หากคุณจะรักษารายการไว้เช่นกันอาจบำรุงรักษา dict จากชื่อไปยังคลาสแทน จากนั้นไม่จำเป็นต้องใช้ eval ()
Fasaxc

3

การใช้importlibทำงานได้ดีที่สุดสำหรับฉัน

import importlib

importlib.import_module('accounting.views') 

สิ่งนี้ใช้รูปแบบจุดสตริงสำหรับโมดูลหลามที่คุณต้องการนำเข้า


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ดังนั้นคุณจะไม่สูญเสียคลาสทั้งหมดที่คุณทำ: ^)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.