我们已经知道函数参数曾经有limit of 255 explicitly passed arguments。但是,此行为现在已更改,因为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',)
这意味着即使您使用for
循环,例如:
for i in range(2**17):
exec(f'var_{i} = {i}')
locals()
将包含2 ** 17个变量,但您无法在函数内执行类似print(var_100)
的操作。
我们知道,基本上不需要动态地向函数添加变量,而您可以使用字典或者换句话说自定义命名空间。但是,测试函数中局部变量最大数量限制的正确方法是什么?
答案 0 :(得分:12)
2 ^ 32。用于加载局部变量的LOAD_FAST
op仅具有1字节或2字节的oparg,具体取决于Python版本,但这可以并且将通过一个或多个EXTENDED_ARG
操作扩展到4个字节,允许访问2 ^ 32个局部变量。您可以在Python/wordcode_helpers.h
中看到EXTENDED_ARG
使用的一些帮助程序。 (请注意,EXTENDED_ARG
文档中dis
的操作码文档尚未更新,以反映新的Python 3.6字码结构。)
答案 1 :(得分:6)
关于exec()
及其与本地人的行为,这里已经有一个公开辩论:How does exec work with locals?。
关于这个问题,似乎几乎不可能通过动态地将变量添加到与函数__code__.co_varnames
共享的本地命名空间来测试它。原因是this is restricted to code that is byte-compiled together。这与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
阅读https://stackoverflow.com/a/49208472/2867928了解详情。
然而,这并不意味着我们无法在理论上找到限制。通过分析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
这里最左边的数字是存储代码的行数。之后的数字列是字节码中每条指令的偏移量。
STOR_FAST
操作码将TOS(堆栈顶部)存储到本地co_varnames[var_num]
。由于其偏移量与下一个操作码的差异为3(6 - 3),这意味着每个STOR_FAST
操作码仅占用内存的3个字节。第一个字节是存储操作或字节码;后两个字节是该字节代码的操作数,这意味着有 2 ^ 16 可能的组合。
因此,在一个byte_compile中,理论上一个函数只能有 65536 局部变量。
Python-3.6之后the Python interpreter now uses a 16-bit wordcode instead of bytecode.实际上是aligning the instructions to always be 2 bytes rather than 1 or 3 by having arguments only take up 1 byte.
因此,如果您在更高版本中进行反汇编,您将获得以下结果,该结果仍然使用两个字节用于STORE_FAST。:
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (10)
2 STORE_FAST 0 (a)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
然而,@ Alex Hall在评论中表示你可以exec
一个包含超过 2 ^ 16 变量的整个函数,这些变量使它们在{ {1}}。但是,这并不意味着测试假设实际上是可行的(因为如果你试图用超过20的功率进行测试,那么它会越来越耗费时间)。但是,这是代码:
__code__.co_varnames
这意味着尽管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
使用2个字节来保留TOS并且“理论上”不能保留超过2个 ^ 16 个不同的变量,但应该有一些其他唯一标识符,比如偏移数,或额外的空间,可以保留超过2 ^ 16 。而且as it turned out它是EXTENDED_ARG
正如文档中提到的那样,它为任何操作码添加前缀,该操作码的参数太大而不能适应默认的两个字节。因此它是2 ^ 16 + 16 = 2 ^ 32 。
EXTENDED_ARG(分机)¶
前缀任何参数太大而不适合默认的两个字节的操作码。 ext保存了两个额外的字节 连同后续操作码的参数,包含一个四字节 参数,ext是两个最重要的字节。