Kas*_*mvd 18 python namespaces function python-3.x python-internals
我们已经知道函数参数曾经有255个显式传递参数的限制.但是,此行为现在已更改,因为Python-3.7没有限制,除了sys.maxsize
实际上是python容器的限制.但是局部变量怎么样?
我们基本上不能以动态方式向函数添加局部变量和/或locals()
不允许直接更改字典,以便人们甚至可以用暴力方式测试它.但问题是,即使您更改locals()
使用compile
模块或exec
函数它也不会影响function.__code__.co_varnames
,因此,您无法在函数内显式访问变量.
In [142]: def bar():
...: exec('k=10')
...: print(f"locals: {locals()}")
...: print(k)
...: g = 100
...:
...:
In [143]: bar()
locals: {'k': 10}
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-143-226d01f48125> in <module>()
----> 1 bar()
<ipython-input-142-69d0ec0a7b24> in bar()
2 exec('k=10')
3 print(f"locals: {locals()}")
----> 4 print(k)
5 g = 100
6
NameError: name 'k' is not defined
In [144]: bar.__code__.co_varnames
Out[144]: ('g',)
Run Code Online (Sandbox Code Playgroud)
这意味着即使您使用如下for
循环:
for i in range(2**17):
exec(f'var_{i} = {i}')
Run Code Online (Sandbox Code Playgroud)
在locals()
将包含2级**17的变量,但你不能这样做print(var_100)
的函数内部.
我们知道,基本上没有必要动态地向函数添加变量,而您可以使用字典或者换句话说自定义命名空间.但是,测试函数中局部变量最大数量限制的正确方法是什么?
use*_*ica 12
2 ^ 32.LOAD_FAST
用于加载局部变量的op只有一个1字节或2字节的oparg,具体取决于Python版本,但是这可以并且将通过一个或多个op扩展到4个字节EXTENDED_ARG
,允许访问2 ^ 32个局部变量.你可以看到一些用于佣工EXTENDED_ARG
在Python/wordcode_helpers.h
.(请注意,操作码文档EXTENDED_ARG
在dis
文档尚未更新,以反映新的Python 3.6 wordcode结构.)
关于exec()
本地人及其与当地人的行为,这里已经存在一个公开的争论:执行官如何与当地人合作?.
关于这个问题,似乎几乎不可能通过动态地将变量添加到与函数共享的本地命名空间来测试它__code__.co_varnames
.原因是这仅限于一起进行字节编译的代码.这与在其他情况下(如执行代码包含私有变量)的功能相同exec
且eval
受限制的行为相同.
In [154]: class Foo:
...: def __init__(self):
...: __private_var = 100
...: exec("print(__private_var)")
In [155]: f = Foo()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-155-79a961337674> in <module>()
----> 1 f = Foo()
<ipython-input-154-278c481fbd6e> in __init__(self)
2 def __init__(self):
3 __private_var = 100
----> 4 exec("print(__private_var)")
5
6
<string> in <module>()
NameError: name '__private_var' is not defined
Run Code Online (Sandbox Code Playgroud)
有关详细信息,请阅读/sf/answers/3444593071/.
但是,这并不意味着我们无法在理论上找到限制.通过分析python将局部变量存储在内存中的方式.
我们这样做的方法是首先查看函数的字节码,并查看各个指令如何存储在内存中.这dis
是一个很好的工具,用于反汇编Python代码,以防万一我们可以反汇编一个简单的函数如下:
>>> # VERSIONS BEFORE PYTHON-3.6
>>> import dis
>>>
>>> def foo():
... a = 10
...
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (10)
3 STORE_FAST 0 (a)
6 LOAD_CONST 0 (None)
9 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
这里最左边的数字是存储代码的行数.之后的数字列是字节码中每条指令的偏移量.
该STOR_FAST
操作码存储TOS(栈顶)到本地co_varnames[var_num]
.由于其偏移量与下一个操作码的差异为3(6 - 3),这意味着每个STOR_FAST
操作码仅占用内存的3个字节.第一个字节是存储操作或字节码; 后两个字节是该字节代码的操作数,这意味着有2 ^ 16种可能的组合.
因此,在一个byte_compile中,理论上一个函数只能有65536个局部变量.
在Python-3.6之后,Python解释器现在使用16位字码而不是字节码.实际上,通过使参数仅占用1个字节,将指令对齐为始终为2个字节而不是1或3.
因此,如果您在更高版本中进行反汇编,您将获得以下结果,该结果仍然使用两个字节用于STORE_FAST:
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (10)
2 STORE_FAST 0 (a)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
但是,@ Alex Hall在评论中表示,您可以exec
使用超过2 ^ 16个变量的整个函数,这些变量也可以使用它们__code__.co_varnames
.但是,这并不意味着测试假设几乎是可行的(因为如果你试图用超过20的功率进行测试,那么它会越来越频繁地进行测试).但是,这是代码:
In [23]: code = '''
...: def foo():
...: %s
...: print('sum:', sum(locals().values()))
...: print('add:', var_100 + var_200)
...:
...: ''' % '\n'.join(f' var_{i} = {i}'
...: for i in range(2**17))
...:
...:
...:
In [24]: foo()
sum: 549755289600
add: 300
In [25]: len(foo.__code__.co_varnames)
Out[25]: 1048576
Run Code Online (Sandbox Code Playgroud)
这意味着虽然STORE_FAST
使用2个字节来保留TOS并且"理论上"不能保留超过2 ^ 16个不同的变量,但是应该有一些其他唯一标识符,如偏移号或额外空间,这样可以保留更多比2 ^ 16.而事实证明它EXTENDED_ARG
是因为它在文档中提到的真实前缀它具有一个参数过大,以适应到默认的两个字节的任何操作码.因此它是2 ^ 16 + 16 = 2 ^ 32.
EXTENDED_ARG(EXT)
前缀任何参数太大而不适合默认的两个字节的操作码.ext保存另外两个字节,它们与后续操作码的参数一起构成一个四字节的参数,ext是两个最重要的字节.