Inheritance And Composition

2024-01-28

Inheritance and Composition: A Python OOP Guide – Real Python

What’s Inheritance?

is_a

  • Classes that inherit from another are called derived classes, subclasses, or subtypes.

  • Classes from which other classes are derived are called base classes or super classes.

  • A derived class is said to derive, inherit, or extend a base class.

这就是所谓的利斯科夫替换原则。该原则指出,如果 S 是 T 的子类型,那么用 S 类型的对象替换 T 类型的对象不会改变程序的行为。

在创建类层次结构时,你应该始终遵循利斯科夫替换原则,否则你会遇到很多问题。

SOLID Principles: Improve Object-Oriented Design in Python – Real Python

What’s Composition?

has_a

组合是一个建立关系模型的概念。它可以通过组合其他类型的对象来创建复杂类型。这意味着一个 Composite 类可以包含另一个 Component 类的对象。

包含其他类对象的类通常被称为复合类,而用于创建更复杂类型的类则被称为组件。

所有类都继承自 object

完整但冗余(不必要)的写法:

class EmptyClass(object):

cls 与 obj 有几乎相同的成员:

>>> class EmptyClass:
...     pass
...
>>> cls = EmptyClass()
>>> dir(cls)
>>> obj = object()
>>> dir(obj)

Exceptions Are an Exception

在 Python 中创建的每个类都隐式地派生自 object。但是,有一个例外:用于通过引发异常来指示错误的类。

>>> class NotAnError:
...     pass
...
>>> raise NotAnError()
Traceback (most recent call last):
  ...
TypeError: exceptions must derive from BaseException

正确的做法只能是:

>>> class AnError(Exception):
...     pass
...
>>> raise AnError()
Traceback (most recent call last):
  ...
AnError

Creating Class Hierarchies

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("")
# ...

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name
# ...

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
# ...

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate
# ...

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

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

Abstract Base Classes in Python

The Employee class in the example above is what is called an abstract base class. Abstract base classes exist to be inherited, but never instantiated. Python provides the abc module to formally define abstract base classes.

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

您从 ABC 派生出 Employee,使其成为一个抽象基类。然后,使用 @abstractmethod 装饰器装饰 .calculate_payroll() 方法。

这一变化有两个很好的副作用:

你在告诉模块的用户不能创建 Employee 类型的对象。

你是在告诉其他开发 hr 模块的开发人员,如果他们派生自 Employee,那么他们必须覆盖 .calculate_payroll() 抽象方法。

>>> import hr
>>> employee = hr.Employee(1, "Abstract")
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class Employee
⮑ with abstract method calculate_payroll

实现继承与接口继承

当你从一个类派生出另一个类时,派生类会继承以下两个方面:

基类接口:派生类继承基类的所有方法、属性和属性。

基类实现:派生类继承实现类接口的代码。

大多数情况下,你会希望继承一个类的实现,但你也会希望实现多个接口,以便在不同情况下使用你的对象。现代编程语言在设计时就考虑到了这一基本概念。它们允许你继承一个类,但可以实现多个接口。

Duck typing

class DisgruntledEmployee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def calculate_payroll(self):
        return 1_000_000

DisgruntledEmployee 类并不是从 Employee 派生的,但它提供了与 PayrollSystem 所需的相同接口。

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,
    ]
)

PayrollSystem 仍然可以处理新对象,因为它符合所需的接口。

既然不必派生自特定的类,程序就可以重用你的对象,那么你可能会问,为什么要使用继承而不是仅仅实现所需的接口呢?

使用继承重用实现:派生类应充分利用基类的大部分实现。它们还必须模拟一种关系。客户 "类可能也有 .id 和 .name,但 "客户 "不是 "员工",因此在这种情况下,不应使用继承。

实现要重用的接口:当你希望你的类被应用程序的某个特定部分重用时,你可以在你的类中实现所需的接口,但你不需要提供一个基类,也不需要从其他类继承。

重构

基本上,您是在派生类中继承 Employee 类的 .id 和 .name 属性的实现。由于 .calculate_payroll() 只是 PayrollSystem.calculate_payroll() 方法的一个接口,因此无需在 Employee 基类中实现它。

删除了 abc 模块的导入,因为 Employee 类不需要抽象。 还删除了抽象的 .calculate_payroll() 方法,因为它没有提供任何实现。

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("")

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

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

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

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

类爆炸问题

If you’re not careful, inheritance can lead you to a huge hierarchical class structure that’s hard to understand and maintain. This is known as the class explosion problem.

现在,您需要为这些类添加一些功能,以便在新的 ProductivitySystem 中使用它们。ProductivitySystem 可根据员工角色跟踪生产率。员工有不同的角色:

  • Managers: They walk around yelling at people, telling them what to do. They’re salaried employees and make more money.

  • Secretaries: They do all the paperwork for managers and ensure that everything gets billed and payed on time. They’re also salaried employees but make less money.

  • Sales employees: They make a lot of phone calls to sell products. They have a salary, but they also get commissions for sales.

  • Factory workers: They manufacture the products for the company. They’re paid by the hour.

# employees.py

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

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

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

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

实现方法保持不变,但将类移至 imployees 模块。

hr 模块也变小了:

# 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("")
# program.py

import hr
import employees

salary_employee = employees.SalaryEmployee(1, "John Smith", 1500)
hourly_employee = employees.HourlyEmployee(2, "Jane Doe", 40, 15)
commission_employee = employees.CommissionEmployee(3, "Kevin Bacon", 1000, 250)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(
    [salary_employee, hourly_employee, commission_employee]
)

在 employee 模块中添加新类:

# ...

class Manager(SalaryEmployee):
    def work(self, hours):
        print(f"{self.name} screams and yells for {hours} hours.")

class Secretary(SalaryEmployee):
    def work(self, hours):
        print(f"{self.name} expends {hours} hours doing office paperwork.")

class SalesPerson(CommissionEmployee):
    def work(self, hours):
        print(f"{self.name} expends {hours} hours on the phone.")

class FactoryWorker(HourlyEmployee):
    def work(self, hours):
        print(f"{self.name} manufactures gadgets for {hours} hours.")
# productivity.py
class ProductivitySystem:
    def track(self, employees, hours):
        print("Tracking Employee Productivity")
        print("==============================")
        for employee in employees:
            employee.work(hours)
        print("")
# program.py

import hr
import employees
import productivity

manager = employees.Manager(1, "Mary Poppins", 3000)
secretary = employees.Secretary(2, "John Smith", 1500)
sales_guy = employees.SalesPerson(3, "Kevin Bacon", 1000, 250)
factory_worker = employees.FactoryWorker(4, "Jane Doe", 40, 15)
employees = [
    manager,
    secretary,
    sales_guy,
    factory_worker,
]

productivity_system = productivity.ProductivitySystem()
productivity_system.track(employees, 40)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(employees)

Inheriting Multiple Classes

Reference

Supercharge Your Classes With Python super() – Real Python