安排一个 Python 对象来发现它被分配给什么名字

Arrange a Python object to discover what name it is assigned to

TL;DR 总结

如何写 foo = MyClass() 并让 MyClass 对象知道它的名字是 foo

完整问题

我想在 Python 中编写一个库,允许构建对象树,支持递归遍历和命名。我想通过在创建并添加到父对象时自动发现对象的名称和父对象来使其易于使用。在这个简单的示例中,必须将名称和父对象显式传递给每个对象构造函数:

class TreeNode:
    def __init__(self, name, parent):
        self.name = name
        self.children = []
        self.parent = parent
        if parent is None:
            self.fullname = name
        else:
            parent.register_child(self)

    def register_child(self, child):
        self.children.append(child)
        child.fullname = self.fullname + "." + child.name

    def recursive_print(self):
        print(self.fullname)
        for child in self.children:
            child.recursive_print()

class CustomNode(TreeNode):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.foo = TreeNode(name="foo", parent=self)
        self.bar = TreeNode(name="bar", parent=self)

root = TreeNode(name="root", parent=None)
root.a = CustomNode(name="a", parent=root)

root.recursive_print()

输出:

root
root.a
root.a.foo
root.a.bar

我希望能够省略显式 nameparent 参数,例如:

class CustomNode(TreeNode):
    def __init__(self):
        self.foo = TreeNode()
        self.bar = TreeNode()

root = TreeNode(parent=None)
root.a = CustomNode()

我现在有一个部分解决方案,我 TreeNode.__setattr__() 检查它是否正在分配一个新的 TreeNode,如果是,则命名并注册;但一个缺点是 TreeNode.__init__() 直到 returns 之后才能知道它的 nameparent,这将是更可取的。

我想知道是否有一些巧妙的方法来做我想做的事情,使用 metclasses 或语言的其他一些特性。

一般情况下是没有办法的。 在 Python 中,“名称”只是对实际对象的引用 - 它可以有多个指向它的名称,如 x = y = z = MyNode() 中那样,或者根本没有名称 - 如果将对象放在数据结构如 mylist.append(MyNode()) .

因此,请记住,在这种情况下,即使是语言本身的原生结构也需要将名称重复为字符串 - 例如,在创建命名元组 (point = namedtuple("point", "x y")) 或创建 [=52 时=]es 以编程方式调用 type,如 MyClass = type("MyClass", (), {})

当然,Python 具有自省功能, 中的构造函数 TreeNode 可以从中检索它所在的函数被调用,通过使用 sys._getframe(),然后检索源代码的文本,如果它是像 self.foo = TreeNode() 这样格式良好的简单行,从那里提取名称 foo细绳。 您不应该这样做,出于上述考虑。 (源代码可能并不总是对 运行 程序可用,在这种情况下此方法将不起作用)

如果您总是在方法内部创建节点,那么第二个最直接的事情似乎是添加一个简短的方法来完成它。最直接的还是在这种情况下输入两次名字,就像你在做的那样。

class CustomNode(TreeNode):
    def __init__(self):
        self.add_node("foo")
        self.add_noded("bar")
        excrafurbate(self.foo)  # attribute can be used, as it is set in the method
    def add_node(self, name):
        setattr(self, name, TreeNode(name=name, parent=self))

不过,两次输入姓名的语言存在一些例外情况。对于这种事情的意思是 class 中的固定属性可以有一个特殊的方法 (__set_name__) 通过它他们可以知道他们的名字。但是,它们被设置为 per_class,并且如果您需要在每个 CustomNode 实例中使用单独的 TreeNode 实例,则必须放入一些其他代码,以便以惰性方式实例化新节点,或者当容器 class 被实例化。

在这种情况下,看起来只要在新实例中访问属性时就可以简单地创建一个新的 TreeNode 实例: __set_name__ 的机制是描述符协议——与 Python property 内置函数使用的相同。如果默认情况下创建的新节点为空,这很容易做到 - 然后您可以控制它们的属性:


class ClsTreeNode:
    def __set_name__(self, owner, name):
        self.name = name
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = getattr(instance, "_" + self.name, None)
        if value is None:
            value = TreeNode(name = self.name, parent=instance)
            setattr(instance, "_" + self.name, value)
        return value
    
    def __set__(self, instance, value):
        # if setting a new node is not desired, just raise a ValueError
        if not isinstance(value, TreeNode):
            raise TypeError("...")
        # adjust name in parent inside parent node
        # oterwise create a new one.
        # ...or accept the node content, and create the new TreeNode here, 
        # one is free to do whatever wanted.
        value.name = self.name
        value.patent = instance
        setattr(instance, "_" + self.name, value)



