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(...),也能减少对具体类名的依赖。目前只有三个方法,保留显式定义同样合理;动态生成主要适合这次实验,或方法数量较多、模式稳定的场景。
备注:内容仅供参考。
