2012-01-16 12 views
5

En Python, ¿hay alguna manera para que una instancia de un objeto vea el nombre de la variable a la que está asignado? Tomemos el siguiente ejemplo para:¿Puede un objeto inspeccionar el nombre de la variable a la que ha sido asignado?

class MyObject(object): 
    pass 

x = MyObject() 

¿Es posible que MiObjeto para ver que ha sido asignado a un nombre de variable x en algún momento? Como en su método __init__?

+0

No es posible - aunque recientemente (hace 3-4 meses) en las ideas de la lista de Python ha habido alguna discusión sobre agregar una capacidad a algo como esto. – jsbueno

+3

La respuesta corta es: no, no intentes ...y la verdadera respuesta es sí, pero no intentes ... :) – wim

Respuesta

4

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

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

x = Foo() 

dos nombres denotan el mismo objeto (self cuando __init__ carreras, x en el ámbito global).

4

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

  • Puede haber varios nombres asignados a un mismo objeto.
  • Puede haber sin nombres en absoluto.

pesar de todo, saber cómo encontrar los nombres de un objeto a veces puede ser útil para propósitos de depuración - y aquí está 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 la tentación de base alrededor de la lógica los nombres de sus variables, haga una pausa por un momento y considere si el rediseño/refactorización del código podría resolver el problema. La necesidad de recuperar el nombre de un objeto del objeto mismo generalmente significa que las estructuras de datos subyacentes en su programa necesitan un replanteamiento.

* al menos en CPython

0

no se puede hacer de ordinario, aunque esto se puede lograr mediante el uso de la introspección e instalaciones destinado a la depuración de un programa. Sin embargo, el código debe ejecutarse desde un archivo ".py", y no solo desde un código byte compilado, o dentro de un módulo comprimido, ya que depende de la lectura del código fuente del archivo, desde el método que debe encontrar sobre "dónde está". corriendo".

El truco es acceder al marco de ejecución donde se inicializó el objeto - con inspect.currentframe - el objeto frame tiene un valor "f_lineno" que indica el número de línea donde se llama al método del objeto (en este caso, __init__) ha sido llamado. La función inspeccionar.nombre de archivo permite recuperar el código fuente del archivo y buscar el número de línea apropiado.

Un análisis ingenuo luego mira la parte anterior 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() 

que no funciona para múltiples assignents, expresiones que componen el objeto antes de la assignemtn se hace, los objetos que se agregan a las listas o añadirse a los diccionarios o en grupos, instancias de objetos de inicialización de for bucles, y Dios sabe qué más situaciones - Y tenga 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 Botton: que es posible, sino como un juguete - que no puede ser utilizado i código de producción - sólo tiene el nombre varibal ser pasado como una cadena durante la inicialización del objeto, al igual que uno tiene que hacer al crear un

la "forma correcta" collections.namedtuple a hacerlo, si usted está necesitando el nombre, es pasar de forma explícita el nombre a la inicialización de objetos, 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, allí es otra forma - sigue leyendo. Debido a la sintaxis de Python, algunas asignaciones especiales, que no utilizan el operador "=", permiten que un objeto sepa que tiene asignado un nombre. Por lo tanto, otros términos de estado que realizan asignadores en Python son las palabras clave para, con, definición y clase. Es posible abusar de esto, ya que una creación de clase y una definición de función son declaraciones de asignación que crean objetos que "conocen" sus nombres.

Centrémonos en la declaración def. Por lo general, crea una función. Pero el uso de un decorador puede utilizar "def" para crear cualquier tipo de objeto - y tienen el nombre que se utiliza para la función a disposición del 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 ser utilizado en código de producción, a diferencia del que recurre a leer el archivo fuente)

+0

¡muy interesante! – wim

0

Aquí hay una función simple para lograr lo que desea, suponiendo que desea recuperar el nombre de la variable donde se asignó la instancia desde una llamada al 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í se muestra 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 
0

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

Al igual que su solución, inspecciono el marco de pila de llamantes, lo que significa que solo funciona correctamente para llamadores implementados en Python (consulte la nota a continuación). A diferencia de él, inspecciono el bytecode de la persona que llama directamente (en lugar de cargar y analizar el código fuente). Usando Python 3.4 + dis.get_instructions() esto se puede hacer con la esperanza de una compatibilidad mínima. Aunque este 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 cuadros de pila de Python y, por lo tanto, se ocultan en este script. Esto dará como resultado falsos positivos. Considere la función Python f() que llama al a = g(). g() está implementado en C y llama al b = f2(). Cuando f2() intenta buscar el nombre asignado, obtendrá a en lugar de b porque la secuencia de comandos es ajena a las funciones de C. (Por lo menos así es como creo que va a trabajar: P) ejemplo

Uso:

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

abc = MyItem() 
assert abc.name == "abc" 
Cuestiones relacionadas