能否像Matlab那样在IPython中显示对象的实例变量?
我正在尝试从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 个回答
你可以通过 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
不过,你可能会发现这个方法在处理真正的对象时效果不好,因为这些对象可能有很多实例变量和方法。
我通过实现 _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
显然,在空格等方面还有一些细节需要调整。不过,这大致上就是我想要实现的效果。
简单来说,在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
派生的类的实例”、“任何类的实例”、“任何新式类的实例”,或者“任何类型的任何值”(模块、类、函数等)。你几乎可以在任何东西上使用 dir
和 getmembers
;具体的细节可以在文档中找到。
还有一点:你可能会注意到 getmembers
返回的内容像是 ('__str__', <method-wrapper '__str__' of Spam object at 0x1066be790>
,这可能并不是你感兴趣的。由于结果只是名称-值对,如果你只想去掉 __dunder__
方法、_private
变量等,这很简单。但通常情况下,你可能想根据“成员的类型”进行过滤。getmembers
函数接受一个过滤参数,但文档没有很好地解释如何使用它(而且,还假设你理解描述符是如何工作的)。基本上,如果你想要一个过滤器,通常是 callable
、lambda 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
值是如何组成的。例如,如果你 print
或 repr
一个 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
看看它们的效果)。