如何正确实现__str__和__repr__方法

6

在我的几个类中,我想要同时实现__str____repr__,通常最终的代码看起来像这样:

class MyClass(object):
    def __init__(self, a):
        self.a = a

    def __str__(self):
        return 'MyClass({})'.format(self.a)

    def __repr__(self):
        return 'MyClass({!r})'.format(self.a)

这段代码的作用和我预期的一样:

>>> myobject = MyClass(np.array([1, 2]))
>>> str(myobject)
'MyClass([1 2])'
>>> repr(myobject)
'MyClass(array([1, 2]))'

然而,这段代码违反了DRY原则,并且随着参数数量的增加,维护变得困难。我经常发现__str____repr__中的一个与另一个“不同步”。

有没有更好的方法在不重复的情况下同时实现__str____repr__


1
strrepr返回两个不同的字符串真的有必要吗? - Mark Ransom
1
你可以简单地说__str__ = __repr__(或反之亦然)以复制实现。 - jasonharper
它不一定要返回不同的字符串,但通常更可取。也许这个例子太简单了,无法传达这一点。 - Jonas Adler
1
你可能想要了解attrs - Ashwini Chaudhary
5个回答

5

由于你的__str____repr__遵循相同的模式,因此你可以编写一个函数为对象创建字符串表示形式。它将接受一个对象、一个属性列表以及strrepr作为参数:

def stringify(obj, attrs, strfunc):
    values = []
    # get each attribute's value and convert it to a string
    for attr in attrs:
        value = getattr(obj, attr)
        values.append(strfunc(value))

    # get the class name
    clsname = type(obj).__name__

    # put everything together
    args = ', '.join(values)
    return '{}({})'.format(clsname, args)

print( stringify(MyClass('foo'), ['a'], repr) )
# output: MyClass('foo')

我建议将此功能放在一个类中,然后继承该类:
class Printable:
    def __str__(self):
        return self.__stringify(str)

    def __repr__(self):
        return self.__stringify(repr)

    def __stringify(self, strfunc):
        values = []
        for attr in self._attributes:
            value = getattr(self, attr)
            values.append(strfunc(value))

        clsname = type(self).__name__
        args = ', '.join(values)
        return '{}({})'.format(clsname, args)

class MyClass(Printable):
    _attributes = ['a']

    def __init__(self, a):
        self.a = a

甚至可以通过直接从__init__函数的签名中获取属性来完全自动化地完成它:

import inspect

class AutoPrintable:
    def __str__(self):
        return self.__stringify(str)

    def __repr__(self):
        return self.__stringify(repr)

    def __stringify(self, strfunc):
        sig= inspect.signature(self.__init__)
        values= []
        for attr in sig.parameters:
            value= getattr(self, attr)
            values.append(strfunc(value))

        clsname= type(self).__name__
        args= ', '.join(values)
        return '{}({})'.format(clsname, args)

class MyClass(AutoPrintable):
    def __init__(self, a, b):
        self.a = a
        self.b = b

print( str(MyClass('foo', 'bar')) ) # output: MyClass(foo, bar)
print( repr(MyClass('foo', 'bar')) ) # output: MyClass('foo', 'bar')

4

官方Python文档和Python增强提案索引似乎都没有明确规定如何覆盖这些方法的清晰指南,除了3.3特殊方法名称之外,其中之一是关于__repr__()的:

如果可能的话,这应该看起来像一个有效的Python表达式,可以用来重新创建具有相同值的对象[...] 这通常用于调试,因此表示形式必须富含信息且不含歧义。

我喜欢从一些标准库模块中实现__repr__()的方式中获得灵感,例如socket.socket

$ python3
>>> from socket import socket
>>> socket()
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>

所以这里的模式是。虽然__repr__()更适合于调试/测试目的,但__str__()的范围更加非正式,我推断甚至可以使用更松散的规则。请注意,如果重写了__repr__()但没有重写__str__(),__repr__()将调用__str__()。在这里,如果我必须选择一些规则,我更喜欢让__str__()类似于__repr__(),但进行修改:显示的项目数量。我不需要像__repr__那样冗长。显示的值类型。我包括最“重要”的值,甚至包括不反映最初传递给__init__()的参数的值。

我正在开发的一款PDF库中有几个例子。其中包括两个类PdfFileReaderPdfFileWriter,它们的__repr__()__str__()方法输出如下:

