对象能否检查其被赋值的变量名称?
在Python中,有没有办法让一个对象实例知道它被赋值给了哪个变量名?举个例子:
class MyObject(object):
pass
x = MyObject()
比如说,MyObject能不能在某个时刻知道自己被赋值给了变量名x?比如在它的__init__方法里?
7 个回答
5
很多人都说,这个事情没法做好。不过受到jsbueno的启发,我有一个替代的解决方案。
和他的方案一样,我会检查调用者的堆栈帧,这意味着它只对用Python写的调用者有效(下面有说明)。不同的是,我直接检查调用者的字节码(而不是加载和解析源代码)。使用Python 3.4及以上版本的dis.get_instructions()
,这样做有一定的兼容性。不过,这段代码还是有点黑科技的感觉。
import inspect
import dis
def take1(iterator):
try:
return next(iterator)
except StopIteration:
raise Exception("missing bytecode instruction") from None
def take(iterator, count):
for x in range(count):
yield take1(iterator)
def get_assigned_name(frame):
"""Takes a frame and returns a description of the name(s) to which the
currently executing CALL_FUNCTION instruction's value will be assigned.
fn() => None
a = fn() => "a"
a, b = fn() => ("a", "b")
a.a2.a3, b, c* = fn() => ("a.a2.a3", "b", Ellipsis)
"""
iterator = iter(dis.get_instructions(frame.f_code))
for instr in iterator:
if instr.offset == frame.f_lasti:
break
else:
assert False, "bytecode instruction missing"
assert instr.opname.startswith('CALL_')
instr = take1(iterator)
if instr.opname == 'POP_TOP':
raise ValueError("not assigned to variable")
return instr_dispatch(instr, iterator)
def instr_dispatch(instr, iterator):
opname = instr.opname
if (opname == 'STORE_FAST' # (co_varnames)
or opname == 'STORE_GLOBAL' # (co_names)
or opname == 'STORE_NAME' # (co_names)
or opname == 'STORE_DEREF'): # (co_cellvars++co_freevars)
return instr.argval
if opname == 'UNPACK_SEQUENCE':
return tuple(instr_dispatch(instr, iterator)
for instr in take(iterator, instr.arg))
if opname == 'UNPACK_EX':
return (*tuple(instr_dispatch(instr, iterator)
for instr in take(iterator, instr.arg)),
Ellipsis)
# Note: 'STORE_SUBSCR' and 'STORE_ATTR' should not be possible here.
# `lhs = rhs` in Python will evaluate `lhs` after `rhs`.
# Thus `x.attr = rhs` will first evalute `rhs` then load `a` and finally
# `STORE_ATTR` with `attr` as instruction argument. `a` can be any
# complex expression, so full support for understanding what a
# `STORE_ATTR` will target requires decoding the full range of expression-
# related bytecode instructions. Even figuring out which `STORE_ATTR`
# will use our return value requires non-trivial understanding of all
# expression-related bytecode instructions.
# Thus we limit ourselfs to loading a simply variable (of any kind)
# and a arbitary number of LOAD_ATTR calls before the final STORE_ATTR.
# We will represents simply a string like `my_var.loaded.loaded.assigned`
if opname in {'LOAD_CONST', 'LOAD_DEREF', 'LOAD_FAST',
'LOAD_GLOBAL', 'LOAD_NAME'}:
return instr.argval + "." + ".".join(
instr_dispatch_for_load(instr, iterator))
raise NotImplementedError("assignment could not be parsed: "
"instruction {} not understood"
.format(instr))
def instr_dispatch_for_load(instr, iterator):
instr = take1(iterator)
opname = instr.opname
if opname == 'LOAD_ATTR':
yield instr.argval
yield from instr_dispatch_for_load(instr, iterator)
elif opname == 'STORE_ATTR':
yield instr.argval
else:
raise NotImplementedError("assignment could not be parsed: "
"instruction {} not understood"
.format(instr))
说明:用C语言实现的函数不会显示为Python的堆栈帧,因此这个脚本看不到它们。这会导致错误的结果。举个例子,假设有个Python函数f()
,它调用了a = g()
。而g()
是用C实现的,它又调用了b = f2()
。当f2()
试图查找赋值的名字时,它会得到a
而不是b
,因为这个脚本对C函数是无能为力的。(至少我猜是这样工作的 :P)
使用示例:
class MyItem():
def __init__(self):
self.name = get_assigned_name(inspect.currentframe().f_back)
abc = MyItem()
assert abc.name == "abc"
19
是的,这是可能的*。不过,这个问题看起来比表面上要复杂得多:
- 同一个对象可能有多个名字。
- 可能根本没有名字。
- 同样的名字可能在不同的命名空间中指向其他对象。
无论如何,知道如何找到一个对象的名字有时对调试很有帮助——下面是具体的方法:
import gc, inspect
def find_names(obj):
frame = inspect.currentframe()
for frame in iter(lambda: frame.f_back, None):
frame.f_locals
obj_names = []
for referrer in gc.get_referrers(obj):
if isinstance(referrer, dict):
for k, v in referrer.items():
if v is obj:
obj_names.append(k)
return obj_names
如果你有想根据变量的名字来设计逻辑的冲动,先停一下,想想是否可以通过重新设计或重构代码来解决问题。想要从对象本身恢复它的名字,通常意味着你的程序中的数据结构需要重新考虑。
* 至少在Cpython中是这样
4
不,对象和名称是存在于不同的层面上的。一个对象在它的生命周期中可以有很多名字,而你很难确定哪个名字是你想要的。就像在这里:
class Foo(object):
def __init__(self): pass
x = Foo()
两个名字指向同一个对象(在__init__
运行时是self
,在全局范围内是x
)。