使用 exec 创建代码

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习 Python 中的 exec() 函数。该函数允许你动态执行以字符串形式表示的 Python 代码。这是一个强大的特性,使你能够在运行时生成并运行代码,让你的程序更具灵活性和适应性。

本实验的目标是学习 exec() 函数的基本用法,使用它动态创建类方法,并探究 Python 标准库在幕后是如何使用 exec() 的。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ControlFlowGroup -.-> python/for_loops("For Loops") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/build_in_functions("Build-in Functions") python/ModulesandPackagesGroup -.-> python/standard_libraries("Common Standard Libraries") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") subgraph Lab Skills python/for_loops -.-> lab-132512{{"使用 exec 创建代码"}} python/function_definition -.-> lab-132512{{"使用 exec 创建代码"}} python/build_in_functions -.-> lab-132512{{"使用 exec 创建代码"}} python/standard_libraries -.-> lab-132512{{"使用 exec 创建代码"}} python/classes_objects -.-> lab-132512{{"使用 exec 创建代码"}} python/constructor -.-> lab-132512{{"使用 exec 创建代码"}} end

理解 exec() 的基础知识

在 Python 中,exec() 函数是一个强大的工具,它允许你执行在运行时动态创建的代码。这意味着你可以根据特定的输入或配置动态生成代码,这在许多编程场景中非常有用。

让我们从探索 exec() 函数的基本用法开始。为此,我们将打开一个 Python 交互式环境。打开你的终端并输入 python3。这个命令将启动交互式 Python 解释器,你可以在其中直接运行 Python 代码。

python3

现在,我们将把一段 Python 代码定义为一个字符串,然后使用 exec() 函数来执行它。以下是具体操作方法:

>>> code = '''
for i in range(n):
    print(i, end=' ')
'''
>>> n = 10
>>> exec(code)
0 1 2 3 4 5 6 7 8 9

在这个示例中:

  1. 首先,我们定义了一个名为 code 的字符串。这个字符串包含一个 Python 的 for 循环。该循环设计为迭代 n 次,并打印每次迭代的数字。
  2. 然后,我们定义了一个变量 n 并将其赋值为 10。这个变量用作循环中 range() 函数的上限。
  3. 之后,我们调用 exec() 函数,并将 code 字符串作为参数传入。exec() 函数会将该字符串作为 Python 代码执行。
  4. 最后,循环运行并打印出从 0 到 9 的数字。

当我们使用 exec() 函数创建更复杂的代码结构(如函数或方法)时,它的真正强大之处就更加明显了。让我们尝试一个更高级的示例,在这个示例中,我们将为一个类动态创建一个 __init__() 方法。

>>> class Stock:
...     _fields = ('name', 'shares', 'price')
...
>>> argstr = ','.join(Stock._fields)
>>> code = f'def __init__(self, {argstr}):\n'
>>> for name in Stock._fields:
...     code += f'    self.{name} = {name}\n'
...
>>> print(code)
def __init__(self, name,shares,price):
    self.name = name
    self.shares = shares
    self.price = price

>>> locs = { }
>>> exec(code, locs)
>>> Stock.__init__ = locs['__init__']

>>> ## Now try the class
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s.price
490.1

在这个更复杂的示例中:

  1. 我们首先定义了一个 Stock 类,该类有一个 _fields 属性。这个属性是一个元组,包含了类的属性名称。
  2. 然后,我们创建了一个字符串,该字符串表示 __init__ 方法的 Python 代码。这个方法用于初始化对象的属性。
  3. 接下来,我们使用 exec() 函数来执行代码字符串。我们还将一个空字典 locs 传递给 exec()。执行结果得到的函数会存储在这个字典中。
  4. 之后,我们将字典中存储的函数赋值给 Stock 类的 __init__ 方法。
  5. 最后,我们创建了一个 Stock 类的实例,并通过访问对象的属性来验证 __init__ 方法是否正常工作。

这个示例展示了如何使用 exec() 函数根据运行时可用的数据动态创建方法。

创建动态的 __init__() 方法

