本节书摘来自异步社区《Python面向对象编程指南》一书中的第2章,第2.1节,作者[美]Steven F. Lott, 张心韬 兰亮 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第2章 与Python无缝集成——基本特殊方法
Python中有一些特殊方法,它们允许我们的类和Python更好地集成。在标准库参考(Standard Library Reference)中,它们被称为基本特殊方法,是与Python的其他特性无缝集成的基础。
例如,我们用字符串来表示一个对象的值。Object基类包含了__repr__()和__str__()的默认实现,它们提供了一个对象的字符串描述。遗憾的是,这些默认的实现不够详细。我们几乎总会想重写它们中的一个或两个。我们还会介绍__format__(),它更加复杂一些,但是和上面两个方法的作用相同。
我们还会介绍其他的转换方法,尤其是__hash__()、__bool__()和__bytes__()。这些方法可以把一个对象转换成一个数字、一个布尔值或者一串字节。例如,当我们实现了__bool__(),我们就可以像下面这样在if语句中使用我们的对象:if someobject:。
接下来,我们会介绍实现了比较运算符的几个特殊方法:__lt__()、__le__()、__eq__()、__ne__()、__gt__()和__ge__()。
当我们定义一个类时,几乎总是需要使用这些基本的特殊方法。
我们会在最后介绍__new__()和__del__(),因为它们的使用更加复杂,而且相比于其他的特殊方法,我们并不会经常使用它们。
我们会详细地介绍如何用这些特殊方法来扩展一个简单类。我们需要了解从object继承而来的默认行为,这样,我们才能理解应该在什么时候使用重写,以及如何使用它。
2.1 __repr__()和__str__()方法
对于一个对象,Python提供了两种字符串表示。它们和内建函数repr()、str()、print()及string.format()的功能是一致的。
- 通常,str()方法表示的对象对用户更加友好。这个方法是由对象的__str__方法实现的。
- repr()方法的表示通常会更加技术化,甚至有可能是一个完整的Python表达式。文档中写道:
对于大多数类型,这个方法会尝试给出和调用eval()一样的结果。
这个方法是由__repr__()方法实现的。
- print()函数会调用str()来生成要输出的对象。
- 字符串的format()函数也可以使用这些方法。当我们使用{!r}或者{!s}格式时,我们实际上分别调用了__repr__()或者__str__()方法。
下面我们先来看一下这些方法的默认实现。
下面是一个很简单的类。
class Card:
insure= False
def __init__( self, rank, suit ):
self.suit= suit
self.rank= rank
self.hard, self.soft = self._points()
class NumberCard( Card ):
def _points( self ):
return int(self.rank), int(self.rank)
我们定义了两个简单类,每个类包含4个属性。
下面是在命令行中使用NumberCard类的结果。
>>> x=NumberCard( '2', '')
>>>str(x)
'<__main__.NumberCard object at 0x1013ea610>'
>>>repr(x)
'<__main__.NumberCard object at 0x1013ea610>'
>>>print(x)
<__main__.NumberCard object at 0x1013ea610>
可以看到,__str__()和__repr__()的默认实现并不能提供非常有用的信息。
在以下两种情况下,我们可以考虑重写__str__()和__repr__()。
- 非集合对象:一个不包括任何其他集合对象的“简单”对象,这类对象的格式化通常不会特别复杂。
- 集合对象:一个包含集合的对象,这类对象的格式化会更为复杂。
2.1.1 非集合对象的__str__()和__repr__()
正如我们在前面看到的,__str__()和__repr__()并没有提供有用的信息,我们几乎总是需要重载它们。下面是当对象中不包括集合时我们可以使用的一种方法。这些方法是我们前面定义的Card类的方法。
def __repr__( self ):
return "{__class__.__name__}(suit={suit!r}, rank={rank!r})".
format(
__class__=self.__class__, **self.__dict__)
def __str__( self ):
return "{rank}{suit}".format(**self.__dict__)
这两个方法依赖于如何将对象的内部实例变量__dict__传递给format()函数。这种方式对于使用__slots__的函数并不合适,通常来说,这些都是不可变的对象。在格式规范中使用名字可以让格式化更加可读,不过它也让格式化模板更长。以__repr__()为例,我们传递了__dict__和__class__作为format()函数的参数。
格式化模板使用了两种格式化的规范。
- {__class__.__name__}模板,有时候也被写成{__class__.__name__!s},提供了类名的简单字符串表示。
- {suit!r}和{rank!r}模板,它们都使用了!r格式规范来给repr()方法提供属性值。
以__str__()为例,我们只传递了对象的__dict__,而内部则是隐式使用了{!s}格式规范来提供str()方法的属性值。
2.1.2 集合中的__str__()和__repr__()
涉及集合的时候,我们需要格式化集合中的单个对象以及这些对象的整体容器。下面是一个包含__str__()和__repr__()的简单集合。
class Hand:
def __init__( self, dealer_card, *cards ):
self.dealer_card= dealer_card
self.cards= list(cards)
def __str__( self ):
return ", ".join( map(str, self.cards) )
def __repr__( self ):
return "{__class__.__name__}({dealer_card!r}, {_cards_str})".
format(
__class__=self.__class__,
_cards_str=", ".join( map(repr, self.cards) ),
**self.__dict__ )
__str__()方法很简单。
1.调用map函数对集合中的每个对象使用str()方法,这会基于返回的字符串集合创建一个迭代器。
2.用”,”.join()将所有对象的字符串表示连接成一个长字符串。
__repr__()方法更加复杂。
1.调用map函数对集合中的每个对象应用repr()方法,这会基于返回的结果集创建一个迭代器。
2.使用”.”.join()连接所有对象的字符串表示。
3.用__class__、集合字符串和__dict__中的不同属性创建一些关键字。我们将集合字符串命名为_card_str,这样就不会和现有的属性冲突。
4.用”{__class__.__name__}({dealer_card!r}, {_cards_str})”.format()来连接类名和之前连接的对象字符串。我们使用!r格式化来保证属性也会使用repr()来转换。
在一些情况下,我们可以优化这个过程,让它更加简单。在格式化中使用位置参数可以在一定程度上简化模板字符串。