目录 | 上一节 (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.shares75>>> s.panic()>>> s.shares0>>>
从新定义现有办法
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 = priceclass 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.pyclass 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.pyimport 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,ChangeAA,100,9.22,-22.98IBM,50,106.28,15.18CAT,150,35.46,-47.98MSFT,200,20.89,-30.34GE,95,13.48,-26.89MSFT,50,20.89,-44.21IBM,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 csvName,Shares,Price,ChangeAA,100,9.22,-22.98IBM,50,106.28,15.18CAT,150,35.46,-47.98MSFT,200,20.89,-30.34GE,95,13.48,-26.89MSFT,50,20.89,-44.21IBM,100,106.28,35.84bash $
探讨
在库和框架中,编写可扩大程序是继承的最常见用处之一。例如,框架领导你定义一个本人的对象,该对象继承自已提供的基类。而后你能够增加实现各种性能的函数。
另一个更深层次的概念是“领有形象的思维”。在练习中,咱们定义了本人的类,用于格式化表格。你可能会看一下本人的代码,而后通知本人“我应该只应用格式化库或其它人曾经编写的货色!”。不,你应该同时应用本人的类和库。应用本人的类能够升高程序的耦合性,减少程序的灵活性。只有你的程序应用的利用接口来自于本人定义的类,那么,只有你想,你就能够更改程序的外部实现以使其依照你想的那样工作。你能够编写全定制(all-custom)代码,也能够应用第三方包(package)。当发现更好的包时,你能够将一个第三方包替换为另一个包。这并不重要——只有你保留这个接口,利用程序代码都不会中断。这是一种弱小的思维,这也是为什么应该应用继承的起因之一。
也就是说,设计面向对象的程序可能会十分艰难。想理解更多信息,你可能应该寻找一本无关设计模式主题的书看一下(只管了解本练习中的内容曾经让你以一种实用的形式在应用对象方面走得很远了)。
目录 | 上一节 (4.1 类) | [下一节 (4.3 非凡办法)]()
注:残缺翻译见 https://github.com/codists/practical-python-zh