Python 类的__repr__方法转换字符串

Python 类的__repr__方法转换字符串,在Python中定义一个自定义类之后,如果尝试在控制台中输出其实例或在解释器会话中查看,并不能得到十分令人满意的结果。默认的“转换成字符串”功能非常原始,缺少细节:

class Car:
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

>>> my_car = Car('red', 37281)
>>> print(my_car)
<__console__.Car object at 0x109b73da0>
>>> my_car
<__console__.Car object at 0x109b73da0>

默认情况下得到的是一个包含类名和对象实例id的字符串(这是CPython中对象的内存地址)。虽然比什么都没有要好,但也没什么用。

你可能会直接打印类的属性,或者向类中添加自定义的to_string()方法来绕过这个问题:

>>> print(my_car.color, my_car.mileage)
red 37281

这个总体思路是正确的,但是忽略了Python将对象转成字符串的约定和内置机制。
因此不要构建自己的字符串转换机制,而是向类中添加双下划线方法__str____repr__。这两个方法以具有Python特色的方式在不同情况下将对象转换为字符串。

来看看这些方法在实践中是如何工作的。首先为前面的Car类增加一个__str__方法:

class Car:
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

    def __str__(self):
        return f'a {self.color} car'

在尝试打印或查看Car实例时,得到的结果稍许有些改进:

>>> my_car = Car('red', 37281)
>>> print(my_car)
'a red car'
>>> my_car
<__console__.Car object at 0x109ca24e0>

在控制台中查看Car对象得到的依然是之前包含对象id的结果,但是打印对象就会得到由新添加的__str__方法返回的字符串。

__str__是Python中的一种双下划线方法,尝试将对象转换为字符串时会调用这个方法:

>>> print(my_car)
a red car
>>> str(my_car)
'a red car'
>>> '{}'.format(my_car)
'a red car'

有了合适的__str__实现,打印对象时就不用担心会直接打印对象属性,也不用编写单独的to_string()函数了。这就是具有Python特色的字符串转换方法。

顺便说一下,有些人把Python的双下划线方法称为“魔法方法”。不过这些方法并没有什么神奇之处,以双下划线起始只是一个命名约定,表示这些方法是Python的核心特性。除此之外,加上双下划线还有助于避免与自己的方法和属性同名。对象构造函数__init__就遵循这样的约定,其中并没有什么神奇或神秘的地方。

因此不要害怕使用Python双下划线方法,这些方法大有用途。

Python 类的repr方法转换字符串 __str____repr__

我们继续来看字符串转换的问题。前一节中,如果在解释器会话中查看my_car,仍然会得到奇怪的<Car object in 0x109ca24e0>

发生这种情况是因为在Python 3中实际上有两个双下划线方法能够将对象转换为字符串。第一个是刚刚介绍的__str__;第二个是__repr__,其工作方式类似于__str__,但用于其他情形。

这里做一个简单的实验,你可以从中了解__str____repr__的使用场景。下面重新定义Car类,添加这两个用来转换成字符串的双下划线方法,用于产生不同的输出:

class Car:
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

    def __repr__(self):
        return '__repr__ for Car'

    def __str__(self):
        return '__str__ for Car'

在尝试上面的示例时,可以看到每个方法生成了对应的字符串转换结果:

>>> my_car = Car('red', 37281)
>>> print(my_car)
__str__ for Car
>>> '{}'.format(my_car)
'__str__ for Car'
>>> my_car
__repr__ for Car

从实验可以得知,在Python解释器会话中查看对象得到的是对象的__repr__结果。

有趣的是,像列表和字典这样的容器总是使用__repr__的结果来表示所包含的对象,哪怕对容器本身调用str()也是如此:

str([my_car])
'[__repr__ for Car]'

为了手动选择其中一种字符串转换方法,比如要更清楚地表示代码的意图,最好使用内置的str()repr()函数。使用这两个函数比直接调用对象的__str____repr__要好,因为结果相同但更美观:

>>> str(my_car)
'__str__ for Car'
>>> repr(my_car)
'__repr__ for Car'

介绍完__str____repr__之后,你可能想知道它们各自在实际使用中的差异。这两个函数的目的看上去相同,因此你可能对其各自的使用场景感到费解。

对于这样的问题,可以看看Python标准库是怎么做的。现在再设计一个实验,创建一个datetime.date对象,看这个对象如何使用__repr____str__来控制字符串转换:

>>> import datetime
>>> today = datetime.date.today()

date对象上,__str__函数的结果侧重于可读性,旨在为人们返回一个简洁的文本表示,以便放心地向用户展示。因此在date对象上调用str()时,得到的是一些看起来像ISO日期格式的东西:

>>> str(today)
'2017-02-02'

