12

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

x = MyObject()

Is it possible for MyObject to see it's been assigned to a variable name x at any point? Like in it's __init__ method?

Will
  • 1,149
  • 12
  • 25

7 Answers7

19

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

wim
  • 338,267
  • 99
  • 616
  • 750
  • 3
    Amazing, and so succinct too. By the way, I believe writing little utility packages e.g. for interactive usage in Jupyter notebooks or in debugging sessions is a perfectly legitimate scenario for such hacks. Just don't use it in production code. – Bananach Apr 25 '19 at 15:07
5

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"
driax
  • 2,528
  • 1
  • 23
  • 20
4

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

Cat Plus Plus
  • 125,936
  • 27
  • 200
  • 224
2

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
Lucas Cimon
  • 1,859
  • 2
  • 24
  • 33
1

It can't be ordinarily done, though this can be achieved by using introspection and facilities meant for debugging a program. The code must run from a ".py" file though, and not from just compiled bytecode, or inside a zipped module - as it relies on the reading of the file source code, from within the method that should find about "where it is running".

The trick is to access the execution frame where the object was initialized from - with inspect.currentframe - the frame object has a "f_lineno" value which states the line number where the call to the object method (in this case, __init__) has been called. The function inspect.filename allows one to retrieve the source code for the file, and fetch the apropriate line number.

A naive parse then peek the part preeceding an "=" sign, and assumes it is the variable that will contain the object.

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

That won't work for multiple assignents, expressions composing with the object before the assignemtn is made, objects being appended to lists or added to dictionaries or sets, object instantiation in intialization of for loops, and God knows which more situations -- And have in mind that after the first attribution, the object could be referenced by any other variable as well.

Botton line: it is possible, but as a toy - it can't be used i production code - just have the varibal name to be passed as a string during object initialization, just as one has to do when creating a collections.namedtuple

The "right way" to do it, if you are needing the name, is to explicitly pass the name to the object initialization, as a string parameter, like in:

class A(object):
  def __init__(self, name):
      self.name = name

x = A("x")

And still, if absolutely need to type the objects'name only once, there is another way - read on. Due to Python's syntax, some special assignments, not using the "=" operator do allow an object to know it is assigned name. So, other statemtns that perform assignents in Python are the for, with, def and class keywords - It is possible to abuse this, as specfically a class creation and a function definition are assignment statements that create objects which "know" their names.

Let's focus on the def statement. It ordinarily creates a function. But using a decorator you can use "def" to create any kind of object - and have the name used for the function available to the constructor:

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

(This last way of doing it could be used in production code, unlike the one which resorts to reading the source file)

jsbueno
  • 99,910
  • 10
  • 151
  • 209
0

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
mynameisvinn
  • 341
  • 4
  • 10
0

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'
abatea
  • 81
  • 1
  • 8