零基础入门Python·面向对象编程篇(上)

144 阅读23分钟

面向对象编程(OOP)是一种编程范式,它使用“对象”来模型化应用程序中的数据和行为。对象是类的实例,而类是对象的蓝图。这种方法不仅帮助程序员以更自然的方式组织和处理数据,而且还提高了代码的重用性、可扩展性和维护性。Python是一种面向对象的语言,这意味着它支持OOP的核心概念,包括继承、封装、多态等。

Python中类的使用极其广泛,几乎所有东西都是对象,从基本的数据类型(如字符串和整数)到库中的数据结构(如列表和字典)。

1.类的基础

1.1 类的定义与实例化

在Python中,使用class关键字来定义一个类。类定义通常包含属性(变量)和方法(函数),它们定义了该类对象的状态和行为。

# 声明一个狗狗类
class Dog:
    pass

一旦定义了类,就可以创建该类的实例,即创建一个对象。

# 实例化狗狗类
my_dog = Dog()

1.2 属性与方法

类属性是属于类本身的属性,对于类的所有实例都是共享的。实例属性则是属于类的单个实例的属性,每个对象都有自己的属性值。

class Dog:
    # 类属性
    species = "Canis familiaris" # 学名/种类 每一个狗狗都应该具有的属性

    def __init__(self, name, age):
        # 实例属性
        self.name = name  # 每只狗的名字
        self.age = age    # 每只狗的年龄

方法是定义在类内部的函数,它们可以操作对象的属性或执行与对象相关的任务。

class Dog:
    # 构造器方法,当创建类的新实例时调用
    def __init__(self, name, age):
        self.name = name  # 实例属性:狗的名字
        self.age = age    # 实例属性:狗的年龄

    # 一个描述狗的方法
    def describe(self):
        # 返回狗的描述信息,现在是中文的
        return f"{self.name}已经{self.age}岁了。"

1.3 构造器__init__方法

构造器是一个特殊的方法(__init__),当新的类实例被创建时自动调用。它通常用于初始化实例属性。

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
  • __init__(self, name, age): 这是一个特殊方法,用于初始化新创建的对象。它接受三个参数:

    • self: 在类的方法定义中,self 代表类的实例本身。通过 self,你可以访问类的属性和方法。
    • name: 这是传递给构造器的第一个参数,用于指定狗的名字。
    • age: 这是传递给构造器的第二个参数,用于指定狗的年龄。
  • 在 __init__ 方法内部,self.name = name 和 self.age = age 这两行代码将传入的参数值分别赋给实例的 name 和 age 属性。这样,每个 Dog 实例都会有自己的名字和年龄。

my_dog = Dog("旺财", 5)

通过这种方式,Python中的OOP允许程序员以一种清晰和逻辑性强的方式来组织代码,使得大型项目和复杂系统的开发和维护变得更加容易。

2.类的高级特性

2.1 继承

继承的基本概念

继承是面向对象编程中的一个核心概念,它允许我们定义一个类(子类或派生类)来继承另一个类(基类或父类)的属性和方法。继承支持代码重用,可以让我们建立一个层次化的类结构。

如何实现继承和多重继承

在Python中,继承可以通过在类定义时将基类作为参数传递给派生类来实现。

# 动物类/基类
class Animal:
    # 构造器方法,用于初始化Animal对象
    def __init__(self, name):
        self.name = name  # 实例属性:动物的名字

    # 一个方法,预期被子类实现
    def make_sound(self):
        pass  # 使用pass语句,因为这个方法是打算被子类覆盖的
# 狗狗类/继承了基类Animal
class Dog(Animal):
    # Dog类继承自Animal类
    def make_sound(self):
        return "旺!旺!旺!"  # 重写make_sound方法,返回狗叫声

Animal类定义了一个构造器__init__,它接受一个参数name并将其赋值给实例属性namemake_sound方法在Animal类中没有具体实现,意味着Animal类是一个抽象的概念,它期望其子类提供具体的make_sound方法实现。

Dog类继承自Animal类,并重写了make_sound方法,提供了具体的实现(返回字符串"旺!旺!旺!")。这表明了DogAnimal的一个具体实现,它具体定义了动物发出的声音是怎样的。

