#python #iteration #variable-assignment #abstract-syntax-tree #inspect
#python #итерация #переменная-присвоение #абстрактное синтаксическое дерево #Осмотр
Вопрос:
Может ли назначенный объект в операторе присваивания распаковки проверять количество переменных, которым он присваивается?
class MyObject:
def __iter__(self):
n = some_diabolical_hack()
print(f"yielding {n} vals")
return iter(["potato"]*n)
Что-то вроде:
>>> x, y = MyObject()
yielding 2 vals
>>> a, b, c = MyObject()
yielding 3 vals
В более общем случае, может ли он анализировать «форму» объекта target_list
, используемого в присваивании?
>>> first, *blob, d[k], (x, y), L[3:7], obj.attr, last = MyObject()
unpacking to <_ast.Tuple object at 0xcafef00d>
Пример потенциального варианта использования: улучшенный MagicMock()
, который не нужно предварительно настраивать с фиксированной длиной итерации при использовании для исправления некоторого объекта в правой части оператора присваивания.
Комментарии:
1. Не было бы проще вернуть кортеж и позволить вызывающей стороне обработать его? Каждый раз, когда вы думаете о том, чтобы добавить «дьявольский хак» в свой код, вам следует сначала подумать о его рефакторинге
2. Конечно, дьявольским взломам почти никогда не место в коде производственного качества. Но я действительно думаю, что это интересный вопрос с академической точки зрения. Я хотел бы увидеть ответ, даже если я никогда не собираюсь стрелять из этого конкретного пистолета в мою ногу 🙂
3. Возврат кортежа только уводит в сторону вопрос (какой длины должен быть возвращаемый кортеж?). Я отредактировал в вопросе пример использования.
Ответ №1:
Вы могли бы использовать модуль обратной трассировки:
import traceback
def diabolically_invoke_traceback():
call = traceback.extract_stack()[-2]
print call[3]
unpackers = call[3].split('=')[0].split(',')
print len (unpackers)
return range(len(unpackers))
In [63]: a, b, c = diabolically_invoke_traceback()
a, b, c = diabolically_invoke_traceback()
3
In [64]: a
Out[64]: 0
In [65]: b
Out[65]: 1
In [66]: c
Out[66]: 2
Комментарии:
1. Краткий и простой 🙂 он не совсем работает в определенных угловых случаях, таких как назначения с продолжением строки (например
a,b,c =
, в одной строке, за которой следуетdiabolically_invoke_traceback()
следующая), но мой ответ также не совсем работает в угловых случаях, поэтому я не в состоянии судить :-p
Ответ №2:
(Отказ от ответственности: я не рекомендую использовать дьявольские методы в коде производственного качества. Все в этом ответе может не работать на другом компьютере, отличном от моего, или на другой версии Python, отличной от моей, или в дистрибутиве, отличном от CPython, и это может не сработать завтра утром.)
Возможно, вы могли бы сделать это, проверив байт-код вызывающего фрейма. Если я правильно читаю руководство по байт-коду, множественное присвоение обрабатывается инструкциями UNPACK_SEQUENCE
или UNPACK_EX
, в зависимости от того, имеет ли целевой список помеченное имя. Обе эти инструкции предоставляют информацию о форме целевого списка в своих аргументах.
Вы могли бы написать свою дьявольскую функцию, чтобы подниматься по иерархии фреймов, пока она не найдет вызывающий фрейм, и проверять инструкцию байт-кода, которая появляется после FUNCTION_CALL
того, что представляет правую часть присваивания. (это предполагает, что ваш вызов MyObject()
— это единственное, что находится в правой части инструкции). Затем вы можете извлечь размер целевого списка из аргумента инструкции и вернуть его.
import inspect
import dis
import itertools
def diabolically_retrieve_target_list_size():
#one f_back takes us to `get_diabolically_sized_list`'s frame. A second one takes us to the frame of the caller of `get_diabolically_sized_list`.
frame = inspect.currentframe().f_back.f_back
#explicitly delete frame when we're done with it to avoid reference cycles.
try:
#get the bytecode instruction that immediately follows the CALL_FUNCTION that is executing right now
bytecode_idx = frame.f_lasti // 2
unresolved_bytecodes = itertools.islice(dis.get_instructions(frame.f_code), bytecode_idx 1, bytecode_idx 3)
next_bytecode = next(unresolved_bytecodes)
if next_bytecode.opname == "UNPACK_SEQUENCE": #simple multiple assignment, like `a,b,c = ...`
return next_bytecode.arg
elif next_bytecode.opname == "EXTENDED_ARG": #multiple assignment with splat, like `a, *b, c = ...`
next_bytecode = next(unresolved_bytecodes)
if next_bytecode.opname != "UNPACK_EX":
raise Exception(f"Expected UNPACK_EX after EXTENDED_ARG, got {next_bytecode.opname} instead")
args_before_star = next_bytecode.arg % 256
args_after_star = next_bytecode.arg >> 8
return args_before_star args_after_star
elif next_bytecode.opname in ("STORE_FAST", "STORE_NAME"): #single assignment, like `a = ...`
return 1
else:
raise Exception(f"Unrecognized bytecode: {frame.f_lasti} {next_bytecode.opname}")
finally:
del frame
def get_diabolically_sized_list():
count = diabolically_retrieve_target_list_size()
return list(range(count))
a,b,c = get_diabolically_sized_list()
print(a,b,c)
d,e,f,g,h,i = get_diabolically_sized_list()
print(d,e,f,g,h,i)
j, *k, l = get_diabolically_sized_list()
print(j,k,l)
x = get_diabolically_sized_list()
print(x)
Результат:
0 1 2
0 1 2 3 4 5
0 [] 1
[0]
Комментарии:
1. Хм, мне приходит в голову, что этот подход учитывает только размер целевого списка верхнего уровня . Вложенные целевые списки, такие как
(m, n, o), p = get_diabolically_sized_list()
, будут корректно генерировать список длиной два, но это не гарантирует, что первый элемент списка может быть распакован в три значения. Таким образом, этот подход не подходит для захвата «формы» сложных целевых списков.2. Если вы продолжите разрабатывать типы назначений, которые вы обрабатываете, вы в конечном итоге перепишете большое подмножество синтаксического анализатора python, что является еще одним уровнем дьявольщины. 😉
3. Очень верно. В чате Python OP задавался
ast
вопросом, может ли модуль быть лучшим способом захватить полный контекст вызывающего оператора присваивания. Я думаю, что он что-то там делает…