Сколько локальных переменных может выполнять функция Python (реализация CPython)?

Мы уже знаем, что аргументы функции используются для ограничения 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',)

Это означает, что даже если вы используете цикл for например:

for i in range(2**17):
    exec(f'var_{i} = {i}')

locals() будут содержать 2 ** 17 переменных, но вы не можете сделать что-то вроде print(var_100) внутри функции.

Мы знаем, что в принципе нет необходимости динамически добавлять переменную в функцию, в то время как вы можете использовать словарь или, другими словами, собственное пространство имен. Но какой правильный способ проверить предел максимального числа локальных переменных в функции?

Ответы

Ответ 1

2 ^ 32. Операция LOAD_FAST используемая для загрузки локальных переменных, имеет только 1-байтовую или 2-байтовую oparg в зависимости от версии Python, но она может и будет расширена до 4 байтов одним или несколькими операциями EXTENDED_ARG, позволяя получить доступ к 2 ^ 32 локальным переменные. Вы можете увидеть некоторые из помощников, используемых для EXTENDED_ARG в Python/wordcode_helpers.h. (Обратите внимание, что документация по opcode для EXTENDED_ARG в dis документах еще не обновлена, чтобы отразить новую структуру словарного кода Python 3.6.)

Ответ 2

О exec() и его поведении с местными жителями уже есть открытые дебаты: как работает 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

Прочтите fooobar.com/info/298113/... для более подробной информации.

Однако это не означает, что мы не можем определить предел в теории. Проанализируя, как 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 код STOR_FAST хранит TOS (верхнюю часть стека) в локальные co_varnames[var_num]. И так как разница его смещения с его следующим кодом операции составляет 3 (6 - 3), это означает, что каждый код операции STOR_FAST занимает только 3 байта памяти. Первый байт предназначен для хранения операции или байтового кода; второй два байта являются операндом для этого байтового кода, что означает, что существует 2 ^ 16 возможных комбинаций.

Поэтому в одном байте_компиляторе теоретически функция может иметь только 65536 локальных переменных.

После Python-3.6 интерпретатор Python теперь использует 16-битный wordcode вместо байт-кода. На самом деле выравнивание инструкций всегда должно быть 2 байта, а не 1 или 3, если аргументы занимают только 1 байт.

Поэтому, если вы выполните разборку в более поздних версиях, вы получите следующий результат, который по-прежнему использует два байта для 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. И, как оказалось, EXTENDED_ARG, как упоминалось в документации, префикс любого кода операции, который имеет слишком большой аргумент, чтобы вписаться в два байта по умолчанию. Поэтому 2 ^ 16 + 16= 2 ^ 32.

EXTENDED_ARG (внешн) ¶

Префикс любого кода операции, у которого слишком большой аргумент, чтобы вписаться в два байта по умолчанию. ext содержит два дополнительных байта, которые вместе с последующим аргументом opcodes содержат четырехбайтовый аргумент, ext - два наиболее значимых байта.