翻譯:《實用的Python編程》04_02_Inheritance
- 2021 年 3 月 8 日
- 筆記
- Python, 實用的Python編程
目錄 | 上一節 (4.1 類) | 下一節 (4.3 特殊方法)
4.2 繼承
繼承(inheritance)是編寫可擴展程式程式的常用手段。本節對繼承的思想(idea)進行探討。
簡介
繼承用於特殊化現有對象:
class Parent:
...
class Child(Parent):
...
新類 Child
稱為派生類(derived class)或子類(subclass)。類 Parent
稱為基類(base class)或超類(superclass)。在子類名後的括弧 ()
中指定基類(Parent
),class Child(Parent):
。
擴展
使用繼承,你可以獲取現有的類,並且可以:
- 添加新方法
- 重新定義現有方法
- 向實例添加新屬性
最後,你擴展了現有程式碼。
示例
假設這是開始的類:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
你可以通過繼承更改 Stock 類的任何部分。
添加新方法
class MyStock(Stock):
def panic(self):
self.sell(self.shares)
(譯註:「panic」 在這裡表示的是「panic selling」,恐慌性拋售)
使用示例:
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>
重新定義現有方法
class MyStock(Stock):
def cost(self):
return 1.25 * self.shares * self.price
使用示例:
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>
新的 cost() 方法代替了舊的 cost() 方法。其它的方法不受影響。
方法覆蓋
有時候,一個類既想擴展現有方法,同時又想在新的定義中使用原有的實現。為此,可以使用 super()
函數實現(譯註:方法覆蓋
有時也譯為 方法重寫
):
class Stock:
...
def cost(self):
return self.shares * self.price
...
class MyStock(Stock):
def cost(self):
# Check the call to `super`
actual_cost = super().cost()
return 1.25 * actual_cost
使用內置函數 super()
調用之前的版本。
注意:在 Python 2 中,語法更加冗餘,像下面這樣:
actual_cost = super(MyStock, self).cost()
__init__
和繼承
如果 __init__
方法在子類中被重新定義,那麼有必要初始化父類。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
class MyStock(Stock):
def __init__(self, name, shares, price, factor):
# Check the call to `super` and `__init__`
super().__init__(name, shares, price)
self.factor = factor
def cost(self):
return self.factor * super().cost()
你需要使用 super
調用父類的 __init__()
方法,如前所示,這是調用先前版本的方法。
使用繼承
有時候,繼承用於組織相關的對象。
class Shape:
...
class Circle(Shape):
...
class Rectangle(Shape):
...
要組織相關的對象,可以考慮使用邏輯層次結構或者進行分類。然而,一種更常見(更實用)的做法是創建可重用和可擴展的程式碼。例如,一個框架可能會定義一個基類,並指導你對其進行自定義。
class CustomHandler(TCPHandler):
def handle_request(self):
...
# Custom processing
基類包含了通用程式碼。你的類繼承基類並自定義特殊的部分。
「is a」 關係
繼承建立了一種類型關係。
class Shape:
...
class Circle(Shape):
...
檢查對象實例:
>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>
重要提示:理想情況下,任何使用父類實例能正常工作的程式碼也能使用子類的實例正常工作。
object
基類
如果一個類沒有父類,那麼有時候你會看到它們使用 object
作為基類。
class Shape(object):
...
在 Python 中,object
是所有對象的基類。
注意:在技術上,它不是必需的,但是你通常會看到 object
在 Python 2 中被保留。如果省略,類仍然隱式繼承自 object
。
多重繼承
你可以通過在類定義中指定多個基類來實現多重繼承。
class Mother:
...
class Father:
...
class Child(Mother, Father):
...
Child
類繼承了兩個父類(Mother,Father)的特性。這裡有一些相當棘手的細節。除非你知道你正在做什麼,否則不要這樣做。雖然更多資訊會在下一節給到,但是我們不會在本課程中進一步使用多重繼承。
練習
繼承的一個主要用途是:以各種方式編寫可擴展和可訂製的程式碼——尤其是在庫或框架中。要說明這點,請考慮 report.py
程式中的 print_report()
函數。它看起來應該像下面這樣:
def print_report(reportdata):
'''
Print a nicely formated table from a list of (name, shares, price, change) tuples.
'''
headers = ('Name','Shares','Price','Change')
print('%10s %10s %10s %10s' % headers)
print(('-'*10 + ' ')*len(headers))
for row in reportdata:
print('%10s %10d %10.2f %10.2f' % row)
當運行 report.py
程式,你應該會獲得像下面這樣的輸出:
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
練習 4.5:擴展性問題
假設你想修改 print_report()
函數,以支援各種不同的輸出格式,例如純文本,HTML, CSV,或者 XML。為此,你可以嘗試編寫一個龐大的函數來實現每一個功能。但是,這樣做可能會導致程式碼非常混亂,無法維護。這是一個使用繼承的絕佳機會。
首先,請關注創建表所涉及的步驟。在表的頂部是標題。標題的後面是數據行。讓我們使用這些步驟把它們放到各自的類中吧。創建一個名為 tableformat.py
的文件,並定義以下類:
# tableformat.py
class TableFormatter:
def headings(self, headers):
'''
Emit the table headings.
'''
raise NotImplementedError()
def row(self, rowdata):
'''
Emit a single row of table data.
'''
raise NotImplementedError()
除了稍後用作定義其它類的設計規範,該類什麼也不做。有時候,這樣的類被稱為「抽象基類」。
請修改 print_report()
函數,使其接受一個 TableFormatter
對象作為輸入,並執行 TableFormatter
的方法來生成輸出。示例:
# report.py
...
def print_report(reportdata, formatter):
'''
Print a nicely formated table from a list of (name, shares, price, change) tuples.
'''
formatter.headings(['Name','Shares','Price','Change'])
for name, shares, price, change in reportdata:
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
formatter.row(rowdata)
因為你在 portfolio_report()
函數中增加了一個參數,所以你也需要修改 portfolio_report()
函數。請修改 portfolio_report()
函數,以便像下面這樣創建 TableFormatter
:
# report.py
import tableformat
...
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.TableFormatter()
print_report(report, formatter)
運行新程式碼:
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
... crashes ...
程式應該會馬上崩潰,並附帶一個 NotImplementedError
異常。雖然這沒有那麼令人興奮,但是結果確實是我們期待的。繼續下一步部分。
練習 4.6:使用繼承生成不同的輸出
在 a 部分定義的 TableFormatter
類旨在通過繼承進行擴展。實際上,這就是整個思想。要說明這點,請像下面這樣定義 TextTableFormatter
類:
# tableformat.py
...
class TextTableFormatter(TableFormatter):
'''
Emit a table in plain-text format
'''
def headings(self, headers):
for h in headers:
print(f'{h:>10s}', end=' ')
print()
print(('-'*10 + ' ')*len(headers))
def row(self, rowdata):
for d in rowdata:
print(f'{d:>10s}', end=' ')
print()
請像下面這樣修改 portfolio_report()
函數:
# report.py
...
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.TextTableFormatter()
print_report(report, formatter)
這應該會生成和之前一樣的輸出:
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
但是,讓我們更改輸出為其它內容。定義一個以 CSV 格式生成輸出的 CSVTableFormatter
。
# tableformat.py
...
class CSVTableFormatter(TableFormatter):
'''
Output portfolio data in CSV format.
'''
def headings(self, headers):
print(','.join(headers))
def row(self, rowdata):
print(','.join(rowdata))
請像下面這樣修改主程式:
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.CSVTableFormatter()
print_report(report, formatter)
然後,你應該會看到像下面這樣的 CSV 輸出:
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
運用類似的思想,定義一個 HTMLTableFormatter
類,生成具有以下輸出的表格:
<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>
請通過修改主程式來測試你的程式碼。 主程式創建的是 HTMLTableFormatter
對象,而不是 CSVTableFormatter
對象。
練習 4.7:多態
面向對象編程(oop)的一個主要特性是:可以將對象插入程式中,並且不必更改現有程式碼即可運行。例如,如果你編寫了一個預期會使用 TableFormatter
對象的程式,那麼不管你給它什麼類型的 TableFormatter
,它都能正常工作。這樣的行為有時被稱為「多態」。
一個需要指出的潛在問題是:弄清楚如何讓用戶選擇它們想要的格式。像 TextTableFormatter
一樣直接使用類名通常有點煩人。因此,你應該考慮一些簡化的方法。如:你可以在程式碼中嵌入 if
語句:
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
if fmt == 'txt':
formatter = tableformat.TextTableFormatter()
elif fmt == 'csv':
formatter = tableformat.CSVTableFormatter()
elif fmt == 'html':
formatter = tableformat.HTMLTableFormatter()
else:
raise RuntimeError(f'Unknown format {fmt}')
print_report(report, formatter)
雖然在此程式碼中,用戶可以指定一個簡化的名稱(如'txt'
或 'csv'
)來選擇格式,但是,像這樣在 portfolio_report()
函數中使用大量的 if
語句真的是最好的思想嗎?把這些程式碼移入其它通用函數中可能更好。
在 tableformat.py
文件中,請添加一個名為 create_formatter(name)
的函數,該函數允許用戶創建給定輸出名(如'txt'
,'csv'
,或 'html'
)的格式器(formatter)。請像下面這樣修改 portfolio_report()
函數:
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.create_formatter(fmt)
print_report(report, formatter)
嘗試使用不同的格式調用該函數,確保它能夠正常工作。
練習 4.8:匯總
請修改 report.py
程式,以便 portfolio_report()
函數使用可選參數指定輸出格式。示例:
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
請修改主程式,以便可以在命令行上指定輸出格式:
bash $ python3 report.py Data/portfolio.csv Data/prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
bash $
討論
在庫和框架中,編寫可擴展程式是繼承的最常見用途之一。例如,框架指導你定義一個自己的對象,該對象繼承自已提供的基類。然後你可以添加實現各種功能的函數。
另一個更深層次的概念是「擁有抽象的思想」。在練習中,我們定義了自己的類,用于格式化表格。你可能會看一下自己的程式碼,然後告訴自己「我應該只使用格式化庫或其它人已經編寫的東西!」。不,你應該同時使用自己的類和庫。使用自己的類可以降低程式的耦合性,增加程式的靈活性。只要你的程式使用的應用介面來自於自己定義的類,那麼,只要你想,你就可以更改程式的內部實現以使其按照你想的那樣工作。你可以編寫全訂製(all-custom)程式碼,也可以使用第三方包(package)。當發現更好的包時,你可以將一個第三方包替換為另一個包。這並不重要——只要你保留這個介面,應用程式程式碼都不會中斷。這是一種強大的思想,這也是為什麼應該使用繼承的原因之一。
也就是說,設計面向對象的程式可能會非常困難。想了解更多資訊,你可能應該尋找一本有關設計模式主題的書看一下(儘管理解本練習中的內容已經讓你以一種實用的方式在使用對象方面走得很遠了)。
目錄 | 上一節 (4.1 類) | 下一節 (4.3 特殊方法)
註:完整翻譯見 //github.com/codists/practical-python-zh