这种方式的优势在于,可以定义多种动物,每种动物都有自己的make_sound方法实现,这体现了多态性——即不同类的对象对同一消息会作出不同的响应。例如,可以进一步创建一个Cat类,它也继承自Animal,但会有不同的make_sound方法实现(比如返回"Meow")。这样,即使是不同类型的动物,只要它们都是Animal的子类,就可以期待它们能够响应make_sound方法调用,而不必关心具体是哪种动物。

Python还支持多重继承,允许一个类继承多个基类。

class A:
    def method(self):
        print("A method")

class B:
    def method(self):
        print("B method")

class C(A, B):
    pass

c = C()
c.method()

上面的代码中,类C继承自两个父类AB,这是Python中的多重继承。在多重继承的情况下,如果存在同名方法,Python会根据类的继承顺序来决定调用哪个方法。这个顺序被称为方法解析顺序(Method Resolution Order,MRO)。

C首先继承自类A,然后继承自类B。因此,当调用c.method()时,Python会根据MRO首先查找类C中是否有method方法。由于类C中没有定义method方法,Python接着会按照继承顺序查找,首先在类A中查找到了method方法并调用它,因此输出将会是:

A method

这展示了在Python中多重继承和MRO是如何工作的。如果想了解一个类的MRO,可以使用.__mro__属性或者mro()方法。例如,print(C.__mro__)print(C.mro())将展示类C的方法解析顺序。

2.2 多态

多态的含义

多态是指不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的实例以不同的方式实现。

Python中多态的实现方式

Python天生支持多态,因为它是动态类型语言。在Python中,我们不需要显示声明接口或基类,多态的行为是自然而然地发生的。

class Dog:
    def speak(self):
        return "汪汪"  # 狗叫声

class Cat:
    def speak(self):
        return "喵喵"  # 猫叫声

def animal_sound(animal):
    # 打印动物的叫声
    print(animal.speak())

dog = Dog()  # 创建一个Dog对象
cat = Cat()  # 创建一个Cat对象

animal_sound(dog)  # 输出: 汪汪(狗叫声)
animal_sound(cat)  # 输出: 喵喵(猫叫声)

Dog类和Cat类分别定义了DogCat对象如何“说话”(即speak方法)。然后,通过一个名为animal_sound的函数,展示了多态的概念:不同类型的对象(DogCat)可以通过相同的接口(speak方法)执行不同的操作。

这段代码展示了如何通过定义一个通用的接口(在这个例子中是speak方法),使得不同的对象能以相同的方式被处理。animal_sound函数接受任何包含speak方法的对象作为参数,并调用该方法,从而不必关心传入对象的具体类型。这种方式增加了代码的灵活性和可重用性。

2.3 封装

封装的概念及其重要性

封装是面向对象编程的另一个核心概念,它指的是将对象的状态(属性)和行为(方法)打包在一起,并对外隐藏对象的内部实现细节。封装的重要性在于它可以限制外部对对象内部状态的直接访问,从而保护对象的完整性并确保其状态的安全。

实现封装的方法

在Python中,封装通常通过使用私有(private)属性和方法来实现,私有属性或方法以两个下划线__开头。

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance  # 私有属性

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print("Deposit successful")

    def __calculate_interest(self):  # 私有方法
        # 假设利率是0.5%
        return self.__balance * 0.005

# 外部无法直接访问__balance和__calculate_interest
account = Account("John", 1000)
print(account.__balance)  # 将引发错误
print(account.__calculate_interest())  # 将引发错误

通过这种方式,只有类内部的方法可以访问和修改私有属性,从外部是无法直接访问的。这样不仅保护了数据的安全性,也提高了数据的封装性。

3.类的特殊机制

3.1 类变量与实例变量

类变量和实例变量的区别

  • 类变量是属于类的变量,它被该类的所有实例共享。类变量用于定义该类所有对象共有的数据。
  • 实例变量是属于类实例的变量,每个对象实例都拥有独立的实例变量副本。实例变量用于存储每个对象特有的数据。

