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