PAYATHON 2026

Python 如何用 exec() 动态生成类方法并解决 SyntaxError?

数据阿明

可以使用 exec() 动态生成这些类方法。你提供的 traceback 中实际调用的是 eval(),而 def 是语句,不能用 eval() 执行。此外,还需要处理生成代码的缩进和 __get_social 的名称改写。

先区分 eval() 和 exec()

eval() 用于求值表达式,例如 1 + 2;exec() 可以执行包含函数定义的代码:

exec("def example():\n    return 42")

先检查实际运行的文件,确认调用已从 eval(...) 改为 exec(...)。问题中的代码片段与 traceback 不一致,只调整缩进无法解决 eval() 执行 def 的错误。

字符串中的 def 应从第 0 列开始

传给 exec() 的字符串会作为独立代码编译。即使 exec() 写在类内部,字符串也不需要沿用外层类的缩进。

例如:

source = (
    'def get_website(self):\n'
    '    return self._Parser__get_social("website")\n'
)

这里 def 前没有空格,函数体缩进四个空格。给顶层 def 添加四个或八个空格,会产生意外缩进错误。

保留双下划线方法时的完整写法

下面的示例保留原来的解析逻辑,在类定义过程中生成三个方法:

import bs4


class Parser:
    def __init__(self, html):
        self.html = html
        self.soup = bs4.BeautifulSoup(html, "html.parser")

    def __get_socials(self):
        socials = self.soup.select("div.external-links")
        assert 1 == len(socials)
        return socials[0]

    def __get_social(self, aria_label):
        social_tags = self.__get_socials()
        social_results = social_tags.select(
            f"a[aria-label='{aria_label}']"
        )
        if 0 == len(social_results):
            return

        assert 1 == len(social_results)
        return social_results[0].get("href")

    for stub in ["website", "instagram", "facebook"]:
        exec(
            f"def get_{stub}(self):\n"
            f"    return self._Parser__get_social({stub!r})\n"
        )

    del stub

在类体中直接执行这段 exec(),定义的函数会进入当前类的命名空间。通过实例访问时,它们会像普通实例方法一样绑定 self:

parser = Parser(html)
website = parser.get_website()
instagram = parser.get_instagram()
facebook = parser.get_facebook()

这里假设 html 已赋值,且包含原代码要求的唯一一个 div.external-links 元素。

为什么要写 _Parser__get_social?

Python 编译类定义时,会将类内部的 __get_social 改写为 _Parser__get_social。

传给 exec() 的字符串会单独编译,即使调用位置在 Parser 类体内,也不会自动应用该类的名称改写规则。因此,动态生成的函数若直接访问 self.__get_social,调用时通常会出现 AttributeError。

示例使用 _Parser__get_social 来匹配原方法的实际名称。这个名称包含类名,所以修改类名时也需要同步调整。

你的 traceback 中还有 class Parser(parser.Parser)。如果私有方法实际定义在基类中,名称前缀由定义该方法的基类名称决定,而不是当前子类的名称。需要结合实际类结构确认。

lambda 的语法和循环写法

lambda 的冒号后只能放表达式,不能写 return。如果只想在类定义阶段批量生成方法,可以把下面的代码放进原来的 Parser 类体,替换三个显式方法:

    for stub in ["website", "instagram", "facebook"]:
        locals()[f"get_{stub}"] = (
            lambda self, label=stub: self.__get_social(label)
        )

    del stub

这里有两个细节:

  • label=stub 在每次创建函数时保存当前标签,避免所有方法在调用时都读取循环结束后的同一个变量。
  • 这段 lambda 直接写在 Parser 类定义中,编译时会对 self.__get_social 执行名称改写。

生成的方法会多一个可选的 label 参数。如果方法签名必须严格保持只有 self,可以继续使用前面的 exec() 示例,也可以使用函数工厂。

原来的 self.get_instagram = lambda self: ... 也有绑定问题:普通函数直接赋给实例属性时,不会自动把实例绑定为第一个参数。另外,在类体中定义方法时,没有可直接使用的实例变量 self。

使用时需要注意什么?

示例只为三个固定标签生成代码,方法名和参数都来自代码中的常量。exec() 能够执行任意 Python 代码,不要把未经检查的外部输入拼进去。

把 __get_social 改名为 _get_social 后,动态代码就可以直接调用 self._get_social(...),也能减少对具体类名的依赖。目前只有三个方法,保留显式定义同样合理;动态生成主要适合这次实验,或方法数量较多、模式稳定的场景。

备注:内容仅供参考。