能否像Matlab那样在IPython中显示对象的实例变量?

6 投票
3 回答
4913 浏览
提问于 2025-04-17 19:34

我正在尝试从Matlab转向Python。虽然IPython中的问号功能很不错,但Matlab有一个非常好的特点,就是你可以在命令行上看到对象的实例变量(在Matlab中称为属性),只需省略分号。请问在Python中也能做到这一点吗?我想可能是通过IPython实现的。

理想情况下,像这样的一个类:

class MyClass(object):
    _x = 5

    @property
    def x(self):
        return self._x + 100

    @x.setter
    def x(self, value):
        self._x = value + 1

    def myFunction(self, y):
        return self.x ** 2 + y

应该显示类似于:

mc = Myclass()
mc
<package.MyClass> <superclass1> <superclass2>

Attributes:
_x: 5
 x: 105

Method Attributes:
myFunction(self, y)

这可以通过重写类的打印方法来实现吗?(如果有这样的东西的话)或者通过IPython中的某个特殊方法呢?

3 个回答

0

你可以通过 obj.__dict__ 来获取一个对象的实例变量,比如:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age


d1 = Dog("Fido", 7)

for key, val in d1.__dict__.items():
    print(key, ": ", val)

输出结果:

age :  7
name :  Fido

不过,你可能会发现这个方法在处理真正的对象时效果不好,因为这些对象可能有很多实例变量和方法。

1

我通过实现 _repr_pretty_,在 IPython 中大致达到了我想要的效果:

def get_public_variables(obj):
    from inspect import getmembers
    return [(name, value) for name, value in
            getmembers(obj, lambda x: not callable(x)) if
            not name.startswith('__')]


class MySuperClass(object):
    def _repr_pretty_(self, p, cycle):
        for (name, value) in get_public_variables(self):
            f = '{:>12}{} {:<} \n'
            line = f.format(str(name), ':', str(value))
            # p.text(str(name) + ': ' + str(value) + '\n')
            p.text(line)

class MyClass(MySuperClass):
    _x = 5

    @property
    def x(self):
        return self._x + 100

这让我得到了:

mc = MyClass()
mc
Out[15]: 
          _x: 5 
           x: 105 

显然,在空格等方面还有一些细节需要调整。不过,这大致上就是我想要实现的效果。

8

简单来说,在Python中没有办法获取一个对象的所有属性列表,因为这些属性可能是动态生成的。举个极端的例子,看看这个类:

>>> class Spam(object):
...     def __getattr__(self, attr):
...         if attr.startswith('x'):
...             return attr[1:]
>>> spam = Spam()
>>> spam.xeggs
'eggs'

即使解释器能够找出所有属性的列表,这个列表也是无限的。

对于简单的类,spam.__dict__ 通常就足够了。它不能处理动态属性、基于 __slots__ 的属性、类属性、C扩展类、从大多数上述内容继承的属性,以及其他各种情况。但至少它是有用的——有时候,这正是你想要的。简单来说,它就是你在 __init__ 或之后明确赋值的东西,没有其他。

如果你想要一个尽量全面且易于人类阅读的结果,可以使用 dir(spam)

如果你想要一个尽量全面且适合程序使用的结果,可以使用 inspect.getmembers(spam)。(实际上,这个实现只是CPython 2.x中对 dir 的一个封装,它 可以 做得更多——实际上在CPython 3.2+中确实做到了。)

这两种方法都能处理很多 __dict__ 不能处理的情况,并且可能会跳过一些在 __dict__ 中但你不想看到的东西。但它们仍然是不完整的。

无论你使用哪种方法,获取值和键都很简单。如果你使用 __dict__getmembers,这很简单;通常情况下,__dict__ 是一个 dict,或者是一个在功能上接近 dict 的东西,而 getmembers 明确返回键值对。如果你使用 dir,你可以很容易地得到一个 dict

{key: getattr(spam, key) for key in dir(spam)}

最后一点:“对象”这个词有点模糊。它可以指“任何从 object 派生的类的实例”、“任何类的实例”、“任何新式类的实例”,或者“任何类型的任何值”(模块、类、函数等)。你几乎可以在任何东西上使用 dirgetmembers;具体的细节可以在文档中找到。

还有一点:你可能会注意到 getmembers 返回的内容像是 ('__str__', <method-wrapper '__str__' of Spam object at 0x1066be790>,这可能并不是你感兴趣的。由于结果只是名称-值对,如果你只想去掉 __dunder__ 方法、_private 变量等,这很简单。但通常情况下,你可能想根据“成员的类型”进行过滤。getmembers 函数接受一个过滤参数,但文档没有很好地解释如何使用它(而且,还假设你理解描述符是如何工作的)。基本上,如果你想要一个过滤器,通常是 callablelambda x: not callable(x),或者是由多个 inspect.isfoo 函数组合而成的 lambda

所以,这种情况很常见,你可能想把它写成一个函数:

def get_public_variables(obj):
    return [(name, value) for name, value 
            in inspect.getmembers(obj, lambda x: not callable(x))
            if not name.startswith('_')]

你可以把它变成一个自定义的IPython %magic 函数,或者只是把它做成一个 %macro,或者就保持它作为一个普通函数并显式调用。


在评论中,你问是否可以把这个打包成一个 __repr__ 函数,而不是尝试创建一个 %magic 函数或其他的。

如果你已经让所有的类都继承自一个根类,这个主意很好。你可以写一个适用于所有类的 __repr__(或者如果它适用于99%的类,你可以在其他1%的类中重写这个 __repr__),然后每次在解释器中评估任何对象或打印它们时,你都会得到你想要的结果。

不过,有几点需要注意:

Python 有 __str__(当你 print 一个对象时得到的)和 __repr__(当你在交互提示符下评估一个对象时得到的)是有原因的。通常,前者是一个易于人类阅读的表示,而后者是可以被 eval(或输入到交互提示符中)的,或者是一个简洁的尖括号形式,能让你区分对象的类型和身份。

这只是一个约定而不是规则,所以你可以自由地打破它。然而,如果你 确实 要打破它,你可能仍然想利用 str/repr 的区别——例如,让 repr 给你一个完整的内部信息,而 str 只显示有用的公共值。

更重要的是,你需要考虑 repr 值是如何组成的。例如,如果你 printrepr 一个 list,你得到的实际上是 '[' + ', '.join(map(repr, item))) + ']'。这在多行的 repr 中看起来会很奇怪。如果你使用任何试图缩进嵌套集合的漂亮打印工具,比如内置于IPython中的那个,结果可能不会难以阅读,但会失去漂亮打印工具的好处。

至于你想显示的具体内容:这都很简单。像这样:

def __repr__(self):
    lines = []

    classes = inspect.getmro(type(self))
    lines.append(' '.join(repr(cls) for cls in classes))

    lines.append('')
    lines.append('Attributes:')
    attributes = inspect.getmembers(self, callable)
    longest = max(len(name) for name, value in attributes)
    fmt = '{:>%s}: {}' % (longest, )
    for name, value in attributes:
        if not name.startswith('__'):
            lines.append(fmt.format(name, value))

    lines.append('')
    lines.append('Methods:')
    methods = inspect.getmembers(self, negate(callable))
    for name, value in methods:
        if not name.startswith('__'):
            lines.append(name)

    return '\n'.join(lines)

右对齐属性名称是这里最难的部分。(而且我可能搞错了,因为这是未经测试的代码……)其他的要么简单,要么有趣(玩不同的过滤器来 getmembers 看看它们的效果)。

撰写回答