¿Puede un objeto inspeccionar el nombre de la variable a la que ha sido asignado?

En Python, ¿hay una manera para que una instancia de un objeto vea el nombre de la variable a la que está asignado? Toma lo siguiente por ejemplo:

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

¿Es posible que MyObject vea que ha sido asignado a un nombre de variable x en cualquier momento? Como en su método __init__?

No. Los objetos y los nombres viven en dimensiones separadas. Un objeto puede tener muchos nombres durante su vida útil, y es imposible determinar cuál puede ser el que usted desea. Incluso aquí:

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

dos nombres denotan el mismo objeto ( self cuando __init__ ejecuta, x en el scope global).

Sí, es posible*. Sin embargo, el problema es más difícil de lo que parece a primera vista:

  • Puede haber varios nombres asignados al mismo objeto.
  • Puede que no haya nombres en absoluto.

En cualquier caso, saber cómo encontrar los nombres de un objeto a veces puede ser útil para propósitos de depuración, y aquí es cómo hacerlo:

 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 

Si alguna vez tiene la tentación de basar la lógica en los nombres de sus variables, haga una pausa por un momento y considere si el rediseño / refactor del código podría resolver el problema. La necesidad de recuperar el nombre de un objeto del propio objeto generalmente significa que las estructuras de datos subyacentes en su progtwig necesitan un replanteamiento.

* al menos en Cpython

Normalmente no se puede hacer, aunque esto se puede lograr mediante la introspección y las instalaciones destinadas a depurar un progtwig. Sin embargo, el código debe ejecutarse desde un archivo “.py”, y no solo desde un bytecode comstackdo, o dentro de un módulo comprimido, ya que se basa en la lectura del código fuente del archivo, dentro del método que debería encontrar “dónde está corriendo”.

El truco consiste en acceder al marco de ejecución desde donde se inicializó el objeto – con inspect.currentframe – el objeto de marco tiene un valor “f_lineno” que indica el número de línea donde se realizó la llamada al método del objeto (en este caso, __init__ ) llamado. La función inspect.filename permite recuperar el código fuente del archivo y obtener el número de línea correspondiente.

Un análisis ingenuo luego mira la parte que precede a un signo “=” y asume que es la variable que contendrá el objeto.

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

Eso no funcionará para múltiples asignatarios, las expresiones que componen el objeto antes de que se realice la asignación, los objetos se agregan a listas o se agregan a diccionarios o conjuntos, la creación de instancias de objetos en la inicialización de los bucles for , y Dios sabe qué más situaciones. teniendo en cuenta que después de la primera atribución, el objeto también podría ser referenciado por cualquier otra variable.

Línea de botones: es posible, pero como juguete, no se puede usar en el código de producción, solo se debe pasar el nombre de varibal como una cadena durante la inicialización del objeto, tal como se debe hacer al crear una collections.namedtuple

La “forma correcta” de hacerlo, si necesita el nombre, es pasar explícitamente el nombre a la inicialización del objeto, como un parámetro de cadena, como en:

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

Y aún así, si es absolutamente necesario escribir el nombre de los objetos solo una vez, hay otra forma: sigue leyendo. Debido a la syntax de Python, algunas asignaciones especiales, que no utilizan el operador “=” permiten que un objeto sepa que se le asigna un nombre. Por lo tanto, otras declaraciones que realizan asignaturas en Python son las palabras clave for, with, def y class. Es posible abusar de esto, ya que específicamente una creación de clase y una definición de función son declaraciones de asignación que crean objetos que “saben” sus nombres.

Centrémonos en la statement de def . Normalmente crea una función. Pero al usar un decorador, puede usar “def” para crear cualquier tipo de objeto, y tener el nombre usado para la función disponible para el 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 

(Esta última forma de hacerlo podría usarse en el código de producción, a diferencia de la que recurre a leer el archivo fuente)

Como muchos otros han dicho, no se puede hacer correctamente. Sin embargo, inspirado en jsbueno, tengo una alternativa a su solución.

Al igual que su solución, inspecciono el marco de la stack de llamantes, lo que significa que solo funciona correctamente para los llamadores implementados por Python (vea la nota a continuación). A diferencia de él, inspecciono el código de bytes de la persona que llama directamente (en lugar de cargar y analizar el código fuente). Usando dis.get_instructions() Python + +, esto se puede hacer con la esperanza de una compatibilidad mínima. Aunque esto sigue siendo un código 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)) 

Nota: las funciones implementadas en C no se muestran como marcos de stack de Python y, por lo tanto, están ocultas en este script. Esto dará lugar a falsos positivos. Considere la función Python f() que llama a = g() . g() es C-implementado y llama b = f2() . Cuando f2() intenta buscar el nombre asignado, obtendrá a lugar de b porque la secuencia de comandos es ajena a las funciones de C. (Al menos así es como creo que funcionará: P)

Ejemplo de uso:

 class MyItem(): def __init__(self): self.name = get_assigned_name(inspect.currentframe().f_back) abc = MyItem() assert abc.name == "abc" 

Aquí hay una función simple para lograr lo que desea, asumiendo que desea recuperar el nombre de la variable a la que se asigna la instancia desde una llamada de método :

 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 

Aquí hay un ejemplo de uso:

 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