詳細解讀Python中的__init__()方法

虎撲掌門人發表於2016-02-28

__init__()方法意義重大的原因有兩個。第一個原因是在物件生命週期中初始化是最重要的一步;每個物件必須正確初始化後才能正常工作。第二個原因是__init__()引數值可以有多種形式。

因為有很多種方式為__init__()提供引數值,對於物件建立有大量的用例,我們可以看看其中的幾個。我們想盡可能的弄清楚,因此我們需要定義一個初始化來正確的描述問題區域。

在我們接觸__init__()方法之前,無論如何,我們都需要粗略、簡單地看看在Python中隱含的object類的層次結構。

在這一章,我們看看不同形式的簡單物件的初始化(例如:打牌)。在這之後,我們還可以看看更復雜的物件,就像包含集合的hands物件以及包含策略和狀態的players。
隱含的超類——object

每一個Python類都隱含了一個超類:object。它是一個非常簡單的類定義,幾乎不做任何事情。我們可以建立object的例項,但是我們不能用它做太多,因為許多特殊的方法容易丟擲異常。

當我們自定義一個類,object則為超類。下面是一個類定義示例,它使用新的名稱簡單的繼承了object:

?
1
2
class X:
  pass

下面是和自定義類的一些互動:

?
1
2
3
4
>>> X.__class__
<class 'type'>
>>> X.__class__.__base__
<class 'object'>

我們可以看到該類是type類的一個物件,且它的基類為object。

就像在每個方法中看到的那樣,我們也看看從object繼承的預設行為。在某些情況下,超類特殊方法的行為是我們所想要的。在其他情況下,我們需要覆蓋這個特殊方法。
基類物件的init()方法

物件生命週期的基礎是建立、初始化和銷燬。我們將建立和銷燬的高階特殊方法推遲到後面的章節中,目前只關注初始化。

所有類的超類object,有一個預設包含pass的__init__()實現,我們不需要去實現__init__()。如果不實現它,則在物件建立後就不會建立例項變數。在某些情況下,這種預設行為是可以接受的。

我們總是給物件新增屬性,該物件為基類object的子類。思考以下類,需要兩個例項變數但不初始化它們:

?
1
2
3
class Rectangle:
  def area(self):
    return self.length * self.width

Rectangle類有一個使用兩個屬性來返回一個值的方法。這些屬性沒有初始化。這是合法的Python程式碼。它可以有效的避免專門設定屬性,雖然感覺有點奇怪,但是有效。

下面是於Rectangle類的互動:

?
1
2
3
4
>>> r = Rectangle()
>>> r.length, r.width = 13, 8
>>> r.area()
104

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

無論如何,這個設計給予了很大的靈活性,這樣有時候我們不用在__init__()方法中設定所有屬性。至此我們走的很順利。一個可選屬性其實就是一個子類,只是沒有真正的正式宣告為子類。我們建立多型在某種程度上可能會引起混亂以及if語句的不恰當使用所造成的盤繞。雖然未初始化的屬性可能是有用的,但很有可能是糟糕設計的前兆。

《Python之禪》中的建議:

    "顯式比隱式更好。"

一個__init__()方法應該讓例項變數顯式。

可憐的多型

靈活和愚蠢就在一念之間。

當我們覺得需要像下面這樣寫的時候,我們正從靈活的邊緣走向愚蠢:

?
1
if 'x' in self.__dict__:

或者:

?
1
2
3
try:
  self.x
except AttributeError:

是時候重新考慮API並新增一個通用的方法或屬性。重構比新增if語句更明智。
在超類中實現init()

我們通過實現__init__()方法來初始化物件。當一個物件被建立,Python首先建立一個空物件,然後為那個新物件呼叫__init__()方法。這個方法函式通常用來建立物件的例項變數並執行任何其他一次性處理。

下面是Card類示例定義的層次結構。我們將定義Card超類和三個子類,這三個子類是Card的變種。兩個例項變數直接由引數值設定,兩個變數通過初始化方法計算:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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沒有簡單正式的機制來定義一個不可變物件,我們將在第三章《屬性訪問、方法屬性和描述符》看看保證不可變性的相關技術。在本示例中,花色不可變是有道理的。

下面這個類,我們將用於建立四個顯式常量:

?
1
2
3
4
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中編碼。惡意程式設計師訪問到原始碼並且修改它僅僅是希望儘可能輕鬆地編寫程式碼來修改一個常數。

相關文章