解释一:看懂了就不用看第二种了
__init__()方法是Python学习当中重要的基础知识,__init__()方法意义重大的原因有两个。第一个原因是在对象生命周期中初始化是最重要的一步;每个对象必须正确初始化后才能正常工作。第二个原因是__init__()参数值可以有多种形式。
因为有很多种方式为__init__()提供参数值,对于对象创建有大量的用例,我们可以看看其中的几个。我们想尽可能的弄清楚,因此我们需要定义一个初始化来正确的描述问题区域。在我们接触__init__()方法之前,无论如何,我们都需要粗略、简单地看看在Python中隐含的object类的层次结构。
隐含的超类——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__()实现,我们不需要去实现__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__()方法应该让实例变量显式。
可怜的多态
灵活和愚蠢就在一念之间。
当我们觉得需要像下面这样写的时候,我们正从灵活的边缘走向愚蠢:
1 | 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超类中的通用初始化可以适用于三个子类NumberCard、AceCard和FaceCard。
这是一种常见的多态设计。每一个子类都提供一个唯一的_points()方法实现。所有子类都有相同的签名:有相同的方法和属性。这三个子类的对象在一个应用程序中可以交替使用。
如果我们为花色使用简单的字符,我们可以创建Card实例,如下所示:
1 | cards = [AceCard( 'A' , '?' ), NumberCard( '2' , '?' ), NumberCard( '3' , '?' ),] |
我们在列表中枚举出一些牌的类、牌值和花色。从长远来说,我们需要更智能的工厂函数来创建Card实例;用这个方法枚举52张牌无聊且容易出错。在我们接触工厂函数之前,我们看一些其他问题。
使用init()创建显式常量
可以给牌定义花色类。在二十一点中,花色无关紧要,简单的字符串就可以。
我们使用花色构造函数作为创建常量对象的示例。在许多情况下,我们应用中小部分对象可以通过常量集合来定义。小部分的静态对象可能是实现策略模式或状态模式的一部分。
在某些情况下,我们会有一个在初始化或配置文件中创建的常量对象池,或者我们可以基于命令行参数创建常量对象。我们会在第十六章《通过命令进行复制》中获取初始化设计和启动设计的详细信息。
Python没有简单正式的机制来定义一个不可变对象,我们将在第三章《属性访问、方法属性和描述符》看看保证不可变性的相关技术。在本示例中,花色不可变是有道理的。
下面这个类,我们将用于创建四个显式常量:
class Suit: def __init__( self , name, symbol): self .name = name self .symbol = symbol |
下面是通过这个类创建的常量:
1 | Club, Diamond, Heart, Spade = Suit( 'Club' , '?' ), Suit( 'Diamond' , '?' ), Suit( 'Heart' , '?' ), Suit( 'Spade' , '?' ) |
现在我们可以通过下面展示的代码片段创建cards:
1 | cards = [AceCard( 'A' , Spade), NumberCard( '2' , Spade), NumberCard( '3' , Spade),] |
这个小示例,这种方法对于单个特性的花色代码来说并不是一个巨大的进步。在更复杂的情况下,会有一些策略或状态对象通过这个方式创建。通过从小的、静态的常量对象中复用可以使策略或状态设计模式更有效率。
我们必须承认,在Python中这些对象并不是技术上一成不变的,它是可变的。进行额外的编码使得这些对象真正不变可能会有一些好处。
无关紧要的不变性
不变性很有吸引力但却容易带来麻烦。有时候被神话般的“恶意程序员”在他们的应用程序中通过修改常量值进行调整。从设计上考虑,这是非常愚蠢的。这些神话般的、恶意的程序员不会停止这样做,因为已经没有更好的方法去更简洁简单的在Python中编码。恶意程序员访问到源码并且修改它仅仅是希望尽可能轻松地编写代码来修改一个常数。
在定义不可变对象的类的时候最好不要挣扎太久。
解释2:与1基本相同,但有简化
__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.xexcept AttributeError:
是时候重新考虑API并添加一个通用的方法或属性。重构比添加if
语句更明智。
在超类中实现__init__()
我们通过实现__init__()
方法来初始化对象。当一个对象被创建,Python首先创建一个空对象并为该新对象调用__init__()
方法。这个方法函数通常用来创建对象的实例变量并执行任何其他一次性处理。
下面是Card
类示例定义的层次结构。我们将定义Card
超类和三个子类,这三个子类是Card
的变种。两个实例变量直接由参数值设置,并通过初始化方法计算:
class Card:def __init__(self, rank, suit):self.suit = suitself.rank = rankself.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, 11class FaceCard(Card):def _points(self):return 10, 10
在这个示例中,我们提取__init__()
方法到超类,这样在Card
超类中的通用初始化可以适用于三个子类NumberCard
、AceCard
和FaceCard
。
这是一种常见的多态设计。每一个子类都提供一个唯一的_points()
方法实现。所有子类都有相同的签名:有相同的方法和属性。这三个子类的对象在一个应用程序中可以交替使用。
如果我们为花色使用简单的字符,我们可以创建Card
实例,如下所示:
cards = [AceCard('A', ''), NumberCard('2',''), NumberCard('3',''),]
我们在列表中枚举出一些牌的类、牌值和花色。从长远来说,我们需要更智能的工厂函数来创建Card
实例,用这个方法枚举52张牌无聊且容易出错。在我们接触工厂函数之前,我们看一些其他问题。
使用__init__()
创建显而易见的常量
可以给牌定义花色类。在二十一点中,花色无关紧要,简单的字符串就可以。
我们使用花色构造函数作为创建常量对象示例。在许多情况下,我们应用中小部分对象可以通过常量集合来定义。小部分的静态对象可能是实现策略模式或状态模式的一部分。
在某些情况下,我们会有一个在初始化或配置文件中创建的常量对象池,或者我们可以基于命令行参数创建常量对象。我们会在第十六章《命令行处理》中获取初始化设计和启动设计的详细信息。
Python没有简单正式的机制来定义一个不可变对象,我们将在第三章《属性访问、特性和描述符》中看看保证不可变性的相关技术。在本示例中,花色不可变是有道理的。
下面这个类,我们将用于创建四个显而易见的常量:
class Suit:def __init__(self, name, symbol):self.name = nameself.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的程序中提供合适的诊断信息来展示如何实现不变性。
通过工厂函数对 __init__()
加以利用
我们可以通过工厂函数来构建一副完整的扑克牌。这会比枚举所有52张扑克牌要好得多。在Python中,我们有如下两种常见的工厂方法:
定义一个函数,该函数会创建所需类的对象。
定义一个类,该类有创建对象的方法。这是一个完整的工厂设计模式,正如设计模式书所描述的那样。在诸如Java这样的语言中,工厂类层次结构是必须的,因为该语言不支持独立的函数。
在Python中,类不是必须的。只有当相关的工厂非常复杂的时候才会显现出优势。Python的优势就是当一个简单的函数可以做的更好时我们决不强迫使用类层次结构。
虽然这是一本关于面向对象编程的书,但函数真是一个好东西。这是常见也是最地道的Python。
如果需要的话,我们总是可以重写一个函数为适当的可调用对象,可以将一个可调用对象重构到我们的工厂类层次结构中。我们将在第五章《使用Callables和Contexts》中学习可调用对象。
一般,类定义的优点是通过继承实现代码重用。工厂类的函数就是包装一些目标类层次结构和复杂对象的构造。如果我们有一个工厂类,当扩展目标类层次结构的时候,我们可以添加子类到工厂类中。这给我们提供了多态工厂类,不同的工厂类定义具有相同的方法签名,可以交替使用。
这个类级别的多态对于静态编译语言如Java或C++非常有用。编译器可以解决类和方法生成代码的细节。
如果选择的工厂定义不能重用任何代码,则类层次结构在Python中不会有任何帮助。我们可以简单的使用具有相同签名的函数。
以下是我们各种Card
子类的工厂函数:
def card(rank, suit):if rank == 1:return AceCard('A', suit)elif 2 <= rank < 11:return NumberCard(str(rank), suit)elif 11 <= rank < 14:name = {11: 'J', 12: 'Q', 13: 'K' }[rank]return FaceCard(name, suit)else:raise Exception("Rank out of range")
这个函数通过rank
数值和suit
对象构建Card
类。现在我们可以更简单的构建牌了。我们已经将构造过程封装到一个单一的工厂函数中处理,允许应用程序在不知道精确的类层次结构和多态设计是如何工作的情况下进行构建。
下面是如何通过这个工厂函数构建一副牌的示例:
deck = [card(rank, suit) for rank in range(1,14) for suit in (Club, Diamond, Heart, Spade)]
它枚举了所有的牌值和花色来创建完整的52张牌。
1. 错误的工厂设计和模糊的else子句
注意card()
函数里面的if
语句结构。我们没有使用“包罗万象”的else
子句来做任何处理;我们只是抛出异常。使用“包罗万象”的else
子句会引出相关的小争论。
一方面,从属于else
子句的条件不能不言而喻,因为它可能隐藏着细微的设计错误。另一方面,一些else
子句确实是显而易见的。
重要的是要避免含糊的else
子句。
考虑下面工厂函数定义的变体:
def card2(rank, suit):if rank == 1:return AceCard('A', suit)elif 2 <= rank < 11:return NumberCard(str(rank), suit)else:name = {11: 'J', 12: 'Q', 13: 'K'}[rank]return FaceCard(name, suit)
以下是当我们尝试创建整副牌将会发生的事情:
deck2 = [card2(rank, suit) for rank in range(13) for suit in (Club, Diamond, Heart, Spade)]
它起作用了吗?如果if
条件更复杂了呢?
一些程序员扫视的时候可以理解这个if
语句。其他人将难以确定是否所有情况都正确执行了。
对于Python高级编程,我们不应该把它留给读者去演绎条件是否适用于else
子句。对于菜鸟来说条件应该是显而易见的,至少也应该是显式的。
何时使用“包罗万象”的else
尽量的少使用,使用它只有当条件是显而易见的时候。当有疑问时,显式的使用并抛出异常。
避免含糊的else
子句。
2. 简单一致的使用elif序列
我们的工厂函数card()
是两种常见工厂设计模式的混合物:
if-elif
序列映射
为了简单起见,最好是专注于这些技术的一个而不是两个。
我们总是可以用映射来代替elif
条件。(是的,总是。但相反是不正确的;改变elif
条件为映射将是具有挑战性的。)
以下是没有映射的Card
工厂:
def card3(rank, suit):if rank == 1:return AceCard('A', suit)elif 2 <= rank < 11:return NumberCard(str(rank), suit)elif rank == 11:return FaceCard('J', suit)elif rank == 12:return FaceCard('Q', suit)elif rank == 13:return FaceCard('K', suit)else:raise Exception("Rank out of range")
我们重写了card()
工厂函数。映射已经转化为额外的elif
子句。这个函数有个优点就是它比之前的版本更加一致。
3. 简单的使用映射和类对象
在一些示例中,我们可以使用映射来代替一连串的elif
条件。很可能发现条件太复杂,这个时候或许只有使用一连串的elif
条件来表达才是明智的选择。对于简单示例,无论如何,映射可以做的更好且可读性更强。
因为class
是最好的对象,我们可以很容易的映射rank
参数到已经构造好的类中。
以下是仅使用映射的Card
工厂:
def card4(rank, suit):class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard)return class_(rank, suit)
我们已经映射rank
对象到类中。然后,我们给类传递rank
值和suit
值来创建最终的Card
实例。
最好我们使用defaultdict
类。无论如何,对于微不足道的静态映射不会比这更简单了。看起来像下面代码片段那样:
defaultdict(lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard})
注意:defaultdict
类默认必须是无参数的函数。我们已经使用了lambda
创建必要的函数来封装常量。这个函数,无论如何,都有一些缺陷。对于我们之前版本中缺少1
到A
和13
到K
的转换。当我们试图增加这些特性时,一定会出现问题的。
我们需要修改映射来提供可以和字符串版本的rank
对象一样的Card
子类。对于这两部分的映射我们还可以做什么?有四种常见解决方案:
可以做两个并行的映射。我们不建议这样,但是会强调展示不可取的地方。
可以映射个二元组。这个同样也会有一些缺点。
可以映射到
partial()
函数。partial()
函数是functools
模块的一个特性。可以考虑修改我们的类定义,这种映射更容易。可以在下一节将
__init__()
置入子类定义中看到。
我们来看看每一个具体的例子。
3.1. 两个并行映射
以下是两个并行映射解决方案的关键代码:
class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard)rank_str = {1:'A', 11:'J', 12:'Q', 13:'K'}.get(rank, str(rank))return class_(rank_str, suit)
这并不可取的。它涉及到重复映射键1
、11
、12
和13
序列。重复是糟糕的,因为在软件更新后并行结构依然保持这种方式。
不要使用并行结构
并行结构必须使用元组或一些其他合适的集合来替代。
3.2. 映射到元组的值
以下是二元组映射的关键代码:
class_, rank_str= {1: (AceCard,'A'),11: (FaceCard,'J'),12: (FaceCard,'Q'),13: (FaceCard,'K'),}.get(rank, (NumberCard, str(rank)))return class_(rank_str, suit)
这是相当不错的,不需要过多的代码来分类打牌中的特殊情况。当我们需要改变Card
类层次结构来添加额外的Card
子类时,我们可以看到它是如何被修改或被扩展。
将rank
值映射到一个类对象的确让人感觉奇怪,且只有类初始化所需两个参数中的一个。将牌值映射到一个简单的类或没有提供一些混乱参数(但不是所有)的函数对象似乎会更合理。
3.3. partial函数解决方案
相比映射到函数的二元组和参数之一,我们可以创建一个partial()
函数。这是一个已经提供一些(但不是所有)参数的函数。我们将从functools
库中使用partial()
函数来创建一个带有rank
参数的partial类。
以下是将rank
映射到partial()
函数,可用于对象创建:
from functools import partialpart_class = {1: partial(AceCard, 'A'),11: partial(FaceCard, 'J'),12: partial(FaceCard, 'Q'),13: partial(FaceCard, 'K'),}.get(rank, partial(NumberCard, str(rank)))return part_class(suit)
映射将rank
对象与partial()
函数联系在一起,并分配给part_class
。这个partial
()函数可以被应用到suit
对象来创建最终的对象。partial()
函数是一种常见的函数式编程技术。它在我们有一个函数来替代对象方法这一特定的情况下使用。
不过总体而言,partial()
函数对于大多数面向对象编程并没有什么帮助。相比创建partial()
函数,我们可以简单地更新类的方法来接受不同组合的参数。partial()
函数类似于给对象创建一个流畅的接口。
3.4. 连贯的工厂类接口
在某些情况下,我们设计的类在方法使用上定义好了顺序,按顺序求方法的值很像partial()
函数。
在一个对象表示法中我们可能会有x.a().b()
。我们可以把它当成x(a, b)
。x.a()
函数是等待b()
的一类partial()
函数。我们可以认为它就像x(a)(b)
那样。
这里的概念是,Python给我们提供两种选择来管理状态。我们既可以更新对象又可以创建有状态性的(在某种程度上)partial()
函数。由于这种等价,我们可以重写partial()
函数到一个流畅的工厂对象中。使得rank
对象的设置为一个流畅的方法来返回self
。设置suit
对象将真实的创建Card
实例。
以下是一个流畅的Card
工厂类,有两个方法函数,必须在特定顺序中使用:
class CardFactory:def rank(self, rank):self.class_, self.rank_str = {1: (AceCard, 'A'),11: (FaceCard,'J'),12: (FaceCard,'Q'),13: (FaceCard,'K'),}.get(rank, (NumberCard, str(rank)))return selfdef suit(self, suit):return self.class_(self.rank_str, suit)
rank()
方法更新构造函数的状态,suit()
方法真实的创建了最终的Card
对象。
这个工厂类可以像下面这样使用:
card8 = CardFactory()deck8 = [card8.rank(r+1).suit(s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
首先,我们创建一个工厂实例,然后我们使用那个实例创建Card
实例。这并没有实质性改变__init__()
在Card
类层次结构中的运作方式。然而,它确实改变了我们应用程序创建对象的方式。
在各个子类中实现__init__()
当我们看到创建Card
对象的工厂函数,再看看Card
类设计。我想我们可能要重构牌值转换功能,因为这是Card
类自身应该负责的内容。这会将初始化向下延伸到每个子类。
这需要共用的超类初始化以及特定的子类初始化。我们要谨遵Don't Repeat Yourself(DRY)原则来保持代码可以被克隆到每一个子类中。
下面的示例展示了每个子类初始化的职责:
class Card:passclass NumberCard(Card):def __init__(self, rank, suit):self.suit = suitself.rank = str(rank)self.hard = self.soft = rankclass AceCard(Card):def __init__(self, rank, suit):self.suit = suitself.rank = "A"self.hard, self.soft = 1, 11class FaceCard(Card):def __init__(self, rank, suit):self.suit = suitself.rank = {11: 'J', 12: 'Q', 13: 'K'}[rank]self.hard = self.soft = 10
这仍是清晰的多态。然而,缺乏一个真正的共用初始化,会导致一些冗余。缺点在于重复初始化suit
,所以必须将其抽象到超类中。各子类的__init__()
会对超类的__init__()
做显式的引用。
该版本的Card
类有一个超类级别的初始化函数用于各子类,如下面代码片段所示:
class Card:def __init__(self, rank, suit, hard, soft):self.rank = rankself.suit = suitself.hard = hardself.soft = softclass NumberCard(Card):def __init__(self, rank, suit):super().__init__(str(rank), suit, rank, rank)class AceCard(Card):def __init__(self, rank, suit):super().__init__("A", suit, 1, 11)class FaceCard(Card):def __init__(self, rank, suit):super().__init__({11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10)
我们在子类和父类都提供了__init__()
函数。好处是简化了我们的工厂函数,如下面代码片段所示:
def card10(rank, suit):if rank == 1:return AceCard(rank, suit)elif 2 <= rank < 11:return NumberCard(rank, suit)elif 11 <= rank < 14:return FaceCard(rank, suit)else:raise Exception("Rank out of range")
简化工厂函数不应该是我们关注的焦点。不过我们从这可以看到一些变化,我们创建了比较复杂的__init__()
函数,而对工厂函数却有一些较小的改进。这是比较常见的权衡。
工厂函数封装复杂性
在复杂的__init__()
方法和工厂函数之间有个权衡。最好就是坚持更直接,更少程序员友好的__init__()
方法,并将复杂性推给工厂函数。如果你想封装复杂结构,工厂函数可以做的很好。
简单复合对象
复合对象也可被称为容器。我们来看一个简单的复合对象:一副单独的牌。这是一个基本的集合。事实上它是如此基本,以至于我们不用过多的花费心思,直接使用简单的list
做为一副牌。
在设计一个新类之前,我们需要问这个问题:使用一个简单的list
是否合适?
我们可以使用random.shuffle()
来洗牌和使用deck.pop()
发牌到玩家手里。
一些程序员急于定义新类就像使用内置类一样草率,这很容易违反面向对象的设计原则。我们要避免一个新类像如下代码片段所示:
d = [card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]random.shuffle(d)hand = [d.pop(), d.pop()]
如果就这么简单,为什么要写一个新类?
答案并不完全清楚。一个好处是,提供一个简化的、未实现接口的对象。正如我们前面提到的工厂函数一样,但在Python中类并不是一个硬性要求。
在前面的代码中,一副牌只有两个简单的用例和一个似乎并不够简化的类定义。它的优势在于隐藏实现的细节,但细节是如此微不足道,揭露它们几乎没有任何意义。在本章中,我们的关注主要放在__init__()
方法上,我们将看一些创建并初始化集合的设计。
设计一个对象集合,有以下三个总体设计策略:
封装:该设计模式是现有的集合的定义。这可能是Facade设计模式的一个例子。
继承:该设计模式是现有的集合类,是普通子类的定义。
多态:从头开始设计。我们将在第六章看看《创建容器和集合》。
这三个概念是面向对象设计的核心。在设计一个类的时候我们必须总是这样做选择。
1. 封装集合类
以下是封装设计,其中包含一个内部集合:
class Deck:def __init__(self):self._cards = [card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]random.shuffle(self._cards)def pop(self):return self._cards.pop()
我们已经定义了Deck
,内部集合是一个list
对象。Deck
的pop()
方法简单的委托给封装好的list
对象。
然后我们可以通过下面这样的代码创建一个Hand
实例:
d = Deck()hand = [d.pop(), d.pop()]
一般来说,Facade设计模式或封装好方法的类是简单的被委托给底层实现类的。这个委托会变得冗长。对于一个复杂的集合,我们可以委托大量方法给封装的对象。
2. 继承集合类
封装的另一种方法是继承内置类。这样做的优势是没有重新实现pop()
方法,因为我们可以简单地继承它。
pop()
的优点就是不用写过多的代码就能创建类。在这个例子中,继承list
类的缺点是提供了一些我们不需要的函数。
下面是继承内置list
的Deck
定义:
class Deck2(list):def __init__(self):super().__init__(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade))random.shuffle(self)
在某些情况下,为了拥有合适的类行为,我们的方法将必须显式地使用超类。在下面的章节中我们将会看到其他相关示例。
我们利用超类的__init__()
方法填充我们的list
对象来初始化单副扑克牌,然后我们洗牌。pop()
方法只是简单从list
继承过来且工作完美。从list
继承的其他方法也能一起工作。
3. 更多的需求和另一种设计
在赌场中,牌通常从牌盒发出,里面有半打喜忧参半的扑克牌。这个原因使得我们有必要建立自己版本的Deck
,而不是简单、纯粹的使用list
对象。
此外,牌盒里的牌并不完全发完。相反,会插入标记牌。因为有标记牌,有些牌会被保留,而不是用来玩。
下面是包含多组52张牌的Deck
定义:
class Deck3(list):def __init__(self, decks=1):super().__init__()for i in range(decks):self.extend(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade))random.shuffle(self)burn = random.randint(1, 52)for i in range(burn):self.pop()
在这里,我们使用super().__init__()
来构建一个空集合。然后,我们使用self.extend()
添加多次52张牌。由于我们在这个类中没有使用覆写,所以我们可以使用super().extend()
。
我们还可以通过super().__init__()
,使用更深层嵌套的生成器表达式执行整个任务。如下面代码片段所示:
(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks))
这个类为我们提供了一个Card
实例的集合,我们可以使用它来模仿赌场21点发牌的盒子。
在赌场有一个奇怪的仪式,他们会翻开废弃的牌。如果我们要设计一个记牌玩家策略,我们可能需要效仿这种细微差别。
复杂复合对象
以下是21点Hand
类描述的一个例子,很适合模拟玩家策略:
class Hand:def __init__(self, dealer_card):self.dealer_card = dealer_cardself.cards = []def hard_total(self):return sum(c.hard for c in self.cards)def soft_total(self):return sum(c.soft for c in self.cards)
在这个例子中,我们有一个基于__init__()
方法参数的self.dealer_card
实例变量。self.cards
实例变量是不基于任何参数的。这个初始化创建了一个空集合。
我们可以使用下面的代码去创建一个Hand
实例
d = Deck()h = Hand(d.pop())h.cards.append(d.pop())h.cards.append(d.pop())
缺点就是有一个冗长的语句序列被用来构建一个Hand
的实例对象。它难以序列化Hand
对象并像这样初始化来重建。尽管我们在这个类中创建一个显式的append()
方法,它仍将采取多个步骤来初始化集合。
我们可以尝试创建一个接口,但这并不是一件简单的事情,对于Hand
对象它只是在语法上发生了变化。接口仍然会导致多种方法计算。当我们看到第2部分中的《序列化和持久化》,我们倾向于使用接口,一个类级别的函数,理想情况下,应该是类的构造函数。我们将在第9章的《序列化和存储——JSON、YAML、Pickle、CSV和XML》深入研究。
还要注意一些不完全遵循21点规则的方法功能。在第二章《通过Python无缝地集成——基本的特殊方法》中我们会回到这个问题。
1. 复杂复合对象初始化
理想情况下,__init__()
方法会创建一个对象的完整实例。这是一个更复杂的容器,当你在创建一个包含内部其他对象集合的完整实例的时候。如果我们可以一步就能构建这个复合对象,它将是非常有帮助的。
逐步增加项目的方法和一步加载所有项目的方法是一样的。
例如,我们可能有如下面的代码片段所示的类:
class Hand2:def __init__(self, dealer_card, *cards):self.dealer_card = dealer_cardself.cards = list(cards)def hard_total(self):return sum(c.hard for c in self.cards)def soft_total(self):return sum(c.soft for c in self.cards)
这个初始化一步就设置了所有实例变量。另一个方法就是之前那样的类定义。我们可以有两种方式构建一个Hand2
对象。第一个示例一次加载一张牌到Hand2
对象:
d = Deck()P = Hand2(d.pop())p.cards.append(d.pop())p.cards.append(d.pop())
第二个示例使用*cards
参数一步加载一序列的Card
类:
d = Deck()h = Hand2(d.pop(), d.pop(), d.pop())
对于单元测试,在一个声明中使用这种方式通常有助于构建复合对象。更重要的是,这种简单、一步的计算来构建复合对象有利于下一部分的序列化技术。
参考:https://segmentfault.com