【问题标题】:How many local variables can a Python (CPython implementation) function possibly hold?Python(CPython 实现)函数可以容纳多少个局部变量?
【发布时间】:2018-10-22 18:22:51
【问题描述】:

我们已经知道函数参数曾经有limit of 255 explicitly passed arguments。然而,这种行为现在改变了,因为 Python-3.7 没有限制,除了 sys.maxsize 这实际上是 python 容器的限制。但是局部变量呢?

我们基本上不能以动态方式向函数添加局部变量和/或直接更改locals() 字典是不允许的,因此甚至可以以蛮力的方式对其进行测试。但问题是,即使您使用compile 模块或exec 函数更改locals(),它也不会影响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) 的操作。

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

【问题讨论】:

  • 请注意,这个问题指的是 CPython(参考实现),而不是 Python(规范)。
  • @Acorn 确实,刚刚更新。

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


【解决方案1】:

2^32。用于加载局部变量的LOAD_FAST 操作只有一个 1 字节或 2 字节的操作参数,具体取决于 Python 版本,但这可以并且将通过一个或多个 EXTENDED_ARG 操作扩展到 4 字节,允许访问2^32 个局部变量。你可以在Python/wordcode_helpers.h 中看到一些用于EXTENDED_ARG 的助手。 (请注意,dis 文档中 EXTENDED_ARG 的操作码文档尚未更新以反映新的 Python 3.6 字码结构。)

【讨论】:

  • 没错。我只是在查看wordcode_helpers.h 文件。
【解决方案2】:

关于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 个变量的整个函数,这使得它们也可以在 __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

这意味着尽管STORE_FAST 使用 2 个字节来保存 TOS 并且“理论上”不能保存超过 2 个^16 不同的变量,但应该有一些其他的唯一标识符,例如偏移量,或额外的空间,可以保存超过 2^16。并且as it turned outEXTENDED_ARG,正如它在文档中提到的那样,它会作为任何操作码的前缀,该操作码的参数太大而无法放入默认的两个字节中。因此它是 2^16 + 16 = 2^32

EXTENDED_ARG(ext)¶

为参数太大而无法放入默认两个字节的任何操作码添加前缀。 ext 包含两个额外的字节 连同随后的操作码的参数,组成一个四字节 参数,ext 是两个最重要的字节。

【讨论】:

  • 也许我理解错了,但我可以用 2^17 个局部变量构造和执行一个函数:repl.it/repls/PleasantMediocreCharacterencoding
  • @AlexHall 这些变量应该存在于__code__.co_varnames 中。请查看相关更新以获得说明。
  • 我不认为你看过我的演示,或者至少不理解它。我已经根据您写的内容对其进行了更新。我不是execing 个人作业,这是一个很大的功能。变量通常以各种方式存在。
  • @AlexHall 是的,效果很好。尽管我最终测试了类似的代码,但它没有按预期工作,但这似乎工作得很好。但理论上它没有意义!
  • @AlexHall 我用您建议的代码更新了答案。感谢您的评论。但是,python 如何将这些 TOS 区分开来并不是 100% 清楚的。我在源代码中也找不到任何东西。
猜你喜欢
  • 2011-04-15
  • 2010-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-10
  • 1970-01-01
  • 1970-01-01
  • 2016-04-04
相关资源
最近更新 更多