在动态创建类时,使用`exec`相对于`type()`有什么优势?

9
我希望能在Python中动态地创建类。
例如,我想要复制下面的代码:
>>> class RefObj(object):
...     def __init__(self, ParentClassName):
...         print "Created RefObj with ties to %s" % ParentClassName
... class Foo1(object):
...     ref_obj = RefObj("Foo1")
... class Foo2(object):
...     ref_obj = RefObj("Foo2")
... 
Created RefObj with ties to Foo1
Created RefObj with ties to Foo2
>>>

...但我希望Foo1、Foo2、Foo类能够动态创建(即在执行期间而不是在第一次编译时创建)。

实现这个的一种方法是使用type(),像下面这样:

>>> class RefObj(object):
...     def __init__(self, ParentClassName):
...         print "Created RefObj with ties to %s" % ParentClassName
... def make_foo_class(index):
...     name = "Foo%s" % index
...     return type(name, (object, ), dict(ref_obj = RefObj(name)))
... 
>>> Foo1 = make_foo_class(1)
Created RefObj with ties to Foo1
>>> Foo2 = make_foo_class(2)
Created RefObj with ties to Foo2
>>> type(Foo1()), type(Foo2())
(<class 'Foo1'>, <class 'Foo2'>)

我也可以使用exec来实现,代码如下:

>>> class RefObj(object):
...     def __init__(self, ParentClassName):
...         print "Created RefObj with ties to %s" % ParentClassName
... def make_foo_object(index):
...     class_template = """class Foo%(index)d(object):
...         ref_obj = RefObj("Foo%(index)d")
...         """ % dict(index = index)
...     global RefObj
...     namespace = dict(RefObj = RefObj)
...     exec class_template in namespace
...     return namespace["Foo%d" % index]
... 
>>> Foo1 = make_foo_object(1)
Created RefObj with ties to Foo1
>>> Foo2 = make_foo_object(2)
Created RefObj with ties to Foo2
>>> type(Foo1()), type(Foo2())
(<class 'Foo1'>, <class 'Foo2'>)

使用exec的方式让我感到不太舒服(我想很多人阅读这个问题时也是这样想的),但是exec恰好是Python的collections.namedtuple()实现的方法(见此行),非常相关的还有该类的创作者Raymond Hettinger在这里对使用exec进行的辩护。在这篇辩护文章中,他说:“命名元组的一个关键特性就是它们与手写类完全相同”,这可能意味着使用type()并不如使用exec好... 有什么区别吗? 为什么要使用exec而不是type() 我想答案可能是两种方法是一样的,只是namedtuple实现有很多命名元组变量,用动态生成闭包来生成所有方法让代码变得难以操作,但我想知道是否还有其他原因。
关于我对exec的不适感,我确实意识到如果没有任何方式让不受信任的方注入恶意代码,那么使用它应该是安全的...只是要确保这一点让我感到紧张。

1
在http://blog.ccpgames.com/kristjan/2011/05/28/namedtuple-and-exec/上有一些关于这个问题的好讨论,还有另一篇我现在找不到的好博客文章。你没有提出任何`type`在你的情况下存在问题的理由,所以我不知道为什么你要费心使用`exec`,因为你知道`type`可以工作。(除了好奇心显然)。 - agf
@agf - 非常好的链接,谢谢!我最初并没有对 type 存在问题,因为两种方法都可以工作。我只是对差异感到好奇,并试图理解 namedtuple 中使用 exec 的原因。然而,提供的类/函数签名参数非常棒... 我经常遇到装饰器问题,需要使用 decorator 包。 - Russ
你能解释一下动态类创建的原因吗,还是只是出于好奇? - dhill
2
从模板生成代码是一种基本而强大的技术。像ANTLR和模板语言这样的工具很好地利用了这种技术。namedtuple的早期版本使用type而不是exec,但这种方法无法很好地扩展——很难维护,有些难以阅读,存在奇怪的错误,并且不是自我记录的。经过一些清理,exec版本最终变得相当美观:http://hg.python.org/cpython/file/3.2/Lib/collections.py#l235 我承认exec让一些开发人员感到不舒服,但这是心理问题,而不是技术问题。 - Raymond Hettinger
3个回答

7

我建议在这里使用type而不是exec

实际上,class语句只是对type的调用的语法糖:类体在自己的命名空间中执行,然后传递给元类,默认情况下,如果没有指定自定义元类,则为type

这种方法不会在运行时解析代码,因此错误率较低,甚至可能更快。


4
为什么不直接在函数中创建一个类?
def foo_factory(index):
    name = 'Foo%d' % index

    class Foo(object):
        ref_obj = RefObj(name)

    Foo.__name__ = name
    return Foo

Foo中实际上有一些动态内容时,您不希望每次都基本相同地创建它。 - agf
请展示如何实现namedtuple或保留签名的装饰器。说这"绝不会使灵活性减少"是不正确的。 - agf
OP并不是在询问如何做到这一点,而是在询问为什么这两种不同的方法是不同的。 - brandizzi
好的,我已经删除了“不太灵活”的句子,但并不是因为namedtuple不能以这种方式实现,而是因为根据实际问题,其他结构可能更适合。 - yak
@brandizzi,我只是想展示另一个选项,这可能是OP没有考虑过的。 - yak
@yak - 谢谢您的建议。事实上,我实际上正在使用这样的选项(我同意它更易读)。然而,我提出问题的主要目的是为了尝试理解在此目的下使用 typeexec 是否有任何区别。 - Russ

2

使用 type() 而不是 exec() 并没有任何劣势。我认为 Raymond 的辩护有点过于防御性了。你必须选择最易读和最易懂的技术。这两种方式都会创建混乱的代码。

首先,你应该尽力避免创建类的代码,这会是最好的选择。


@agfL 抱歉,我不确定你指的是什么。 - Ned Batchelder
3
decorator 库中使用 exec 的一个论点是它更容易保留被装饰函数/方法的签名。我认为这也是 namedtuple 的一个论点,使用 exec 构建的类与手写代码构建的类完全相同,而使用 type 则不同。 - agf
我肯定想避免在代码运行时动态创建类,但是我有一个情况,在这种情况下,我需要根据无法保证在类创建之前处于作用域内的参数创建类属性。目前看来,动态类似乎是最简单的方法。 - Russ
@Russ 或许你可以事后设置类属性? - Ned Batchelder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接