Сколько локальных переменных может содержать функция Python (реализация CPython)? - PullRequest
0 голосов
/ 12 мая 2018

Мы уже знаем, что аргументы функции раньше имели ограничение в 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) внутри функции.

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

Ответы [ 2 ]

0 голосов
/ 12 мая 2018

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

0 голосов
/ 12 мая 2018

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

...