关于python:对象是否可以检查已分配给它的变量的名称?

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()

两个名称表示相同的对象(当__init__运行时,selfx在全局范围内)。


正如许多人所说,这是做不好的。不管受到JSBUENO的启发,我有一个替代他的解决方案。

和他的解决方案一样,我检查了调用者堆栈帧,这意味着它只适用于Python实现的调用者(见下面的注释)。与他不同,我直接检查调用者的字节码(而不是加载和解析源代码)。使用python 3.4+的dis.get_instructions(),这可以在希望兼容性最小的情况下完成。尽管这仍然是一些黑客代码。

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函数f(),它调用a = g()g()是C实现的,调用b = f2()。当f2()试图查找分配的名称时,它将得到a而不是b,因为脚本对c函数不敏感。(至少我想这是它的工作原理:p)

用法示例:

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"值,该值说明调用对象方法(在本例中为__init__的行号)。函数inspect.filename允许检索文件的源代码,并获取相应的行号。

幼稚的分析然后扫视在"="符号前面的部分,并假设它是将包含对象的变量。

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()

它不适用于多个赋值、在赋值之前与对象组合的表达式、添加到列表中或添加到字典或集合中的对象、初始化for循环中的对象实例化,以及上帝知道更多的情况。--记住,在第一个属性之后,对象也可以被任何其他变量引用。

博顿生产线:这是可能的,但作为一个玩具,它不能用我的生产代码-只需在对象初始化期间将varibal名称作为字符串传递,就像创建collections.namedtuple时必须做的那样。

如果您需要名称,则执行此操作的"正确方法"是将名称作为字符串参数显式传递给对象初始化,如:

1
2
3
4
5
class A(object):
  def __init__(self, name):
      self.name = name

x = A("x")

而且,如果绝对只需要输入一次对象的名称,还有另一种方法——继续读。由于python的语法,一些特殊的赋值(不使用"="运算符)确实允许对象知道它被赋值了。因此,在python中执行赋值的其他状态是for、with、def和class关键字——这可能会被滥用,因为在特定情况下,类创建和函数定义是创建"知道"其名称的对象的赋值语句。

让我们集中讨论一下def声明。它通常创建一个函数。但是,使用decorator,您可以使用"def"创建任何类型的对象,并让构造函数可以使用函数的名称:

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