现在,我们将把所学的关于 exec() 函数的知识应用到一个实际的编程场景中。在 Python 里,exec() 函数允许你执行存储在字符串中的 Python 代码。在这一步,我们将修改 Structure 类,以动态创建一个 __init__() 方法。__init__() 方法是 Python 类中的一个特殊方法,当类的对象被实例化时会调用该方法。我们将基于 _fields 类变量来创建这个方法,该变量包含了类的字段名列表。

首先,让我们看一下现有的 structure.py 文件。这个文件包含了 Structure 类的当前实现,以及一个继承自它的 Stock 类。要查看文件内容,在 WebIDE 中使用以下命令打开它:

cat /home/labex/project/structure.py

在输出中,你会看到当前的实现采用手动方式处理对象的初始化。这意味着初始化对象属性的代码是显式编写的,而不是动态生成的。

现在,我们要修改 Structure 类。我们将添加一个 create_init() 类方法,该方法将动态生成 __init__() 方法。要进行这些更改,在 WebIDE 编辑器中打开 structure.py 文件,并按照以下步骤操作:

  1. Structure 类中移除现有的 _init()set_fields() 方法。这些方法是手动初始化方式的一部分,由于我们要采用动态方式,所以不再需要它们。

  2. Structure 类添加 create_init() 类方法。以下是该方法的代码:

@classmethod
def create_init(cls):
    """Dynamically create an __init__ method based on _fields."""
    ## Create argument string from field names
    argstr = ','.join(cls._fields)

    ## Create the function code as a string
    code = f'def __init__(self, {argstr}):\n'
    for name in cls._fields:
        code += f'    self.{name} = {name}\n'

    ## Execute the code and get the generated function
    locs = {}
    exec(code, locs)

    ## Set the function as the __init__ method of the class
    setattr(cls, '__init__', locs['__init__'])

在这个方法中,我们首先创建一个字符串 argstr,它包含所有用逗号分隔的字段名。这个字符串将用作 __init__() 方法的参数列表。然后,我们将 __init__() 方法的代码创建为一个字符串。我们遍历字段名,并向代码中添加行,将每个参数赋值给相应的对象属性。之后,我们使用 exec() 函数执行代码,并将生成的函数存储在 locs 字典中。最后,我们使用 setattr() 函数将生成的函数设置为类的 __init__() 方法。

  1. 修改 Stock 类以使用这种新方法:
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

在这里,我们为 Stock 类定义 _fields,然后调用 create_init() 方法为 Stock 类生成 __init__() 方法。

你完整的 structure.py 文件现在应该类似于以下内容:

class Structure:
    ## Restrict attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"No attribute {name}")

    ## String representation for debugging
    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f"{type(self).__name__}({args})"

    @classmethod
    def create_init(cls):
        """Dynamically create an __init__ method based on _fields."""
        ## Create argument string from field names
        argstr = ','.join(cls._fields)

        ## Create the function code as a string
        code = f'def __init__(self, {argstr}):\n'
        for name in cls._fields:
            code += f'    self.{name} = {name}\n'

        ## Execute the code and get the generated function
        locs = {}
        exec(code, locs)

        ## Set the function as the __init__ method of the class
        setattr(cls, '__init__', locs['__init__'])

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

现在,让我们测试我们的实现,确保它能正常工作。我们将运行单元测试文件,检查所有测试是否都通过。使用以下命令:

cd /home/labex/project
python3 -m unittest test_structure.py

如果你的实现正确,你应该会看到所有测试都通过。这意味着动态生成的 __init__() 方法按预期工作。

你也可以在 Python 交互式环境中手动测试这个类。以下是具体操作方法:

>>> from structure import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s
Stock('GOOG', 100, 490.1)
>>> s.shares = 50
>>> s.share = 50  ## This should raise an AttributeError
Traceback (most recent call last):
  ...
AttributeError: No attribute share

在 Python 交互式环境中,我们首先从 structure.py 文件中导入 Stock 类。然后,我们创建一个 Stock 类的实例并打印它。我们还可以修改对象的 shares 属性。然而,当我们尝试设置一个不在 _fields 列表中的属性时,应该会得到一个 AttributeError

