如何使用 exec() 函数正确创建 class 的实例?

How to correctly create an instance of a class with exec() function?

我正在使用 Python 中的 TKinter 库制作 GUI。我希望用户从组合框中 select 一个选项,然后按下一个按钮,它应该创建一个名为 selected 选项的 class 实例。为了节省代码,我决定这样使用 exec() 函数: exec('instance = ' + comboExample.get() + '()')。 这将启动 class 的 __init__() 方法,但是当我尝试使用 instance.method() 调用其他方法(在本例中来自继承的 class)时,它显示以下错误:NameError: name 'instance' is not defined。这里有一个脚本示例:

from tkinter import *
from tkinter import ttk

master = Tk()

#Create classes
class Base():
    def method(self):
        self.label = Label(master, text = self.sentence)
        self.label.pack()

class Example1(Base):
    def __init__(self):
        print('Example1 created')
        self.sentence = 'This is example 1.'

class Example2(Base):
    def __init__(self):
        print('Example2 created')
        self.sentence = 'This is example 2'

#Create Combobox and Button
combo = ttk.Combobox(master, state = 'readonly')
combo['values'] = ['Example1', 'Example2']
combo.pack()

def callback():
    exec('instance = ' + combo.get() + '()')
    #Here is the error
    instance.method()

button = Button(master, command = callback, text = 'Button')
button.pack()

master.mainloop()

我现在不知道为什么,但是当我尝试使用以下代码时它可以正常工作:

class Example():
    def __init__(self):
        self.text = 'This is an example'
    def add_text(self):
        print(self.text)

exec('instance = Example()')
instance.add_text()

目前,我只找到了一个解决方案,即不使用 exec(),但比使用它更让我浪费代码,尤其是如果我想创建很多 class 就像 Example1Example2。都和之前的大脚本一样,只是改了callback()函数:

def callback():
    if combo.get() == 'Example1':
        instance = Example1()
    if combo.get() == 'Example2':
        instance = Example2()
    instance.method()

就是这样。我在 Python 才开始编程才 2 个月前,我也是 Whosebug 的新手,所以如果我在解释或其他方面犯了一些错误,请告诉我,我会改正。 谢谢你的时间。任何帮助将不胜感激。

您不应将 exec 用于此目的。 exec 是一个强大的工具,但它不适合这项工作。

一种更简单的方法是创建从用户输入到 类 的映射。然后,您可以将该映射同时用于组合框和回调。

示例:

...
mapping = {"Example1": Example1, "Example2": Example2}

#Create Combobox and Button
combo = ttk.Combobox(master, state = 'readonly')
combo['values'] = sorted(mapping.keys())
combo.pack()

def callback():
    class_name = combo.get()
    cls = mapping[class_name]
    instance = cls()
    instance.method()
...

您甚至可以通过遍历 类 的列表来自动生成映射,尽管对于这个例子来说,这似乎有点过分了。

问题不在于你的语法;那是你想做一些非法的事情。您不能使用 exec 创建新的局部变量。 (函数外的相同代码起作用的原因是通常您 可以 使用 exec 创建一个新的全局变量,但这仍然是一个坏主意。)

但您也不需要那样做。在Python中,一切都是对象,包括classes。因此,您只需要从名称中获取 class。然后,您可以创建 class 的实例并将其存储在局部变量中,方法是使用与静态实例化 class 相同的常规语法并将其存储在局部变量中。


正确的方法是存储一个映射名称到 class 对象的字典。如果你想变得聪明,你可以写一个装饰器,用那个字典注册 classes,但如果你觉得这听起来像希腊语,那就明确地做:

classes = {'Spam': Spam, 'Eggs': Eggs}

如果你有几十个这样的,你可以通过这样的理解来避免重复:

from your_module import Spam, Eggs
classes = {cls.__name__: cls for cls in (Spam, Eggs)}

…但到那时你可能最好学习如何编写装饰器。

无论哪种方式,您都可以用该字典的键填充您的组合框,而不是在 combo['values'] 行中重复自己。

然后,要创建一个实例,您只需这样做:

cls = classes[comboExample.get()]
instance = cls()

(显然你可以将其折叠成一行,但我认为如果我们将两部分分开会更容易理解。)


如果你真的想以一种 hacky 的方式做到这一点,你可以。您在此模块中创建的每个 class 都已按名称存储在字典中——模块的全局命名空间。这与您尝试使用 exec 隐含地找到它的位置相同,但您可以通过在 globals() 中查找来明确地找到它。然而,全局命名空间还包含所有函数、导入模块、顶级常量和变量等的名称,所以这通常不是一个好主意。 (显然,exec 有完全相同的问题。)