r = PdfFileReader("samplecode/pdfsamples/jpeg.pdf")
w = PdfFileWriter()

print(r)
print(str(r))
print(repr(r))

print(w)
print(str(w))
print(repr(w))

$ python3 repr.py
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>

请参阅 2. 内置函数 以了解 repr()

对于许多类型,此函数尝试返回一个字符串,该字符串在传递给 eval() 时将生成一个具有相同值的对象,否则表示是一个带有角括号的字符串,其中包含对象类型的名称以及通常包括对象的名称和地址的其他信息。 [...]


3
没有任何规则或明确的指导来实现__str____repr__, 至少在任何地方都没有被一致遵循 (甚至在标准库中都不是如此)。因此,没有办法自动获取“标准行为”,仅仅是因为没有标准行为。这取决于你,在这种情况下,如果你为自己设置了指导方针,也许你可以想出一个实用程序来使其更容易遵循。
例如,在您的情况下,您可以创建一个基类,该基类提供__str____repr__的实现:
class AutoStRepr(object):
    _args = []
    def __repr__(self):
        return '{}({})'.format(type(self).__name__,
            ', '.join(repr(getattr(self, a)) for a in self._args))
    def __str__(self):
        return '{}({})'.format(type(self).__name__,
            ', '.join(str(getattr(self, a)) for a in self._args))

你可以将其用于许多不同类型的应用中:
class MyClass(AutoStRepr):
    _args = ['a']
    def __init__(self, a):
        self.a = a

class MyOtherClass(AutoStRepr):
    _args = ['a', 'bc']
    def __init__(self, a, b, c):
        self.a = a
        self.bc = b * c

>>> MyClass('foo')
MyClass('foo')
>>> MyOtherClass('foo', 2, 5)
MyOtherClass('foo', 10)

不是一个坏答案,但从某种程度上来说,这与原始解决方案一样违反了DRY原则 - 你可能想把那两个庞大的.format移到一个帮助函数中。 - Aran-Fey
1
@Rawing 那些“大量连接”?它们为什么是大量的?这只是在一个简单的生成器表达式上进行的连接。而且,不,这不违反DRY原则;任何避免重复的解决方案都会增加一些没有实际好处的极其复杂的东西。DRY原则是关于避免重复相同的逻辑。这里有两次,实际上是不同的逻辑,可能会进一步分歧。 - poke

2
不需要重复,只需不实现__str__
这样,对象的行为就像__str__ = __repr__
我认为你还应该阅读这个答案

我非常清楚不重复的可能性,但对于“大型”对象来说,这里的差异(仅为array(...))可能相当大,并且同时具有str和repr可能很有用。 - Jonas Adler
然后遵循__repr__是无歧义的,__str__是可读的准则。具体实现完全取决于您对无歧义和可读性的定义。 - bergerg
我认为这是一个不好的想法,因为:__str__ 的目的是通过 str() 返回一个可用和可读的表示。但是,__repr__ 的目的是通过 eval(repr()) 返回一个用于重现实例的字符串。 - Sukombu

0

这个例子怎么样?

#!/usr/bin/env python3

class Quaternion:
    _x: float = 0.0
    _y: float = 0.0
    _z: float = 0.0
    _w: float = 1.0

    @property
    def x(self) -> float:
        return self._x

    @property
    def y(self) -> float:
        return self._y

    @property
    def z(self) -> float:
        return self._z

    @property
    def w(self) -> float:
        return self._w

    def __init__(self, x: float, y: float, z: float, w: float) -> None:
        self._x = float(x)
        self._y = float(y)
        self._z = float(z)
        self._w = float(w)

    def __str__(self) -> str:
        return ", ".join(
            (
                str(self._x),
                str(self._y),
                str(self._z),
                str(self._w)
            )
        )

    def __repr__(self) -> str:
        cls = self.__class__
        module = cls.__module__
        return f"{module + '.' if module != '__main__' else ''}{cls.__qualname__}({str(self)})"

优点

  • str() 返回纯值:0.0, 0.0, 0.0, 1.0
  • repr() 返回类似于:Quaternion(0.0, 0.0, 0.0, 1.0)mymodule.Quaternion(0.0, 0.0, 0.0, 1.0) (如果它在一个导入的子模块中)。因此,您可以通过eval(repr())轻松地重现它。

缺点

目前还没有发现任何缺点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接