使用场景和注意事项

  • 类变量适用于存储类级别的属性和常量数据。
  • 实例变量适用于存储每个对象特有的信息。

注意事项:

  • 修改类变量会影响到该类的所有实例,除非该实例已经通过实例变量覆盖了类变量的值。
  • 访问未被实例覆盖的类变量可以直接通过实例访问,但是修改类变量应该通过类名进行,以避免在实例中创建同名的实例变量。
class Employee:
    # 类变量
    raise_amount = 1.04

    def __init__(self, first, last, pay):
        # 实例变量
        self.first = first
        self.last = last
        self.pay = pay

    def apply_raise(self):
        # 使用类变量raise_amount
        self.pay = int(self.pay * self.raise_amount)

# 创建Employee类的实例
emp_1 = Employee('John', 'Doe', 50000)
emp_2 = Employee('Jane', 'Doe', 60000)

# 使用类变量之前
print(emp_1.pay)  # 输出: 50000
emp_1.apply_raise()
print(emp_1.pay)  # 输出: 52000 (50000 * 1.04)

# 修改类变量
Employee.raise_amount = 1.05

# 使用修改后的类变量
emp_2.apply_raise()
print(emp_2.pay)  # 输出: 63000 (60000 * 1.05)

# 创建一个实例变量,这不会影响类变量
emp_1.raise_amount = 1.06
emp_1.apply_raise()
print(emp_1.pay)  # 输出: 55120 (52000 * 1.06), 因为emp_1现在使用的是它的实例变量

# 类变量仍然是1.05
print(Employee.raise_amount)  # 输出: 1.05

注意!!!

  • 在上面的例子中,raise_amount是一个类变量,它被所有Employee实例共享。当我们修改Employee.raise_amount时,所有未显式设置raise_amount为实例变量的实例都会使用新值。
  • 当我们为emp_1设置raise_amount时,我们实际上是在emp_1实例中创建了一个同名的实例变量。这不会影响其他实例或类变量本身。
  • 使用类变量时,应当通过类名来修改它,以确保变更对所有实例有效。如果通过实例来修改,实际上会创建一个同名的实例变量,这可能会导致意想不到的行为。

3.2 静态方法和类方法

定义和使用静态方法

静态方法是使用@staticmethod装饰器定义的方法。它不自动传递类或实例的引用。静态方法的使用场景通常是一些工具函数,与类的任何实例都不强相关。

class Math:
    @staticmethod
    def add(x, y):
        return x + y

定义和使用类方法

类方法是使用@classmethod装饰器定义的方法。它将类本身作为第一个参数自动传递(通常命名为cls)。类方法可以访问和修改类状态。

class MyClass:
    count = 0
    
    @classmethod
    def increment_count(cls):
        cls.count += 1

3.3 魔术方法/特殊方法

常见的魔术方法及其用途

  • __init__(self, ...): 构造器,创建实例时调用。
  • __str__(self): 定义实例的字符串表示,用于printstr()
  • __repr__(self): 定义实例的官方字符串表示,用于调试和开发。
  • __add__(self, other): 定义加法操作的行为。
  • __len__(self): 定义当len()被调用时的行为。
  • __getitem__(self, key): 定义获取容器中元素的行为。

如何自定义魔术方法

自定义魔术方法允许定义或修改类的默认行为。例如,可以定义一个类的加法行为:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

通过定义__add__方法,我们可以使用加号+来直接对两个Vector实例进行加法操作。

魔术方法提供了一种强大的机制,通过它们,可以自定义或扩展类的行为,使得类的使用更加直观和自然。

4.访问控制与属性管理

4.1 私有属性和方法

私有属性和方法的定义

在面向对象编程中,私有属性和方法是指那些不希望在类的外部被访问或修改的属性和方法。在Python中,私有属性和方法通过在其名称前加上两个下划线__)来定义。这种命名约定告诉Python解释器对这些属性和方法进行名称改写,以防止它们在类的外部被访问。

示例:

