【问题标题】:Can an object inspect the name of the variable it's been assigned to?对象可以检查分配给它的变量的名称吗?
【发布时间】:2018-10-17 21:33:04
【问题描述】:

在 Python 中,有没有办法让对象的实例查看分配给它的变量名称?举个例子:

class MyObject(object):
    pass

x = MyObject()

MyObject 是否有可能在任何时候看到它已被分配给变量名 x?就像它的 __init__ 方法一样?

【问题讨论】:

  • 简短的回答是:不,不要尝试.. 真正的答案是肯定的,但不要尝试.. :)

标签: python


【解决方案1】:

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

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

x = Foo()

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

【讨论】:

    【解决方案2】:

    是的,有可能*。然而,这个问题比乍看之下要困难得多:

    • 可能有多个名称分配给同一个对象。
    • 可能根本没有没有名字
    • 相同的名称可能引用不同命名空间中的某些其他对象。

    无论如何,知道如何查找对象的名称有时对于调试目的很有用 - 下面是如何做到这一点:

    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
    

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

    * 至少在 Cpython 中

    【讨论】:

    • 很棒,也很简洁。顺便说一句,我相信编写小的实用程序包,例如在 Jupyter 笔记本或调试会话中进行交互式使用是此类黑客攻击的完全合法场景。只是不要在生产代码中使用它。
    【解决方案3】:

    通常无法做到这一点,尽管这可以通过使用内省和用于调试程序的工具来实现。代码必须从“.py”文件运行,而不是从编译的字节码或压缩模块内部运行 - 因为它依赖于文件源代码的读取,从应该找到“它在哪里”的方法中运行”。

    诀窍是访问初始化对象的执行框架——使用inspect.currentframe——框架对象有一个“f_lineno”值,它表示调用对象方法的行号(在这种情况下,@ 987654321@) 已被调用。函数inspect.filename 允许检索文件的源代码,并获取适当的行号。

    一个简单的解析然后查看“=”符号之前的部分,并假设它是包含对象的变量。

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

    这不适用于多个分配,在进行分配之前与对象组成的表达式,将对象附加到列表或添加到字典或集合,for 循环初始化中的对象实例化,天知道还有哪些情况—— 请记住,在第一个属性之后,该对象也可以被任何其他变量引用。

    底线:它可能的,但作为一个玩具 - 它不能在生产代码中使用 - 只需在对象初始化期间将变量名称作为字符串传递,就像在创建 collections.namedtuple 时所做的那样@

    如果您需要名称,“正确的方法”是将名称作为字符串参数显式传递给对象初始化,例如:

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

    而且,如果绝对需要只键入一次对象的名称,还有另一种方法 - 继续阅读。 由于 Python 的语法,一些不使用“=”运算符的特殊赋值允许对象知道它被分配了名称。因此,在 Python 中执行赋值的其他 statemtns 是 for、with、def 和 class 关键字 - 可能会滥用这一点,因为具体而言,类创建和函数定义是创建“知道”其名称的对象的赋值语句。

    让我们关注def 语句。它通常创建一个函数。但是使用装饰器,您可以使用“def”来创建任何类型的对象 - 并将名称用于构造函数可用的函数:

    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
    

    (这最后一种方法可以在生产代码中使用,不像求助于读取源文件的方法)

    【讨论】:

      【解决方案4】:

      这里有一个简单的函数来实现你想要的,假设你希望从方法调用中检索分配实例的变量的名称

      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
      

      这是一个用法示例:

      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
      

      【讨论】:

      • 现在可以了。使用 Python 3.6.2 测试
      【解决方案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))
      

      注意:C 实现的函数不会显示为 Python 堆栈帧,因此在此脚本中是隐藏的。这将导致误报。考虑调用a = g() 的Python 函数f()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"
      

      【讨论】:

        【解决方案6】:

        假设:

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

        然后您可以通过对象的 id 搜索环境,当匹配时返回键。

        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
        

        【讨论】:

          【解决方案7】:

          我独立完成了这项工作,并拥有以下内容。它没有 driax 的答案那么全面,但有效地涵盖了所描述的情况,并且不依赖于在全局变量中搜索对象的 id 或解析源代码......

          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'
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-08-22
            • 1970-01-01
            • 1970-01-01
            • 2016-06-30
            相关资源
            最近更新 更多