Подтвердить что ты не робот

Сколько локальных переменных может выполнять функция 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) внутри функции.

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

4b9b3361

Ответ 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 - два наиболее значимых байта.