首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >连载 Python OOP指南(1)

连载 Python OOP指南(1)

作者头像
公众号---人生代码
发布2020-05-16 22:17:53
7590
发布2020-05-16 22:17:53
举报
文章被收录于专栏:人生代码人生代码

什么是继承与组成?

继承和组合是面向对象的程序设计中的两个主要概念,它们为两个类之间的关系建模。它们驱动应用程序的设计,并确定随着添加新功能或需求变更,应用程序应如何发展。

它们都支持代码重用,但是它们以不同的方式来实现

什么是继承?

继承为关系建模。这意味着,当您有一个Derived从类继承的Base类时,您创建了一个关系,其中Derived 是的专门版本Base

继承是通过统一建模语言或UML通过以下方式表示的:

类以框的形式表示,框的名称在顶部。继承关系由派生类中指向基类的箭头表示。这个词延伸,通常添加到箭头。

注意:在继承关系中:

  • 从另一个继承的类称为派生类,子类或子类型。
  • 派生其他类的类称为基类或超类。
  • 派生类据说可以派生,继承或扩展基类。

假设您有一个基类,Animal并且您从基类派生了一个Horse类。继承关系规定a HorseAnimal。这意味着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

组合使您可以通过将对象添加到其他对象来重用代码,这与继承其他类的接口和实现相反。既HorseDog类可以利用的功能性Tail通过组合物在不脱离其他导出一个类。

Python继承概述

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()是需要员工的收集和打印他们的方法idname使用,并取适量.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。类初始化与idname基类要求,并使用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_workedhour_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

您派生CommissionEmployeeSalaryEmployee这两个类都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传递的对象包含idnamecalculate_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()

Python中的抽象基类

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装饰该方法。

此更改有两个很好的副作用:

  1. 您是在告诉模块用户Employee不能创建类型的对象。
  2. 您要告诉其他在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()。派生类必须重写该方法,以允许创建其类型的对象。

实现继承与接口继承

当您从另一个类派生一个类时,派生类将继承这两个类:

  1. 基类接口:派生类继承基类的所有方法,属性和属性。
  2. 基类实现:派生类继承了实现类接口的代码。

大多数时候,您将希望继承一个类的实现,但是您将希望实现多个接口,因此可以在不同情况下使用您的对象。

现代编程语言的设计考虑了这一基本概念。它们允许您从单个类继承,但是您可以实现多个接口。

在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
])
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CryptoCode 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是继承与组成?
    • 什么是继承?
      • 什么成分?
      • Python继承概述
        • 对象超类
          • 例外是例外
            • 创建类层次结构
              • Python中的抽象基类
                • 实现继承与接口继承
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档