class MyClass:
    def __init__(self):
        self.public_attribute = "这个属性可以在类的外部被访问。"
        self.__private_attribute = "这是一个私有属性。"

    def __private_method(self):
        return "这是一个私有方法。"

    def public_method(self):
        # 拼接字符串,并返回
        return "这是一个公共方法。它可以访问私有属性和方法。 " + self.__private_method()

# 创建 MyClass 的一个实例
my_instance = MyClass()

# 访问公共属性
print(my_instance.public_attribute)  # 输出: 这个属性可以在类的外部被访问。

# 尝试访问私有属性(这将导致错误)
# print(my_instance.__private_attribute)  # 如果尝试执行,将引发 AttributeError

# 调用公共方法
print(my_instance.public_method())  # 输出: 这是一个公共方法。它可以访问私有属性和方法。 这是一个私有方法。

# 尝试调用私有方法(这将导致错误)
# my_instance.__private_method()  # 如果尝试执行,将引发 AttributeError

访问私有属性和方法的方法

尽管私有属性和方法的主要目的是在类的外部隐藏它们,但在类的内部,仍然可以自由访问它们。如果确实需要从类的外部访问私有成员,可以通过以下方式之一:

  1. 通过公共方法访问:最常见的方式是提供一个公共方法作为私有属性或方法的访问接口。这种方式允许类的设计者控制对私有成员的访问。

  2. 名称改写(不推荐):Python通过简单地改写私有成员的名称来实现其私有性。这意味着,如果知道改写的规则,理论上可以直接访问它们。例如,私有属性__private_attribute可以通过_MyClass__private_attribute来访问。然而,这种方式破坏了封装原则,不推荐使用。

4.2 属性装饰器@property

@property装饰器的作用

@property装饰器允许将一个方法转换为只读属性的形式,使得可以像访问属性一样访问一个方法,而不需要在方法名后加上括号。这使得可以在不改变类接口的情况下,将一个类的字段从一个简单的属性变成一个用getter方法实现的属性。

如何使用@property管理属性访问

通过@property装饰器,可以定义一个方法,该方法在访问属性时会被自动调用(getter)。如果还需要允许用户修改属性值,可以使用@属性名.setter装饰器来定义一个setter方法。

示例:

class Circle:
    def __init__(self, radius):
        self.__radius = radius

    @property
    def radius(self):
        """获取半径的getter方法。"""
        return self.__radius

    @radius.setter
    def radius(self, value):
        """设置半径的setter方法。"""
        if value >= 0:
            self.__radius = value
        else:
            raise ValueError("半径不能为负数")

    @property
    def diameter(self):
        """计算直径的方法。"""
        return self.__radius * 2

在这个例子中,Circle类很好地展示了如何在Python中使用属性装饰器(@property)来创建可管理的属性。这种方法允许对属性的获取和设置进行更细致的控制,同时保持了使用简单属性的语法简洁性。

这个类有一个私有属性__radius,它通过radius属性的getter和setter方法进行访问和修改。这种做法确保了半径值在设置时能够进行验证,避免了给半径赋予无效值(如负数)。

此外,还定义了一个diameter属性,它是只读的,因为它只定义了getter方法。这个属性根据半径计算圆的直径。

如何使用

circle = Circle(5)  # 创建一个半径为5的圆

print(circle.radius)  # 输出: 5
print(circle.diameter)  # 输出: 10

circle.radius = 10  # 修改圆的半径
print(circle.radius)  # 输出: 10
print(circle.diameter)  # 输出: 20

# 尝试设置一个负数半径,将引发 ValueError
# circle.radius = -5  # 解除注释将引发 ValueError: Radius cannot be negative

5.抽象类和接口

5.1 抽象类和抽象方法

抽象类的定义和用途

在面向对象编程中,抽象类是一种不能被实例化的类。它的主要目的是为其他类提供一个基类,这些子类应该实现基类中的一个或多个抽象方法。抽象类表达了一个概念层面的实现,它规定了子类必须实现哪些方法,但不强制规定这些方法的具体实现细节。

抽象类通常用于设计大型系统中的组件接口,它允许定义一组基本操作,而具体如何执行这些操作则留给子类去实现。这种方法有助于减少系统各部分之间的耦合,并提高代码的模块性。