恭喜你!你已经成功使用 exec() 函数基于类属性动态创建了一个 __init__() 方法。这种方法可以让你的代码更具灵活性,也更易于维护,尤其是在处理具有可变数量属性的类时。

✨ 查看解决方案并练习

探究 Python 标准库如何使用 exec()

在 Python 中,标准库是一个强大的预编写代码集合,提供了各种有用的函数和模块。其中一个这样的函数就是 exec(),它可用于动态生成和执行 Python 代码。动态生成代码意味着在程序执行期间即时创建代码,而不是将其硬编码。

collections 模块中的 namedtuple 函数是标准库中使用 exec() 的一个著名示例。namedtuple 是一种特殊的元组,允许你通过属性名和索引来访问其元素。它是创建简单数据持有类的便捷工具,无需编写完整的类定义。

让我们来探究 namedtuple 是如何工作的,以及它在背后是如何使用 exec() 的。首先,打开你的 Python 交互式环境。你可以在终端中运行以下命令来实现。这个命令会启动一个 Python 解释器,你可以在其中直接运行 Python 代码:

python3

现在,让我们看看如何使用 namedtuple 函数。以下代码展示了如何创建一个 namedtuple 并访问其元素:

>>> from collections import namedtuple
>>> Stock = namedtuple('Stock', ['name', 'shares', 'price'])
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s[1]  ## namedtuples also support indexing
100

在上面的代码中,我们首先从 collections 模块导入 namedtuple 函数。然后,我们创建了一个名为 Stock 的新 namedtuple 类型,它有 namesharesprice 字段。我们创建了 Stock namedtuple 的一个实例 s,并通过属性名(s.names.shares)和索引(s[1])来访问其元素。

现在,让我们看看 namedtuple 是如何实现的。我们可以使用 inspect 模块来查看它的源代码。inspect 模块提供了几个有用的函数,用于获取关于实时对象(如模块、类、方法等)的信息。

>>> import inspect
>>> from collections import namedtuple
>>> print(inspect.getsource(namedtuple))

当你运行这段代码时,你会看到打印出大量的代码。如果你仔细观察,会发现 namedtuple 使用 exec() 函数来动态创建一个类。它所做的是构造一个包含类定义的 Python 代码的字符串。然后,它使用 exec() 将这个字符串作为 Python 代码执行。

这种方法非常强大,因为它允许 namedtuple 在运行时创建具有自定义字段名的类。字段名由你传递给 namedtuple 函数的参数决定。这是一个 exec() 如何用于动态生成代码的实际示例。

关于 namedtuple 的实现,有以下几个关键点需要注意:

  1. 它使用字符串格式化来构造类定义。字符串格式化是一种将值插入字符串模板的方法。在 namedtuple 的情况下,它使用这种方法来创建具有正确字段名的类定义。
  2. 它处理字段名的验证。这意味着它会检查你提供的字段名是否是有效的 Python 标识符。如果不是,它会抛出相应的错误。
  3. 它提供了额外的特性,如文档字符串(docstring)和方法。文档字符串是用于记录类或函数的用途和用法的字符串。namedtuple 为它创建的类添加了有用的文档字符串和方法。
  4. 它使用 exec() 执行生成的代码。这是将包含类定义的字符串转换为真正的 Python 类的核心步骤。

这种模式与我们在 create_init() 方法中实现的类似,但更复杂。namedtuple 的实现必须处理更复杂的场景和边缘情况,以提供一个健壮且用户友好的接口。

总结

在这个实验中,你学习了如何使用 Python 的 exec() 函数在运行时动态创建和执行代码。关键要点包括 exec() 执行基于字符串的代码片段的基本用法、基于属性动态创建类方法的高级用法,以及它在 Python 标准库中与 namedtuple 的实际应用。

动态生成代码的能力是一项强大的特性,它能让程序更具灵活性和适应性。尽管由于安全和可读性方面的考虑,使用时需要谨慎,但在特定场景下,如创建 API、实现装饰器或构建领域特定语言时,它对 Python 程序员来说是一个有价值的工具。当你创建能适应运行时条件的代码,或构建基于配置生成代码的框架时,就可以应用这些技术。