Динамический ответ на распаковку - PullRequest
2 голосов
/ 09 апреля 2019

Может ли назначенный объект в распакованном операторе присваивания проверять количество переменных, которым он назначается?

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

Ответы [ 2 ]

3 голосов
/ 09 апреля 2019

Вы можете использовать модуль трассировки:

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
2 голосов
/ 09 апреля 2019

(Отказ от ответственности: я не рекомендую использовать дьявольские методы в коде производственного качества. Все в этом ответе может не работать на другом компьютере, отличном от моего, или другой версии Python от моего, или в дистрибутиве, отличном от CPython,и это может не сработать завтра утром.)

Возможно, вы могли бы сделать это, проверив байт-код вызывающего кадра.Если я правильно читаю руководство по байт-коду *1003*, множественное назначение обрабатывается инструкциями 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]
...