如何定义抽象方法和实现它们

在Python中,抽象类和抽象方法可以通过abc模块来定义。abc模块提供了ABCMeta类,它是所有抽象类的元类,以及abstractmethod装饰器,用于指示哪些方法是抽象方法。

下面是一个定义抽象类及其抽象方法的示例:

# 导入抽象基类(ABC)和abstractmethod装饰器
from abc import ABC, abstractmethod

# 定义一个名为Shape的抽象基类
class Shape(ABC):
    # 定义一个抽象方法area,用于计算面积
    @abstractmethod
    def area(self):
        pass

    # 定义一个抽象方法perimeter,用于计算周长
    @abstractmethod
    def perimeter(self):
        pass

# 定义一个名为Rectangle的类,它继承自Shape类
class Rectangle(Shape):
    # Rectangle类的构造函数,接收宽度和高度作为参数
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # 实现了Shape类中定义的抽象方法area,计算并返回矩形的面积
    def area(self):
        return self.width * self.height

    # 实现了Shape类中定义的抽象方法perimeter,计算并返回矩形的周长
    def perimeter(self):
        return 2 * (self.width + self.height)

# 下面的代码尝试实例化Shape类将会抛出错误,因为Shape是一个抽象基类,不能被直接实例化
# my_shape = Shape()  # 这会引发TypeError

# 创建一个Rectangle对象,宽度为10,高度为20
my_rectangle = Rectangle(10, 20)

# 调用Rectangle对象的area方法,并打印出结果,预期输出为200
print(my_rectangle.area())  # 输出: 200

# 调用Rectangle对象的perimeter方法,并打印出结果,预期输出为60
print(my_rectangle.perimeter())  # 输出: 60

在这个例子中,Shape类是一个抽象类,它定义了两个抽象方法:areaperimeter。这些方法在Shape类中没有具体实现,而是留给继承Shape类的子类(如Rectangle类)来提供具体实现。

任何试图实例化抽象类的操作都会导致错误,因为抽象类仅定义了接口规范而没有提供足够的实现细节。只有继承了抽象类并实现了所有抽象方法的子类才能被实例化。这种机制强制要求子类遵循由抽象类定义的接口规范,确保了一致性和可预测性。

6.实践应用

6.1 方法重写和super函数

方法重写的场景和方法

方法重写(也称为方法覆盖)发生在子类中定义了一个与父类同名的方法时。通过方法重写,子类可以提供特定于自己的行为实现,替换或扩展父类中的方法实现。方法重写的场景包括:

  • 修改或扩展继承自父类的行为。
  • 提供更高效的算法。
  • 更新或修改继承方法的参数列表。
  • 增强安全性或满足特定的业务需求。

使用super函数调用父类方法

super()函数是用来调用父类(超类)的一个方法。它的一个常见用途是在子类中调用父类的初始化方法(__init__)。使用super()可以确保父类被正确初始化,并允许子类扩展父类的行为。

示例:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method") # 子类必须实现抽象方法

class Dog(Animal):
    def speak(self):
        return super().speak() + " says Woof!"

# 尽管Animal的speak方法抛出了异常,但这里展示了如何使用super调用父类方法的语法。
# 实际应用中,可能不会在这种情况下使用super,而是完全重写speak方法。

示例中很好地阐述了方法重写和使用super函数调用父类方法的概念。不过,正如上面所指出的,示例中的Dog类尝试通过super().speak()调用父类Animalspeak方法,这并不是一个实际应用中推荐的做法,因为父类的speak方法是设计为抽象方法,旨在被子类重写以提供具体的实现,而不是直接被调用。

让我们通过一个更贴近实际应用的示例来进一步探讨super的使用场景:

class Animal:
    def __init__(self, name):
        self.name = name
        print(f"Animal {self.name} has been initialized")

    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # 使用super调用父类的__init__方法来初始化父类的属性
        self.breed = breed  # 初始化Dog类特有的属性
        print(f"Dog {self.name} of breed {self.breed} has been initialized")

    def speak(self):
        return f"{self.name} says Woof!"

