Can an object inspect the name of the variable it's been assigned to?

前端 未结 7 1481

In Python, is there a way for an instance of an object to see the variable name it\'s assigned to? Take the following for example:

class MyObject(object):
           


        
相关标签:
7条回答
  • 2020-11-29 11:51

    I was independently working on this and have the following. It's not as comprehensive as driax's answer, but efficiently covers the case described and doesn't rely on searching for the object's id in global variables or parsing source code...

    import sys
    import dis
    
    class MyObject:
        def __init__(self):
            # uses bytecode magic to find the name of the assigned variable
            f = sys._getframe(1) # get stack frame of caller (depth=1)
            # next op should be STORE_NAME (current op calls the constructor)
            opname = dis.opname[f.f_code.co_code[f.f_lasti+2]]
            if opname == 'STORE_NAME': # not all objects will be assigned a name
                # STORE_NAME argument is the name index
                namei = f.f_code.co_code[f.f_lasti+3]
                self.name = f.f_code.co_names[namei]
            else:
                self.name = None
    
    x = MyObject()
    
    x.name == 'x'
    
    0 讨论(0)
  • 2020-11-29 11:53

    Yes, it is possible*. However, the problem is more difficult than it seems upon first glance:

    • There may be multiple names assigned to the same object.
    • There may be no names at all.
    • The same name(s) may refer to some other object(s) in a different namespace.

    Regardless, knowing how to find the names of an object can sometimes be useful for debugging purposes - and here is how to do it:

    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
    

    If you're ever tempted to base logic around the names of your variables, pause for a moment and consider if redesign/refactor of code could solve the problem. The need to recover an object's name from the object itself usually means that underlying data structures in your program need a rethink.

    * at least in Cpython

    0 讨论(0)
  • 2020-11-29 11:54

    No. Objects and names live in separate dimensions. One object can have many names during its lifetime, and it's impossible to determine which one might be the one you want. Even in here:

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

    two names denote the same object (self when __init__ runs, x in global scope).

    0 讨论(0)
  • 2020-11-29 11:55

    As many others have said, it can't be done properly. However inspired by jsbueno's, I have an alternative to his solution.

    Like his solution, I inspect the callers stack frame, which means it only works properly for Python-implemented callers (see note below). Unlike him, I inspect the bytecode of the caller directly (instead of loading and parsing the source code). Using Python 3.4+'s dis.get_instructions() this can be done with some hope of minimal compatibility. Though this is still some hacky code.

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

    Note: C-implemented functions don't show up as Python stack frames and are thus hidden to this script. This will result in false positives. Consider Python function f() which calls a = g(). g() is C-implemented and calls b = f2(). When f2() tries to lookup up the assigned name, it will get a instead of b because the script is oblivious to C functions. (At least this is how I guess it will work :P )

    Usage example:

    class MyItem():
        def __init__(self):
            self.name = get_assigned_name(inspect.currentframe().f_back)
    
    abc = MyItem()
    assert abc.name == "abc"
    
    0 讨论(0)
  • 2020-11-29 11:58

    Here is a simple function to achieve what you want, assuming you wish to retrieve the name of the variable where the instance is assigned from a method call :

    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 list(matches.keys())[0] if matches else None
    

    Here is an usage example :

    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
    
    0 讨论(0)
  • 2020-11-29 12:04

    assuming this:

    class MyObject(object):
        pass
    
    x = MyObject()
    

    then you can search through the environment by the object's id, returning the key when there is a match.

    keys = list(globals().keys())  # list all variable names
    target = id(x)  # find the id of your object
    
    for k in keys:
        value_memory_address = id(globals()[k])  # fetch id of every object
        if value_memory_address == target:
            print(globals()[k], k)  # if there is a variable assigned to that id, then it is a variable that points to your object
    
    0 讨论(0)
提交回复
热议问题