4

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

__init__()方法意义重大的原因有两个。第一是在对象生命周期中初始化是最重要的一步;每个对象必须正确初始化后才能正常工作。第二是__init__()参数值可以有多种形式。

因为有很多种方式为__init__()提供参数值,所以对于对象创建有大量的使用案例,我们可以看看其中的几个。我们想尽可能的弄清楚,因此我们需要定义一个初始化来正确的描述问题域。

在我们接触__init__()方法之前,无论如何,我们都需要简单粗略地看看Python中隐含的object类的层次结构。

在这一章,我们看看不同形式的简单对象的初始化(例如:打牌)。在这之后,我们还可以看看更复杂的对象,就像包含集合的hands以及包含策略和状态的players

隐式超类——object

每一个Python类都隐含了一个超类:object。它是一个非常简单的类定义,几乎不做任何事情。我们可以创建object的实例,但是我们不能用它做太多,因为许多特殊的方法容易抛出异常。

当我们自定义一个类,object则为超类。下面是一个类定义示例,它使用新的名称简单的继承了object

class X:
    pass

下面是和自定义类的一些交互:

>>> X.__class__
<class 'type'>
>>> X.__class__.__base__
<class 'object'>

我们可以看到该类是type类的一个对象,且它的基类为object

就像在每个方法中看到的那样,我们也看看从object继承的默认行为。在某些情况下,超类的特殊方法是我们想要的。而在其他情况下,我们又需要覆盖这个特殊方法。

基类对象的__init__()方法

对象生命周期的基础是它的创建、初始化和销毁。我们将创建和销毁推迟到后面章节的高级特殊方法中讲,目前只关注初始化。

所有类的超类object,有一个默认包含pass__init__()方法,我们不需要去实现它。如果不实现它,则在对象创建后就不会创建实例变量。在某些情况下,这种默认行为是可以接受的。

我们总是给对象添加属性,该对象为基类object的子类。思考下面的类,它需要两个实例变量但不初始化它们:


class Rectangle:
    def area(self):
        return self.length * self.width

Rectangle类有一个使用两个属性来返回一个值的方法。这些属性没有初始化,是合法的Python代码。它可以明确地避免设置属性,虽然感觉有点奇怪,但是合法。

下面是与Rectangle类的交互:

>>> r = Rectangle()
>>> r.length, r.width = 13, 8
>>> r.area()
104

显然这是合法的,但这也是容易混淆的根源,所以也是我们需要避免的原因。

无论如何,这个设计给予了很大的灵活性,这样有时候我们不用在__init__()方法中设置所有属性。至此我们走的很顺利。一个可选属性其实就是一个子类,只是没有真正的正式声明为子类。我们创建多态在某种程度上可能会引起混乱,以及if语句的不恰当使用所造成的盘绕。虽然未初始化的属性可能是有用的,但也很有可能是糟糕设计的前兆。

《Python之禅》中的建议:

"显式比隐式更好。"

一个__init__()方法应该让实例变量显式。

非常差的多态

灵活和愚蠢就在一念之间。

当我们觉得需要像下面这样写的时候,我们正从灵活的边缘走向愚蠢:

if 'x' in self.__dict__:

或者:

try:
    self.x
except AttributeError:

是时候重新考虑API并添加一个通用的方法或属性。重构比添加if语句更明智。

在超类中实现__init__()

我们通过实现__init__()方法来初始化对象。当一个对象被创建,Python首先创建一个空对象并为该新对象调用__init__()方法。这个方法函数通常用来创建对象的实例变量并执行任何其他一次性处理。

下面是Card类示例定义的层次结构。我们将定义Card超类和三个子类,这三个子类是Card的变种。两个实例变量直接由参数值设置,并通过初始化方法计算:


class Card:
    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)

class AceCard(Card):
    def _points(self):
        return 1, 11

class FaceCard(Card):
    def _points(self):
        return 10, 10

在这个示例中,我们提取__init__()方法到超类,这样在Card超类中的通用初始化可以适用于三个子类NumberCardAceCardFaceCard

这是一种常见的多态设计。每一个子类都提供一个唯一的_points()方法实现。所有子类都有相同的签名:有相同的方法和属性。这三个子类的对象在一个应用程序中可以交替使用。

如果我们为花色使用简单的字符,我们可以创建Card实例,如下所示:

cards = [AceCard('A', '♠'), NumberCard('2','♠'), NumberCard('3','♠'),]

我们在列表中枚举出一些牌的类、牌值和花色。从长远来说,我们需要更智能的工厂函数来创建Card实例,用这个方法枚举52张牌无聊且容易出错。在我们接触工厂函数之前,我们看一些其他问题。

使用__init__()创建显而易见的常量

可以给牌定义花色类。在二十一点中,花色无关紧要,简单的字符串就可以。

我们使用花色构造函数作为创建常量对象示例。在许多情况下,我们应用中小部分对象可以通过常量集合来定义。小部分的静态对象可能是实现策略模式状态模式的一部分。

在某些情况下,我们会有一个在初始化或配置文件中创建的常量对象池,或者我们可以基于命令行参数创建常量对象。我们会在第十六章《命令行处理》中获取初始化设计和启动设计的详细信息。

Python没有简单正式的机制来定义一个不可变对象,我们将在第三章《属性访问、特性和描述符》中看看保证不可变性的相关技术。在本示例中,花色不可变是有道理的。

下面这个类,我们将用于创建四个显而易见的常量:


class Suit:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol

下面是通过这个类创建的常量:


Club, Diamond, Heart, Spade = Suit('Club','♣'), Suit('Diamond','♦'), Suit('Heart','♥'), Suit('Spade','♠')

现在我们可以通过下面展示的代码片段创建cards


cards = [AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade),]

这个小示例的方法对于单个字符花色的代码来说并没有多大改进。在更复杂的情况下,会通过这个方式创建一些策略或状态对象。从小的静态常量池中复用对象使得策略或状态设计模式效率更高。

我们必须承认,在Python中这些对象并不是技术上一成不变的,它是可变的。进行额外的编码使得这些对象真正不可变可能会有一些好处。

无关紧要的不变性

不变性很有吸引力但却容易带来麻烦。有时候神话般的“恶意程序员”在他们的应用程序中通过修改常量值进行调整。从设计上考虑,这是非常愚蠢的。这些神话般的、恶意的程序员不会停止这样做。在Python中没有更好的方法保证没有白痴的代码。恶意程序员访问到源码并且修改它仅仅是希望尽可能轻松地编写代码来修改一个常数。

在定义不可变对象的类的时候最好不要挣扎太久。在第三章《属性访问、特性和描述符》中,我们将在有bug的程序中提供合适的诊断信息来展示如何实现不变性。


wanyoung
2k 声望363 粉丝