Python(CPython实现)函数可能包含多少个局部变量?

时间:2018-05-12 14:48:07

标签: python python-3.x function namespaces python-internals

我们已经知道函数参数曾经有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)的操作。

我们知道,基本上不需要动态地向函数添加变量,而您可以使用字典或者换句话说自定义命名空间。但是,测试函数中局部变量最大数量限制的正确方法是什么?

2 个答案:

答案 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。这与execeval等函数在其他情况下的约束相同 执行代码包含私有变量。

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是两个最重要的字节。