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

Gro*_*ady 8 python

在Python中,有没有办法让对象的实例看到它所分配的变量名?以下面的例子为例:

class MyObject(object):
    pass

x = MyObject()
Run Code Online (Sandbox Code Playgroud)

MyObject是否可以在任何时候看到它已被赋值给变量名x?喜欢它的__init__方法?

wim*_*wim 9

对的,这是可能的*.但是,问题比初看起来更难:

  • 可能有多个名称分配给同一对象.
  • 可能根本没有名字.

无论如何,知道如何查找对象的名称有时可用于调试目的 - 以下是如何执行此操作:

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
Run Code Online (Sandbox Code Playgroud)

如果您曾试图围绕变量名称建立逻辑,请暂停一下,并考虑重新设计/重构代码是否可以解决问题.从对象本身恢复对象名称的需要通常意味着程序中的底层数据结构需要重新思考.

*至少在Cpython中

  • 太神奇了,而且也很简洁。顺便说一句,我相信编写一些小实用程序包(例如在 Jupyter 笔记本中或在调试会话中交互式使用)对于此类黑客攻击来说是完全合法的场景。只是不要在生产代码中使用它。 (3认同)

dri*_*iax 5

正如许多其他人所说,它无法正确完成。无论受到 jsbueno 的启发,我都有他的解决方案的替代方案。

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

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))
Run Code Online (Sandbox Code Playgroud)

注意:C 实现的函数不会显示为 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"
Run Code Online (Sandbox Code Playgroud)


Cat*_*lus 4

不,物体和名字存在于不同的维度。一个对象在其生命周期内可以有多个名称,并且无法确定哪一个可能是您想要的名称。即使在这里:

class Foo(object):
    def __init__(self): pass

x = Foo()
Run Code Online (Sandbox Code Playgroud)

两个名称表示同一个对象(运行self__init__x在全局范围内)。