exec与eval向传入的执行环境添加__builtins__的机制是什么?
exec/eval执行环境调用行为问题
问题背景
为了理解eval和exec对传入执行环境(globals、locals参数)的处理逻辑,实现了一个可以记录方法调用的LogDict类,其行为和普通字典一致,但是会记录除__new__外的绝大多数方法调用:
from functools import wraps class LogDict(dict): logs = {} def _make_wrapper(name): @wraps(getattr(dict, name)) def wrapper(self, *args, **kwargs): LogDict.logs.setdefault(id(self), []).append({ 'name': name, 'args': tuple(map(repr, args)), 'kwargs': dict((key, repr(kwargs[key])) for key in kwargs) }) return getattr(super(), name)(*args, **kwargs) return wrapper for attr in dir(dict): if callable(getattr(dict, attr)) and attr not in {'__new__',}: locals()[attr] = _make_wrapper(attr) def logrepr(self): return ''.join( "{fun}({rargs}{optsep}{rkwargs})\n".format( fun = logitem['name'], rargs = ', '.join(logitem['args']), optsep = ', ' if len(logitem['kwargs'])>0 else '', rkwargs = ', '.join('{} = {}'\ .format(key, logitem['kwargs'][key]) for key in logitem['kwargs']) ) for logitem in LogDict.logs[id(self)])
基础功能验证
运行如下测试代码:
d = LogDict() d['1'] = 3 d['1'] += .5 print('1' in d) print('log:') print(d.logrepr())
得到输出符合预期:
True log: __init__() __setitem__('1', 3) __getitem__('1') __setitem__('1', 3.5) __contains__('1') __getattribute__('logrepr')
异常现象
将LogDict实例作为执行环境传入exec观察调用行为时,日志中仅能看到查找print的__getitem__调用,但最终字典中出现了__builtins__键,没有任何相关操作被记录:
测试代码:
print('\tTesting exec(smth, logdict):') d = LogDict() exec('print("this line is inside the exec statement")', d) print('the log is:') print(d.logrepr(), end='') print('the env now contains:') print(d)
输出结果:
Testing exec(smth, logdict): this line is inside the exec statement the log is: __init__() __getitem__('print') __getattribute__('logrepr') the env now contains: [a dictionary containing __builtins__]
问题解答
你没有遗漏方法的记录,__builtins__的检查和注入是CPython解释器通过底层C API直接操作字典内部存储完成的,没有走Python层面重载的魔术方法,所以不会被LogDict捕获。
具体逻辑如下:
- 当你给
exec传入自定义globals参数时,解释器会首先检查该字典中是否存在__builtins__键,这个检查调用的是C层面的PyDict_GetItem接口,直接访问字典的底层哈希表,不会触发Python类中重载的__getitem__方法。 - 如果未检测到
__builtins__,解释器会通过C层面的PyDict_SetItem接口直接将内置命名空间注入到globals的底层存储中,同样不会触发Python类中重载的__setitem__方法。 - 这种实现是解释器的性能优化手段,普通场景下使用原生
dict作为执行环境时不会感知到差异,只有当你继承dict重载了相关魔术方法时才会出现方法调用未被捕获的情况。
内容的提问来源于stack exchange,提问作者sortai
相关产品推荐
相关产品推荐

