Can an object inspect the name of the variable it's been assigned to?
在Python中,有没有一种方法可以让对象的实例看到它分配给的变量名?以下面的例子为例:
1 2 3 4 | class MyObject(object): pass x = MyObject() |
MyObject是否可以在任何时候看到它被分配给变量名X?就像它的初始化方法一样?
是的,这是可能的。然而,这个问题比乍一看看起来更困难:
- 可能有多个名称分配给同一对象。
- 可能根本没有名字。
无论如何,知道如何找到对象的名称有时对调试很有用-下面是如何做到这一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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中
不。对象和名称位于不同的维度中。一个对象在其生命周期中可以有多个名称,而且不可能确定哪一个可能是您想要的。即使在这里:
1 2 3 4 | class Foo(object): def __init__(self): pass x = Foo() |
两个名称表示相同的对象(当
正如许多人所说,这是做不好的。不管受到JSBUENO的启发,我有一个替代他的解决方案。
和他的解决方案一样,我检查了调用者堆栈帧,这意味着它只适用于Python实现的调用者(见下面的注释)。与他不同,我直接检查调用者的字节码(而不是加载和解析源代码)。使用python 3.4+的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 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函数
用法示例:
1 2 3 4 5 6 | class MyItem(): def __init__(self): self.name = get_assigned_name(inspect.currentframe().f_back) abc = MyItem() assert abc.name =="abc" |
号
虽然这可以通过使用自省和调试程序的工具来实现,但通常不能做到。但是,代码必须从一个".py"文件运行,而不仅仅是从编译的字节码运行,或者在压缩模块中运行,因为它依赖于从应该找到"运行位置"的方法中读取文件源代码。
诀窍是访问从中初始化对象的执行帧-使用inspect.currentframe-该帧对象有一个"f_lineno"值,该值说明调用对象方法(在本例中为
幼稚的分析然后扫视在"="符号前面的部分,并假设它是将包含对象的变量。
1 2 3 4 5 6 7 8 9 10 11 12 | from inspect import currentframe, getfile class A(object): def __init__(self): f = currentframe(1) filename = getfile(f) code_line = open(filename).readlines()[f.f_lineno - 1] assigned_variable = code_line.split("=")[0].strip() print assigned_variable my_name = A() other_name = A() |
它不适用于多个赋值、在赋值之前与对象组合的表达式、添加到列表中或添加到字典或集合中的对象、初始化
博顿生产线:这是可能的,但作为一个玩具,它不能用我的生产代码-只需在对象初始化期间将varibal名称作为字符串传递,就像创建
如果您需要名称,则执行此操作的"正确方法"是将名称作为字符串参数显式传递给对象初始化,如:
1 2 3 4 5 | class A(object): def __init__(self, name): self.name = name x = A("x") |
。
而且,如果绝对只需要输入一次对象的名称,还有另一种方法——继续读。由于python的语法,一些特殊的赋值(不使用"="运算符)确实允许对象知道它被赋值了。因此,在python中执行赋值的其他状态是for、with、def和class关键字——这可能会被滥用,因为在特定情况下,类创建和函数定义是创建"知道"其名称的对象的赋值语句。
让我们集中讨论一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class MyObject(object): def __new__(cls, func): # Calls the superclass constructor and actually instantiates the object: self = object.__new__(cls) #retrieve the function name: self.name = func.func_name #returns an instance of this class, instead of a decorated function: return self def __init__(self, func): print"My name is", self.name #and the catch is that you can't use"=" to create this object, you have to do: @MyObject def my_name(): pass |
(这最后一种方法可以用在生产代码中,而不是用来读取源文件的方法)
这里有一个简单的函数来实现您想要的,假设您希望检索从方法调用中分配实例的变量的名称:
1 2 3 4 5 6 7 | import inspect def get_instance_var_name(method_frame, instance): parent_frame = method_frame.f_back matches = {k: v for k,v in parent_frame.f_globals.items() if v is instance} assert len(matches) < 2 return matches.keys()[0] if matches else None |
。
下面是一个用法示例:
1 2 3 4 5 6 7 8 9 10 11 12 | class Bar: def foo(self): print get_instance_var_name(inspect.currentframe(), self) bar = Bar() bar.foo() # prints 'bar' def nested(): bar.foo() nested() # prints 'bar' Bar().foo() # prints None |
。