О 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
Подробнее см. https://stackoverflow.com/a/49208472/2867928.
Однако это не означает, что мы не можем определить предел в теории. Т.е. анализируя способ, которым питон хранит локальные переменные в памяти.
Способ, которым мы можем это сделать, - сначала посмотреть на байт-коды функции и посмотреть, как соответствующие инструкции хранятся в памяти. 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 интерпретатор Python теперь использует 16-битный код слова вместо байт-кода. Фактически выравнивает инструкции, чтобы они всегда были 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 содержит два дополнительных байта, которые, взятые
вместе с последующим аргументом кода операции содержат четырехбайтовый
аргумент, ext - два наиболее значимых байта.