继承和组合是面向对象的程序设计中的两个主要概念,它们为两个类之间的关系建模。它们驱动应用程序的设计,并确定随着添加新功能或需求变更,应用程序应如何发展。
它们都支持代码重用,但是它们以不同的方式来实现
继承为关系建模。这意味着,当您有一个Derived
从类继承的Base
类时,您创建了一个关系,其中Derived
是的专门版本Base
。
继承是通过统一建模语言或UML通过以下方式表示的:
类以框的形式表示,框的名称在顶部。继承关系由派生类中指向基类的箭头表示。这个词延伸,通常添加到箭头。
注意:在继承关系中:
假设您有一个基类,Animal
并且您从基类派生了一个Horse
类。继承关系规定a Horse
为 Animal
。这意味着Horse
继承了的接口和实现Animal
,并且Horse
对象可用于替换Animal
应用程序中的对象。
这就是所谓的Liskov替代原理。原则指出:“在计算机程序中,如果S
是的子类型,则可以用类型的T
对象T
替换类型的对象,S
而无需更改程序的任何所需属性”。
您将在本文中看到为什么在创建类层次结构时应始终遵循Liskov替换原理,否则将遇到问题。
合成是一个模型,该模型具有关系。它可以通过组合其他类型的对象来创建复杂类型。这意味着一个类Composite
可以包含另一个类的对象Component
。这种关系意味着a Composite
有一个 Component
。
UML表示组成如下:
合成通过在复合类上指向组件类的菱形线条表示。复合端可以表达关系的基数。基数表示该类将包含的Component
实例数或有效范围Composite
。
在上图中,1
表示Composite
类包含一个type类型的对象Component
。基数可以通过以下方式表示:
数字表示Component
中包含的实例数Composite
。
*符号表示Composite
该类可以包含可变数量的Component
实例。
范围1..4表示Composite
该类可以包含一系列Component
实例。用最小和最大实例数或最小和许多实例(如1 .. *中)指示范围。
注意:包含其他类的对象的类通常称为组合,其中用于创建更复杂类型的类称为组件。
例如,您的Horse
类可以由类型另一个对象组成Tail
。合成使您可以通过说一个Horse
有一个 来表达这种关系Tail
。
组合使您可以通过将对象添加到其他对象来重用代码,这与继承其他类的接口和实现相反。既Horse
和Dog
类可以利用的功能性Tail
通过组合物在不脱离其他导出一个类。
Python中的所有内容都是一个对象。模块是对象,类定义和函数是对象,当然,从类创建的对象也是对象。
继承是每种面向对象编程语言的必需功能。这意味着Python支持继承,并且正如您将在后面看到的那样,它是支持多重继承的少数几种语言之一。
使用类编写Python代码时,即使您不知道在使用继承,也在使用继承。让我们看看这意味着什么。
在Python中查看继承的最简单方法是跳入Python交互式外壳并编写一些代码。您将从编写最简单的类开始:
>>> class MyClass:
... pass
...
您声明了一个MyClass
不会做太多事情的类,但是它将说明最基本的继承概念。现在已经声明了类,您可以使用该dir()
函数列出其成员了:
>>> c = MyClass()
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__']
dir()
返回指定对象中所有成员的列表。您尚未在中声明任何成员MyClass
,因此列表来自何处?您可以使用交互式解释器进行查找:
>>> o = object()
>>> dir(o)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
如您所见,这两个列表几乎相同。有一些附加的部件MyClass
等__dict__
和__weakref__
,但每单件object
类也存在于MyClass
。
这是因为您在Python中创建的每个类都隐式地派生自object
。您可以更加明确和易于编写class MyClass(object):
,但这是多余且不必要的。
注意:在Python 2中,您必须object
出于超出本文讨论范围的原因而明确地从中派生,但是您可以在Python 2文档的“ 新样式和经典类”部分中进行阅读。
您在Python中创建的每个类都将隐式派生自object
。该规则的异常是用于通过引发异常来指示错误的类。
您可以使用Python交互式解释器查看问题:
>>> class MyError:
... pass
...
>>> raise MyError()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exceptions must derive from BaseException
您创建了一个新类来指示错误类型。然后,您尝试使用它引发异常。引发了一个异常,但是输出指出该异常的类型TypeError
不是not MyError
并且为all exceptions must derive from BaseException
。
BaseException
是为所有错误类型提供的基类。若要创建新的错误类型,您必须从BaseException
或从其派生类中派生您的类。Python中的约定是从派生自定义错误类型Exception
,而自定义错误类型又从派生BaseException
。
定义错误类型的正确方法如下:
>>> class MyError(Exception):
... pass
...
>>> raise MyError()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.MyError
如您所见,当您引发时MyError
,输出正确地指出了引发的错误的类型。
继承是用于创建相关类的层次结构的机制。这些相关的类将共享一个将在基类中定义的公共接口。派生类可以通过提供适用的特定实现来专门化接口。
在本部分中,您将开始为HR系统建模。该示例将演示继承的使用以及派生类如何提供基本类接口的具体实现。
人力资源系统需要处理公司员工的薪资,但是根据员工薪资的计算方式,员工的类型有所不同。
首先,实现一个PayrollSystem
处理工资单的类:
# In hr.py
class PayrollSystem:
def calculate_payroll(self, employees):
print('Calculating Payroll')
print('===================')
for employee in employees:
print(f'Payroll for: {employee.id} - {employee.name}')
print(f'- Check amount: {employee.calculate_payroll()}')
print('')
该PayrollSystem
工具一个.calculate_payroll()
是需要员工的收集和打印他们的方法id
,name
使用,并取适量.calculate_payroll()
暴露每一个员工对象的方法。
现在,您实现一个基类Employee
,该基类处理每种员工类型的公共接口:
# In hr.py
class Employee:
def __init__(self, id, name):
self.id = id
self.name = name
Employee
是所有员工类型的基类。它由id
和构成name
。您在说的是,每个人都Employee
必须有一个id
分配的名称。
人力资源系统要求每个Employee
处理人员必须提供一个.calculate_payroll()
界面,该界面返回员工的每周薪水。该接口的实现因的类型而异Employee
。
例如,行政管理人员的薪水是固定的,因此每周获得的薪水是相同的:
# In hr.py
class SalaryEmployee(Employee):
def __init__(self, id, name, weekly_salary):
super().__init__(id, name)
self.weekly_salary = weekly_salary
def calculate_payroll(self):
return self.weekly_salary
您创建一个SalaryEmployee
继承的派生类Employee
。类初始化与id
和name
基类要求,并使用super()
初始化基类的成员。您可以使用Python super()super()
在“ 增强类”中阅读所有内容。
SalaryEmployee
还需要一个weekly_salary
初始化参数,该参数代表员工每周的收入。
该类提供.calculate_payroll()
了HR系统使用的必需方法。实现只返回存储在中的金额weekly_salary
。
该公司还雇用按小时计薪的制造工人,因此您HourlyEmployee
在HR系统中添加了:
# In hr.py
class HourlyEmployee(Employee):
def __init__(self, id, name, hours_worked, hour_rate):
super().__init__(id, name)
self.hours_worked = hours_worked
self.hour_rate = hour_rate
def calculate_payroll(self):
return self.hours_worked * self.hour_rate
所述HourlyEmployee
类被初始化id
并且name
,像基类,再加上hours_worked
和hour_rate
计算工资必需的。.calculate_payroll()
通过返回工作时间乘以小时费率来实现该方法。
最终,公司雇用了销售助理,这些销售助理通过固定薪金加上根据其销售的佣金支付,因此您可以创建一个CommissionEmployee
类:
# In hr.py
class CommissionEmployee(SalaryEmployee):
def __init__(self, id, name, weekly_salary, commission):
super().__init__(id, name, weekly_salary)
self.commission = commission
def calculate_payroll(self):
fixed = super().calculate_payroll()
return fixed + self.commission
您派生CommissionEmployee
自SalaryEmployee
这两个类都weekly_salary
需要考虑。同时,CommissionEmployee
使用commission
基于员工销售额的值初始化。
.calculate_payroll()
利用基类的实现来检索fixed
薪水并增加佣金值。
由于从CommissionEmployee
派生SalaryEmployee
,您可以weekly_salary
直接访问该属性,并且可以.calculate_payroll()
使用该属性的值来实现。
直接访问属性的问题在于,如果SalaryEmployee.calculate_payroll()
更改了实现,则还必须更改的实现CommissionEmployee.calculate_payroll()
。最好依靠基类中已经实现的方法并根据需要扩展功能。
您为系统创建了一流的层次结构。这些类的UML图如下所示:
该图显示了类的继承层次结构。派生的类实现IPayrollCalculator
接口,这是所需的PayrollSystem
。该PayrollSystem.calculate_payroll()
实现要求employee
传递的对象包含id
,name
和calculate_payroll()
实施。
接口的表示类似于类,接口名称上方带有单词interface。接口名称通常以大写字母作为前缀I
。
该应用程序创建其员工,并将其传递到薪资系统以处理薪资:
# In program.py
import hr
salary_employee = hr.SalaryEmployee(1, 'John Smith', 1500)
hourly_employee = hr.HourlyEmployee(2, 'Jane Doe', 40, 15)
commission_employee = hr.CommissionEmployee(3, 'Kevin Bacon', 1000, 250)
payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll([
salary_employee,
hourly_employee,
commission_employee
])
您可以在命令行中运行该程序并查看结果:
$ python program.py
Calculating Payroll
===================
Payroll for: 1 - John Smith
- Check amount: 1500
Payroll for: 2 - Jane Doe
- Check amount: 600
Payroll for: 3 - Kevin Bacon
- Check amount: 1250
该程序创建三个雇员对象,每个派生类一个。然后,它创建薪资系统,并将员工列表传递给其.calculate_payroll()
方法,该方法计算每个员工的薪资并打印结果。
注意Employee
基类如何不定义.calculate_payroll()
方法。这意味着,如果您要创建一个普通Employee
对象并将其传递给PayrollSystem
,则会出现错误。您可以在Python交互式解释器中尝试一下:
>>> import hr
>>> employee = hr.Employee(1, 'Invalid')
>>> payroll_system = hr.PayrollSystem()
>>> payroll_system.calculate_payroll([employee])
Payroll for: 1 - Invalid
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/hr.py", line 39, in calculate_payroll
print(f'- Check amount: {employee.calculate_payroll()}')
AttributeError: 'Employee' object has no attribute 'calculate_payroll'
虽然可以实例化一个Employee
对象,但是不能使用该对象PayrollSystem
。为什么?因为它不能.calculate_payroll()
为Employee
。为了满足的要求PayrollSystem
,您需要将Employee
当前为具体类的类转换为抽象类。这样一来,没有一个员工会成为Employee
一个实现的员工.calculate_payroll()
。
Employee
上面示例中的类是所谓的抽象基类。存在要继承的抽象基类,但从未实例化。Python提供了abc
定义抽象基类的模块。
您可以在类名称中使用前导下划线来传达不应创建该类的对象的信息。下划线提供了一种防止滥用代码的友好方法,但是它们并不能阻止热心的用户创建该类的实例。
Python标准库中的abc
模块提供了防止从抽象基类创建对象的功能。
您可以修改Employee
类的实现以确保无法实例化:
# In hr.py
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, id, name):
self.id = id
self.name = name
@abstractmethod
def calculate_payroll(self):
pass
您Employee
从派生ABC
,使其成为抽象的基类。然后,.calculate_payroll()
用@abstractmethod
decorator装饰该方法。
此更改有两个很好的副作用:
Employee
不能创建类型的对象。hr
模块上工作的开发人员,如果他们从派生Employee
,那么他们必须重写.calculate_payroll()
abstract方法。您会看到Employee
无法使用交互式解释器创建类型的对象:
>>> import hr
>>> employee = hr.Employee(1, 'abstract')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Employee with abstract methods
calculate_payroll
输出显示该类无法实例化,因为它包含抽象方法calculate_payroll()
。派生类必须重写该方法,以允许创建其类型的对象。
当您从另一个类派生一个类时,派生类将继承这两个类:
大多数时候,您将希望继承一个类的实现,但是您将希望实现多个接口,因此可以在不同情况下使用您的对象。
现代编程语言的设计考虑了这一基本概念。它们允许您从单个类继承,但是您可以实现多个接口。
在Python中,您不必显式声明接口。可以使用实现所需接口的任何对象代替另一个对象。这就是所谓的鸭子打字。鸭子打字通常被解释为“如果表现得像鸭子,那就是鸭子。”
为了说明这一点,您现在将DisgruntledEmployee
在上面的示例中添加一个并非源自的类Employee
:
# In disgruntled.py
class DisgruntledEmployee:
def __init__(self, id, name):
self.id = id
self.name = name
def calculate_payroll(self):
return 1000000
本DisgruntledEmployee
类不从派生Employee
的,但它暴露了所需的相同的接口PayrollSystem
。在PayrollSystem.calculate_payroll()
需要实现以下接口对象的列表:
id
返回员工ID 的属性或属性name
代表雇员的名字属性或特性.calculate_payroll()
不带任何参数并返回工资总额进行处理的方法DisgruntledEmployee
班级满足了所有这些要求,因此PayrollSystem
仍可以计算其工资单。
您可以修改程序以使用DisgruntledEmployee
该类:
# In program.py
import hr
import disgruntled
salary_employee = hr.SalaryEmployee(1, 'John Smith', 1500)
hourly_employee = hr.HourlyEmployee(2, 'Jane Doe', 40, 15)
commission_employee = hr.CommissionEmployee(3, 'Kevin Bacon', 1000, 250)
disgruntled_employee = disgruntled.DisgruntledEmployee(20000, 'Anonymous')
payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll([
salary_employee,
hourly_employee,
commission_employee,
disgruntled_employee
])