__repr__侧重的则是得到无歧义的结果,生成的字符串更多的是帮助开发人员调试程序。为此需要尽可能明确地说明这个对象是什么,因此在对象上调用repr()会得到相对更复杂的结果,其中甚至包括完整的模块和类名称:

>>> repr(today)
'datetime.date(2017, 2, 2)'

复制并粘贴由这个__repr__返回的字符串,可以作为有效的Python语句重新创建原date对象。这种方式很不错,在编写自己的repr时值得借鉴。
然而我发现这个模式实践起来相当困难,通常不值得这么做,因为这只会带来额外的工作。我的经验法则是只要让__repr__生成的字符串对开发人员清晰且有帮助就可以了,并不需要能从中恢复对象的完整状态。

Python 类的repr方法转换字符串 __repr__

如果不提供__str__方法,Python在查找__str__时会回退到__repr__的结果。因此建议总是为自定义类添加__repr__方法,这只需花费很少的时间,但能保证在几乎所有情况下都能得到可用字符串转换结果。

下面介绍如何快速高效地为自定义类添加基本的字符串转换功能。对于前面的Car类,首先添加一个__repr__

def __repr__(self):
    return f'Car({self.color!r}, {self.mileage!r})'

这里使用!r转换标志来确保输出字符串使用的是repr(self.color)repr(self.mileage),而不是str(self.color)str(self.mileage)
虽然这样能正常工作,但缺点是在格式字符串中硬编码了类名称。有一种技巧能避免这种硬编码,即使用对象的__class__.__name__属性来获得以字符串表示的类名称。
这样做的好处是在改变类名称时,不必修改__repr__的实现,因此能更好地遵循“不要重复自己”(DRY)原则:

def __repr__(self):
    return (f'{self.__class__.__name__}('
            f'{self.color!r}, {self.mileage!r})')

其缺点是格式字符串非常长且笨拙。但如果仔细设置格式,依然能保持良好的代码形式,并符合PEP 8规范。
有了上面的__repr__实现,在直接查看对象或调用repr()时能够得到有用的结果:

>>> repr(my_car)
'Car(red, 37281)'

打印对象或调用str()会返回相同的字符串,因为默认的__str__实现只是简单地调用__repr__

>>> print(my_car)
'Car(red, 37281)'
>>> str(my_car)
'Car(red, 37281)'

我认为这是一种标准方法,能做到事半功倍,并且在大部分情况下可以直接使用。因此,我总是会为自定义类添加一个基本的__repr__实现。
下面是一个针对Python 3的完整示例,其中还包括可选的__str__实现:

class Car:
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

    def __repr__(self):
        return (f'{self.__class__.__name__}('
                f'{self.color!r}, {self.mileage!r})')

    def __str__(self):
        return f'a {self.color} car'

Python 类的repr方法转换字符串 与__unicode__的差异

在Python 3中使用str数据类型表示文本,其中用到了unicode字符,可以表示世界上大部分书写系统。

使用不同的数据模型来表示字符串。有两种类型可以表示文本:str(仅限于ASCII字符集)和unicode(等同于Python 3的str

由于这种差异,Python 2中还有另一种双下划线方法能够控制字符串转换:__unicode__。在Python 2中,__str__返回字节,而__unicode__返回字符

大多数情况下应优先使用新的__unicode__方法转换字符串。同时还有一个内置的unicode()函数,该函数会调用相应的双下划线方法,与str()repr()的工作方式相似。

到目前为止还不错,但Python 2中调用__str____unicode__的规则非常古怪:print语句和str()调用__str__;内置的unicode()先调用__unicode__,若没有__unicode__则回退到__str__,此时使用系统文本编码对结果进行解码。
与Python 3相比,这些特殊情况让文本转换规则变得更加复杂。不过能针对实际情况进一步简化。unicode是Python程序中处理文本的首选,同时也是趋势。
所以一般情况下,建议将所有字符串格式化代码放入__unicode__方法中,然后创建一个__str__存根实现,返回以UTF-8编码的unicode表示形式:

def __str__(self):
    return unicode(self).encode('utf-8')

大多数自定义类中的__str__存根函数都是相同的,所以可以根据需要复制和粘贴(或者放到一个基类中)。所有生成针对非开发人员的字符串转换代码都应位于__unicode__中。
下面是一个针对python 2.x的完整示例:

class Car(object):
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

    def __repr__(self):
       return '{}({!r},
           {!r})'.format( self.__class__.__name__,
           self.color, self.mileage)

    def __unicode__(self):
        return u'a {self.color}
            car'.format( self=self)

    def __str__(self):
        return unicode(self).encode('utf-8')

关键要点

  • 使用__str____repr__双下划线方法能够自行控制类中的字符串转换。

  • __str__的结果应该是可读的。__repr__的结果应该是无歧义的。

  • 总是为类添加__repr____str__默认情况下会调用__repr__

  • 在Python 2中使用__unicode__而不是__str__

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程