# 创建一个Dog对象
dog = Dog("Buddy", "Golden Retriever")
print(dog.speak())

在这个示例中,Dog类通过super().__init__(name)调用了父类Animal的构造器来确保name属性被正确初始化。这显示了如何使用super()来调用父类的初始化方法,这是super()最常见的用途之一。

接下来,Dog类添加了自己的属性breed(品种),并在自己的构造器中初始化这个属性。这展示了如何通过方法重写来扩展父类的功能,同时通过super()保持父类的初始化逻辑。

最后,Dog类重写了Animal类的speak方法来提供狗叫声的具体实现,这是一个典型的方法重写示例,用于替换父类的抽象方法实现。

通过这个示例,我们可以看到super()的一个重要用途是在子类中调用父类的构造器,这样可以确保父类的属性被正确初始化,同时也允许子类扩展或修改父类的行为。

6.2 多重继承的应用和注意事项

多重继承的实现

多重继承指的是一个类可以继承自多个父类,这允许它继承所有父类的属性和方法。Python 支持多重继承,这让设计更加灵活,但也需要谨慎使用以避免复杂性。

示例:

class Father:
    def skills(self):
        print("酸萝卜别吃")

class Mother:
    def skills(self):
        print("你也别吃")

class Child(Father, Mother):
    def skills(self):
        Father.skills(self)
        Mother.skills(self)
        print("略略略~")

child = Child()
child.skills()
# 输出:
# 酸萝卜别吃
# 你也别吃
# 略略略~

这段代码演示了Python中的多重继承及其与方法重写和直接调用父类方法的结合使用。多重继承允许一个类同时继承多个父类,从而能够组合或扩展多个父类的功能。这里的Child类继承自两个父类:FatherMother,并重写了skills方法以展示Child类对象的技能集合,同时也包括了从两个父类继承来的技能。

代码详解如下:

  • Father类定义了一个skills方法,输出"Father"类特有的技能:"酸萝卜别吃"。
  • Mother类同样定义了一个skills方法,输出"Mother"类特有的技能:"你也别吃"。
  • Child类通过多重继承,同时继承了FatherMother两个类。在Child类中,skills方法被重写,以展示Child类特有的技能。

Child类的skills方法中,首先通过Father.skills(self)直接调用Father类中的skills方法,接着通过Mother.skills(self)调用Mother类中的skills方法,最后输出"Child"类特有的技能:"略略略~"。这种调用方式确保了Child类不仅展示了自己的技能,也包括了从父类继承来的技能。

需要注意的是,在调用Father.skills(self)Mother.skills(self)时,需要显式地传递self参数,因为这里是直接通过类名调用实例方法,而非通过实例对象调用,所以Python不会自动传递self参数。

这个例子展示了多重继承和方法重写在Python面向对象编程中的应用,以及如何通过直接调用父类方法来扩展子类的功能。

多重继承可能遇到的问题和解决方案

  • 钻石问题(Diamond Problem):当两个父类继承自同一个祖先类,并且子类同时继承这两个父类时,可能会不清楚调用祖先类方法的路径。Python 通过C3线性化算法解决了这个问题,确保每个类在继承链中只会被访问一次,并且保持了合理的顺序。
  • 方法解析顺序(MRO):在多重继承中,理解方法解析顺序非常重要。可以使用__mro__属性或mro()方法来查看类的方法解析顺序。
  • 一致性问题:在多重继承中,保持接口一致性是个挑战。应确保所有父类的方法在接口上保持一致,以避免混淆。

多重继承是一个强大但复杂的特性,应当谨慎使用。在设计类的层次结构时,如果可能的话,优先考虑使用组合而不是继承,特别是在涉及多重继承的情况下。

总结

本文深入探讨了面向对象编程(OOP)在Python中的应用,从基础概念到高级特性,为读者提供了一个全面的OOP概览。通过本文,读者可以掌握如何在Python中有效地使用类和对象来构建更加模块化、可维护和复用的代码。下一章的综合示例和作业中我们会对上一章中的自走棋实例进行面向对象改造,从应用中更一步理解Python面向对象之美