动态响应解包赋值语句
Dynamically responding to an unpacking assignment statement
在拆包赋值语句中,被赋值的对象可以检查它被赋值的变量的数量吗?
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
的 "shape" 吗?
>>> first, *blob, d[k], (x, y), L[3:7], obj.attr, last = MyObject()
unpacking to <_ast.Tuple object at 0xcafef00d>
潜在用例示例:改进的MagicMock()
,在用于修补某些对象时不需要预先配置固定的迭代长度在赋值语句的右侧。
(免责声明:我不建议在生产质量代码中使用恶魔般的技术。此答案中的所有内容可能不适用于与我不同的计算机,或与我不同的 Python 版本,或非 CPython 发行版,明天早上可能无法正常工作。)
也许您可以通过检查调用框架的字节码来做到这一点。如果我正确阅读 bytecode guide,多重赋值由指令 UNPACK_SEQUENCE
或 UNPACK_EX
处理,具体取决于目标列表是否有星号名称。这两条指令都在其参数中提供了有关目标列表形状的信息。
您可以编写恶魔般的函数来攀登框架层次结构,直到它找到调用框架,并检查出现在 FUNCTION_CALL
之后的字节码指令,它代表赋值的右侧。 (这是假设您对 MyObject()
的调用是语句右侧的唯一内容)。然后你可以从指令的参数和return中提取目标列表大小。
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]
您可以使用回溯模块:
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
在拆包赋值语句中,被赋值的对象可以检查它被赋值的变量的数量吗?
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
的 "shape" 吗?
>>> first, *blob, d[k], (x, y), L[3:7], obj.attr, last = MyObject()
unpacking to <_ast.Tuple object at 0xcafef00d>
潜在用例示例:改进的MagicMock()
,在用于修补某些对象时不需要预先配置固定的迭代长度在赋值语句的右侧。
(免责声明:我不建议在生产质量代码中使用恶魔般的技术。此答案中的所有内容可能不适用于与我不同的计算机,或与我不同的 Python 版本,或非 CPython 发行版,明天早上可能无法正常工作。)
也许您可以通过检查调用框架的字节码来做到这一点。如果我正确阅读 bytecode guide,多重赋值由指令 UNPACK_SEQUENCE
或 UNPACK_EX
处理,具体取决于目标列表是否有星号名称。这两条指令都在其参数中提供了有关目标列表形状的信息。
您可以编写恶魔般的函数来攀登框架层次结构,直到它找到调用框架,并检查出现在 FUNCTION_CALL
之后的字节码指令,它代表赋值的右侧。 (这是假设您对 MyObject()
的调用是语句右侧的唯一内容)。然后你可以从指令的参数和return中提取目标列表大小。
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]
您可以使用回溯模块:
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