class  CustomNode(TreeNode):
    foo = ClsTreeNode()
    bar = ClsTreeNode()

如前所述,这仅在 ClsTreeNode 是 class 属性时才有效 - 查看 descriptor protocol docs 了解更多详情。

另一种不必输入两次名称的方法将再次属于“hackish,请勿使用”,即:滥用 class 语句。 使用适当的自定义元 class,class foo(metaclass=TreeNodeMeta): pass 语句可以 不需要创建新的 class,而是 return 任何新对象 - 并且对 metaclass __new__ 方法的调用将传递名称 的 class。它仍然必须求助于检查调用堆栈中的 Frame 对象以找出其父对象(同时,如上所示,通过 使用描述符协议,确实可以免费获得父对象)。

有 [traceback] 库 :

This module provides a standard interface to extract, format and print stack traces of Python programs.

这是一个概念证明:

import traceback


def foo():
    stack = traceback.extract_stack()
    frame = stack[-2]  # 0 is the root, -1 is the current one (`foo`), -2 is the one before (the caller)
    # we have some information :
    print(f"{frame.name=} {frame.locals=!r}")
    print(f"{frame.filename=!r} {frame.lineno=}")
    print(f"{frame.line=!r}")
    assign_name = frame.line.split("=")[0].strip()  # FIXME !!!!
    return assign_name


a = foo()
print(a)
frame.name='<module>' frame.locals=None
frame.filename='/home/stack_overflow/so70873290.py' frame.lineno=15
frame.line='a = foo()'
a

它基于从 Python call stack 中获取前一帧。在您的 __init__ 方法中使用它可以知道分配给它的名称。

但是它有很多问题:

  • 如果我不分配会发生什么,只需调用您的 TreeNode ?它不会知道它的名字。
  • 如果我分配给 self.something.else.foo 会怎样?节点名称会是 something.else.foo 吗?如果 something 是一个 TreeNode 怎么办?

此外,如果我的分配分布在多行,我使用 traceback 的解决方案将不起作用:

a = (
    foo()
)

在这里,traceback找到了它的极限。在幕后,traceback.extract_stack just formats what's in sys._getframe().f_back.

相反,我们可以使用真正的交易:Python 字节码!虽然inspect may be used for that purpose, here I used dis

import dis
import itertools
import sys


def foo():
    frame = sys._getframe()  # /!\ It is not guaranteed to exist in all implementations of Python.
    lasti = frame.f_back.f_lasti

    assign_instruction = next(
        itertools.dropwhile(
            lambda inst: inst.opcode != 90,  # same as `opname != "STORE_NAME"`
            itertools.dropwhile(
                lambda inst: inst.offset <= lasti,
                dis.get_instructions(frame.f_back.f_code)  # generator
            )
        ),
        None
    )
    return assign_instruction.argval


a = (
    foo()
)
print(a)
a

但是正如 jsbueno 的回答中所解释的那样,我警告不要使用它。但所有这些都是标准库,谨慎使用以实现有用的功能(例如 traceback.walk 的工作方式)。

求助:

  • How should I understand the output of dis.dis?

如果没有非常智能的 AST 元编程,我认为您无法实现您想要实现的目标。所以这给我们留下了选择。如果您的层次结构是静态的,您可以进行两阶段初始化。

您使用子字段的类型定义类型,并将它们的名称列为属性注释。并使用装饰器或元类对其进行检测。我更喜欢装饰器:meta类 并且继承会变得非常讨厌。

@node
class TopNode:
    subnode1: OtherNode1
    subnode2: OtherNode2

    def init(self, arg1, arg2=None):
        self.subnode1.init(arg1)
        self.subnode2.init(arg1, arg2=arg2)
        self.a = 1

不是 __init__ 中的 运行 常规 call-then-setattr 实例化,而是调用 init 函数,该函数作用于已实例化的对象。这很像 C++ 类 的工作方式。在你问之前,我不认为 data类 会在这里工作,或者如果他们会,那会很痛苦。