exec 和 eval 如何将 __builtins__ 添加到给定环境?

sor*_*tai 3 python eval python-3.x

我试图了解 eval 和 exec 如何处理它们所给的环境(全局变量和局部变量),因此我创建了一个类“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)])
Run Code Online (Sandbox Code Playgroud)

例如,这段代码:

d = LogDict()
d['1'] = 3
d['1'] += .5
print('1' in d)
print('log:')
print(d.logrepr())
Run Code Online (Sandbox Code Playgroud)

产生这个输出:

True
log:
__init__()
__setitem__('1', 3)
__getitem__('1')
__setitem__('1', 3.5)
__contains__('1')
__getattribute__('logrepr')
Run Code Online (Sandbox Code Playgroud)

我尝试将其提供给 exec 以了解它的使用方式,但我看不到它访问字典的意义超出了意义:

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)
Run Code Online (Sandbox Code Playgroud)
    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__]
Run Code Online (Sandbox Code Playgroud)

所以 exec 函数没有调用我正在记录的任何方法,除了 __getitem__ 来查看其中是否有“print”(稍后当我打印日志时调用 __getattribute__ );它是如何设置键“__builtins__”(或检查它是否尚未定义)?我只是错过了它正在使用的方法,还是它在做一些更底层的事情?

Blc*_*ght 6

exec函数使用 Python C API 中的低级字典函数将__builtins__模块插入到全局命名空间字典中。您可以在 CPython 源代码中看到该调用。

因为调用的是低级 dict API,所以它不会在您的类中查找您重写的__setitem__方法,它只是直接写入底层字典存储。该exec函数要求传入它的全局命名空间是一个dict(或dict子类,但不是其他映射类型),因此这始终是安全的,至少不会导致解释器崩溃。但它确实绕过了你的日志记录。

不幸的是,我没有看到任何添加日志记录的方法,以便您可以看到__builtins__添加到全局命名空间。这可能意味着您直接观察exec的行为的尝试注定要失败。但如果您只是想了解它的作用,那么阅读 C 源代码也许是一个合适的选择。使用开源编程语言的好处之一是,当您遇到此类问题时,您可以查看解释器是如何编程的。它确实需要阅读 C,而不仅仅是 Python,但该builtin_exec_impl函数足够简单(实际的代码执行发生在其他地方,并且肯定要复杂得多)。