面向对象
# 面向对象编程 (OOP)
面向对象编程 (Object-Oriented Programming, OOP) 是一种强大的编程范式,它将世界看作是由一个个独立的、相互交互的“对象”组成的。Python 从设计之初就是一门面向对象的语言,理解 OOP 是从入门到精通的关键一步。
# 1. OOP 核心思想:类与对象
想象一下,你要建造一栋房子。你首先需要一张设计蓝图,上面规定了房子有几扇窗、几扇门、墙壁的材质等。这张蓝图就是 类 (Class)。它定义了这类事物潜在的属性和可以执行的行为。
根据这张蓝图,你可以建造出许多栋实体房子。每一栋房子都是一个独立的存在,有自己的地址、颜色。这些实体房子就是 对象 (Object),也叫实例 (Instance)。每个对象都有类定义的属性和行为,但属性可以有具体的值(如 窗户数量 = 10
)。
- 类 (Class): 创建对象的模板或蓝图。它定义了一类事物共有的属性 (Attributes) 和方法 (Methods)。
- 对象 (Object): 类的具体实例。它拥有类所定义的属性和方法,并且可以有自己独特的状态(属性值)。
# 2. 创建你的第一个类
在 Python 中,我们使用 class
关键字来定义一个类。类名通常遵循 驼峰命名法 (PascalCase),这是为了将其与使用 snake_case
的函数和变量区分开。
# class 关键字开始了类的定义,后面跟着类名 Dog
class Dog:
# pass 是一个占位符,表示这个类的主体暂时是空的
pass
# 实例化:调用类名就像调用函数一样,创建了一个 Dog 类的实例
# Python 在内存中为 dog1 分配了一块空间
dog1 = Dog()
print(type(dog1)) # 输出: <class '__main__.Dog'>
print(dog1) # 输出: <__main__.Dog object at 0x...> (一个内存地址)
2
3
4
5
6
7
8
9
10
11
底层发生了什么? 当你调用 Dog()
时,Python 实际上执行了两步:
- 调用
__new__
方法(我们通常不重写它),在内存中创建一个空的Dog
对象。 - 调用
__init__
方法,并将第一步创建的对象作为self
参数传入,来初始化这个对象。
# 3. 对象的“出生证明”:__init__
构造方法
__init__
方法是一个特殊的“魔术方法”,在创建对象时自动被调用,用于为新创建的对象设置初始状态,即给它的属性赋初始值。
class Dog:
# __init__ 方法,也叫构造方法或初始化方法
def __init__(self, name, age):
# 这里的 self 是一个指向刚被创建的实例的引用
print(f"初始化一只名叫 {name} 的狗狗...")
# self.name = name 的含义是:
# “在这个实例(self)上,创建一个名为 'name' 的属性,
# 并将传入的局部变量 name 的值赋给它。”
self.name = name # 实例属性
self.age = age # 实例属性
my_dog = Dog("旺财", 2)
print(f"我的狗叫 {my_dog.name},它今年 {my_dog.age} 岁了。")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self
是一个约定俗成的名称,它不是关键字。它永远是类中实例方法的第一个参数。
- 作用: 代表实例本身。在类的方法内部,通过
self
可以访问该实例的任何属性和调用其任何方法。 - 自动传递: 当你调用
my_dog.some_method(arg1)
时,Python 解释器会自动将其转换为Dog.some_method(my_dog, arg1)
。my_dog
实例被自动作为第一个参数self
传入。
# 4. 属性:对象的数据
- 实例属性 (Instance Attribute): 每个实例独有的数据。在
__init__
方法中通过self.attribute_name
定义。 - 类属性 (Class Attribute): 整个类共享的数据。直接在类定义下声明。通常用于定义该类所有实例都应具备的共性特征或常量。
class Dog:
# 类属性:所有 Dog 实例共享
species = "犬科"
def __init__(self, name):
self.name = name
d1 = Dog("大黄")
d2 = Dog("小白")
# 属性查找顺序:Python 会先在实例自己的属性中查找,找不到再去类属性中查找。
print(f"d1 的物种是: {d1.species}") # d1 实例没有 species 属性,向上找到 Dog.species
# 实例属性的独立性
d1.name = "阿黄"
print(f"d1 的名字是 {d1.name},d2 的名字是 {d2.name}。") # d1 的修改不影响 d2
# 类属性的共享性
Dog.species = "Canis lupus familiaris"
print(f"d1 的物种更新为 {d1.species},d2 的也更新为 {d2.species}。")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
属性遮蔽 (Attribute Shadowing): 如果你给一个实例设置了与类属性同名的属性,那么实例属性会“遮蔽”类属性。
d1.species = "中华田园犬" # 这是在 d1 实例上创建了一个新的实例属性
print(f"d1 的物种: {d1.species}") # 输出: 中华田园犬 (实例属性)
print(f"d2 的物种: {d2.species}") # 输出: Canis lupus familiaris (类属性)
print(f"类的物种: {Dog.species}") # 输出: Canis lupus familiaris (类属性)
2
3
4
# 5. 方法:对象的行为
方法(Methods)是定义在类中的函数,用于封装和实现对象的行为。根据方法与类和实例的关联方式,Python 中的方法主要分为三种:实例方法、类方法和静态方法。
# 5.1 实例方法 (Instance Method)
这是最常见的方法类型。它与类的单个实例紧密绑定,能够访问和修改该实例的状态(即实例属性)。
- 定义: 方法的第一个参数必须是
self
,它代表调用该方法的实例对象。 - 调用: 通过实例来调用,例如
my_instance.method_name()
。 - 核心用途: 操作实例属性。几乎所有需要读取或修改单个对象状态的操作,都应通过实例方法完成。
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
# 这是一个实例方法
def show_score(self):
# self 自动指向调用此方法的实例 (例如下面的 'student_a')
# 通过 self 可以访问这个实例的 name 和 score 属性
print(f"学生 {self.name} 的分数是 {self.score}")
# 这也是一个实例方法,它修改了实例的状态
def update_score(self, new_score):
print(f"学生 {self.name} 的分数从 {self.score} 更新为 {new_score}")
self.score = new_score
# 创建一个实例
student_a = Student("小明", 85)
# 调用实例方法
# Python 自动将调用者 student_a 作为 self 参数传入 show_score
student_a.show_score() # 等价于 Student.show_score(student_a)
student_a.update_score(92)
student_a.show_score()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 5.2 类方法 (Class Method)
类方法与整个类相关联,而不是与类的某个特定实例相关联。它通常用于处理与类本身相关的逻辑,如读取或修改类属性,或者提供备用的构造方式。
- 定义: 必须使用
@classmethod
装饰器。方法的第一个参数必须是cls
,它代表类本身。 - 调用: 可以通过类名直接调用(
ClassName.method_name()
),也可以通过实例调用(但仍然操作的是类级别的状态)。 - 核心用途:
- 访问或修改类属性。
- 创建备用构造器 (Alternative Constructors):这是类方法最经典、最有用的场景。
import datetime
class Person:
# 类属性
species = "智人"
def __init__(self, name, age):
self.name = name
self.age = age
# 一个普通的实例方法
def display(self):
print(f"{self.name}, 年龄 {self.age}, 物种: {self.species}")
# 一个类方法,用于创建实例
# 比如我们希望可以从一个出生年份来创建 Person 对象
@classmethod
def from_birth_year(cls, name, birth_year):
# cls 在这里就是 Person 类本身
# cls(...) 就等同于 Person(...)
current_year = datetime.date.today().year
age = current_year - birth_year
# 返回一个通过计算得出的新实例
return cls(name, age)
# --- 使用 ---
# 正常创建实例
person_1 = Person("张三", 30)
person_1.display()
# 使用类方法这个“备用构造器”来创建实例
person_2 = Person.from_birth_year("李四", 1990)
person_2.display()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
在这个例子中,from_birth_year
提供了一种比 __init__
更直观、更具描述性的方式来创建对象,增强了类的易用性。
# 5.3 静态方法 (Static Method)
静态方法在功能上与类完全独立,它既不访问实例状态(self
),也不访问类状态(cls
)。它就像一个恰好被放在类这个“命名空间”里的普通函数。
- 定义: 必须使用
@staticmethod
装饰器。方法没有self
或cls
这样的强制性首个参数。 - 调用: 可以通过类名或实例名调用。
- 核心用途: 作为工具函数(Utility Function)。当某个功能在逻辑上与一个类相关,但其实现又完全不需要类或实例的任何信息时,就应使用静态方法。
class MathUtils:
# 这是一个静态方法,它只是一个逻辑上相关的工具函数
@staticmethod
def circle_area(radius):
# 这个计算不依赖于任何 MathUtils 的实例或类属性
return 3.14159 * (radius ** 2)
@staticmethod
def is_positive(number):
return number > 0
# --- 使用 ---
# 无需创建实例,直接通过类名调用
area = MathUtils.circle_area(5)
print(f"半径为5的圆面积是: {area}")
print(f"10 是正数吗? {MathUtils.is_positive(10)}")
print(f"-5 是正数吗? {MathUtils.is_positive(-5)}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5.4 三种方法对比总结
特性 | 实例方法 (Instance Method) | 类方法 (Class Method) | 静态方法 (Static Method) |
---|---|---|---|
装饰器 | 无 | @classmethod | @staticmethod |
第一个参数 | self (代表实例) | cls (代表类) | 无特殊参数 |
核心用途 | 操作实例的状态(实例属性) | 操作类的状态(类属性),或创建备用构造器 | 逻辑上相关的工具函数 |
访问能力 | 可访问实例属性和类属性 | 只能访问类属性 | 不能访问实例属性或类属性 |
调用方式 | instance.method() | Class.method() 或 instance.method() | Class.method() 或 instance.method() |
# 6. OOP 三大支柱之一:封装 (Encapsulation)
封装是面向对象编程的一个核心概念,其主旨是将数据(属性)和操作数据的代码(方法)捆绑在一起,并对外部隐藏对象的内部实现细节。
想象一下电视机。作为用户,你只需要使用遥控器(公开接口)来开关、换台、调音量。你不需要(也不应该)打开电视外壳,去直接操作里面的电路板(内部状态)。封装就是这层“外壳”,它保护了内部状态不被随意修改,同时提供了简单、安全的交互方式。
在 Python 中,我们主要通过命名约定和 @property
装饰器来实现封装。
# 6.1 _
和 __
命名约定:访问控制的信号
Python 没有像 Java 或 C++ 那样严格的 private
或 public
关键字。相反,它依赖于程序员之间的约定,通过在属性或方法名前添加下划线来表示其可见性。
# 6.1.1 单下划线前缀 _
(受保护成员)
- 约定:
_variable
或_method()
- 含义: 这是一个内部使用的属性或方法,它不应该被类的外部直接访问。这是一种“君子协定”,告诉其他开发者:“这是内部实现,如果你要用,请自己承担风险。”
- 效果: Python 解释器不会做任何特殊处理。你在外部依然可以访问它,但这样做是不被推荐的。
class Person:
def __init__(self, name, age):
self.name = name # 公开属性
self._age = age # 约定为受保护的属性
def _get_birth_year(self): # 约定为受保护的方法
return 2023 - self._age
p = Person("小明", 25)
# 虽然可以访问,但不推荐这样做
print(f"{p.name} 的年龄是 {p._age}")
print(f"出生年份大约是 {p._get_birth_year()}")
2
3
4
5
6
7
8
9
10
11
12
13
# 6.1.2 双下划线前缀 __
(私有成员)
- 约定:
__variable
或__method()
- 含义: 这是一个私有成员,旨在仅供类的内部使用。
- 效果: Python 会对此类名称进行名称改写 (Name Mangling)。它会将
__name
这样的名字自动变为_ClassName__name
。 - 目的: 这种机制的主要目的不是为了制造真正的“私有”,而是为了避免在子类中意外地覆盖父类的私有成员,从而增强类的独立性。
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # 私有属性
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self.__log_transaction(f"存入 {amount}")
def get_balance(self):
# 在类的内部,可以直接使用 __balance
return self.__balance
def __log_transaction(self, message): # 私有方法
print(f"交易记录: {message}, 当前余额: {self.__balance}")
acc = BankAccount("Alice", 1000)
# 外部无法直接访问私有成员,会报错
# print(acc.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
# acc.__log_transaction("尝试记录") # AttributeError
# 只能通过公开的接口 (public method) 来与对象交互
acc.deposit(500)
print(f"Alice 的余额是: {acc.get_balance()}")
# 名称改写后的名字仍然可以访问(强烈不推荐)
print(f"通过名称改写访问余额: {acc._BankAccount__balance}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 6.2 Pythonic 封装:@property
装饰器
虽然命名约定很有用,但更优雅、更符合 Python 哲学的封装方式是使用 @property
装饰器。它能让你将一个方法伪装成一个属性来使用,使得代码更简洁,同时还能在背后执行复杂的逻辑(如计算、验证)。
@property
主要用于以下场景:
- 只读属性:属性的值由其他属性计算得出,不允许直接修改。
- 数据验证:在给属性赋值时,检查新值的有效性。
# 6.2.1 创建只读属性
@property
最简单的用法是创建一个只读属性。你只需要定义一个 getter
方法。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""
这个方法被 @property 装饰后,就变成了一个名为 'area' 的只读属性。
每次访问 rect.area 时,这个方法都会被自动调用。
"""
return self.width * self.height
# --- 使用 ---
r = Rectangle(10, 5)
# 像访问普通属性一样访问 area,背后其实是调用了 area() 方法
print(f"矩形的面积是: {r.area}") # 输出: 50
# 修改构成 area 的基础属性
r.width = 12
# area 属性的值会自动重新计算
print(f"修改宽度后,面积是: {r.area}") # 输出: 60
# 尝试直接给 area 赋值会失败,因为它是一个只读属性
# r.area = 100 # 这会引发 AttributeError: can't set attribute
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 6.2.2 创建可写属性 (Getter 和 Setter)
要让一个 property
属性变得可写,我们需要为它定义一个 setter
方法。这是实现数据验证的最佳位置。
语法结构:
- 用
@property
装饰getter
方法,方法名将成为公开的属性名。 - 用
@属性名.setter
装饰setter
方法。 - 在内部,通常会有一个以下划线开头的“真实”属性来存储值。
class Student:
def __init__(self, name):
self.name = name
self._score = 0 # 内部属性,用于存储真实的分数
@property
def score(self):
"""Getter: 当访问 student.score 时调用"""
print(f"正在获取 {self.name} 的分数...")
return self._score
@score.setter
def score(self, value):
"""Setter: 当 student.score = value 赋值时调用"""
print(f"正在为 {self.name} 设置分数...")
if not (0 <= value <= 100):
# 在 setter 中进行数据验证
raise ValueError("分数必须在 0 到 100 之间!")
self._score = value
# --- 使用 ---
s = Student("小华")
# 像普通属性一样赋值,背后调用了 setter 方法
s.score = 95 # 输出: 正在为 小华 设置分数...
# 像普通属性一样访问,背后调用了 getter 方法
print(f"{s.name} 的分数是 {s.score}") # 输出: 正在获取 小华 的分数... 小华 的分数是 95
# 尝试赋一个无效值,setter 中的验证逻辑会起作用
try:
s.score = 120
except ValueError as e:
print(f"赋值失败: {e}") # 输出: 赋值失败: 分数必须在 0 到 100 之间!
# 再次查看分数,确认它没有被修改
print(f"当前分数依然是: {s.score}") # 输出: ... 当前分数依然是: 95
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 6.2.3 完整的 Property: Getter, Setter, Deleter
property
还有一个删除器 (deleter
),用于定义 del obj.attribute
时的行为。
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
"""Getter for email"""
return self._email
@email.setter
def email(self, value):
"""Setter for email"""
if '@' not in value:
raise ValueError("无效的邮箱地址")
self._email = value
@email.deleter
def email(self):
"""Deleter for email"""
print(f"正在删除用户邮箱: {self._email}")
self._email = None
# --- 使用 ---
user = User("test@example.com")
print(f"当前邮箱: {user.email}")
user.email = "new.test@example.com"
print(f"新邮箱: {user.email}")
# 使用 del 关键字会触发 deleter
del user.email
# 输出: 正在删除用户邮箱: new.test@example.com
print(f"删除后的邮箱: {user.email}") # 输出: None
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 7. OOP 三大支柱之二:继承 (Inheritance)
继承允许我们创建一个新类(称为子类或派生类),它会获取另一个类(称为父类、基类或超类)的所有属性和方法。这是实现代码重用和构建清晰层级关系(例如 “狗 是一种 动物”)的核心机制。
子类不仅拥有父类的所有功能,还可以:
- 添加新功能:定义父类没有的新属性或新方法。
- 重写已有功能:对父类的方法提供自己的实现版本。
# 7.1 继承的基础语法
在定义类时,将父类的名字放在类名后的括号中,即可实现继承。
# 定义一个父类 Animal
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 正在吃东西。")
def speak(self):
# 使用 raise NotImplementedError 强制子类必须实现这个方法
raise NotImplementedError("子类必须实现 speak 方法")
# Dog 类继承自 Animal 类
# 这意味着 Dog 自动拥有了 Animal 的 __init__ 和 eat 方法
class Dog(Animal):
# 子类可以有自己的方法
def fetch(self):
print(f"{self.name} 跑去捡球了!")
# 子类可以重写父类的方法
def speak(self):
return f"{self.name} 说:汪汪!"
# Cat 类也继承自 Animal
class Cat(Animal):
def speak(self):
return f"{self.name} 说:喵喵~"
# --- 使用 ---
my_dog = Dog("旺财")
my_cat = Cat("咪咪")
# 子类对象可以直接调用继承自父类的 eat 方法
my_dog.eat() # 输出: 旺财 正在吃东西。
my_cat.eat() # 输出: 咪咪 正在吃东西。
# 子类对象调用自己重写后的 speak 方法
print(my_dog.speak()) # 输出: 旺财 说:汪汪!
print(my_cat.speak()) # 输出: 咪咪 说:喵喵~
# 子类对象调用自己的新方法
my_dog.fetch() # 输出: 旺财 跑去捡球了!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 7.2 方法重写与 super()
调用
当子类的方法与父类的方法同名时,子类的方法会覆盖父类的方法,这称为方法重写 (Method Overriding)。
但有时我们不想完全覆盖,而是想在父类方法的基础上增加一些新功能。这时,就需要 super()
函数来帮助我们调用父类中被重写的方法。
# 7.2.1 使用 super()
扩展 __init__
方法
一个非常常见的场景是,子类在自己的 __init__
中需要先完成父类的初始化。
重要:如果你在子类中定义了
__init__
方法,Python 不会再自动调用父类的__init__
方法。你必须手动使用super().__init__(...)
来调用它。
class Animal:
def __init__(self, name):
print(f"【父类 Animal 初始化】为 '{name}' 分配名字。")
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
# 1. 使用 super() 调用父类的 __init__ 方法
# 这样就完成了 self.name = name 的初始化
print(f"【子类 Dog 初始化开始】准备调用父类初始化...")
super().__init__(name)
# 2. 然后再添加子类自己的初始化逻辑
print(f"【子类 Dog 初始化】为 '{name}' 分配品种。")
self.breed = breed # 这是 Dog 类独有的属性
# 当创建 Dog 实例时,观察输出顺序
d = Dog("小白", "萨摩耶")
print(f"狗的名字: {d.name}, 品种: {d.breed}")
# 输出:
# 【子类 Dog 初始化开始】准备调用父类初始化...
# 【父类 Animal 初始化】为 '小白' 分配名字。
# 【子类 Dog 初始化】为 '小白' 分配品种。
# 狗的名字: 小白, 品种: 萨摩耶
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 7.2.2 使用 super()
扩展普通方法
同样地,你也可以用 super()
来扩展任何被重写的普通方法。
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_details(self):
return f"姓名: {self.name}, 薪水: {self.salary}"
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self.department = department
def get_details(self):
# 先调用父类的 get_details 方法获取基本信息
base_details = super().get_details()
# 然后再附加子类自己的信息
return f"{base_details}, 部门: {self.department}"
manager = Manager("王经理", 20000, "技术部")
print(manager.get_details())
# 输出: 姓名: 王经理, 薪水: 20000, 部门: 技术部
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 7.3 多重继承与 MRO
一个子类可以同时继承多个父类,这就是多重继承。
class Father:
def skill_f(self):
print("会做饭")
def common_skill(self):
print("Father's skill")
class Mother:
def skill_m(self):
print("会织毛衣")
def common_skill(self):
print("Mother's skill")
# Child 同时继承 Father 和 Mother
# 继承顺序 (Father, Mother) 很重要!
class Child(Father, Mother):
pass
c = Child()
c.skill_f() # 输出: 会做饭
c.skill_m() # 输出: 会织毛衣
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当多个父类有同名方法时(如 common_skill
),Python 会根据方法解析顺序 (Method Resolution Order, MRO) 来决定调用哪一个。MRO 遵循“从左到右,深度优先”的原则。
c.common_skill() # 输出: Father's skill (因为 Father 在继承列表的左边)
# 我们可以打印出 MRO 列表来查看查找顺序
print(Child.mro())
# 输出:
# [<class '__main__.Child'>, # 1. 先找自己
# <class '__main__.Father'>, # 2. 再找第一个父类 Father
# <class '__main__.Mother'>, # 3. 再找第二个父类 Mother
# <class 'object'>] # 4. 最后找所有类的基类 object
2
3
4
5
6
7
8
9
注意:多重继承虽然强大,但也可能导致代码结构复杂、难以理解(菱形继承问题)。在实际开发中应谨慎使用。
# 7.4 内置函数 isinstance()
和 issubclass()
isinstance(obj, Class)
: 检查一个对象是否是某个类或其子类的实例。issubclass(Sub, Super)
: 检查一个类是否是另一个类的子类。
my_dog = Dog("旺财")
print(f"my_dog 是 Dog 类的实例吗? {isinstance(my_dog, Dog)}") # True
print(f"my_dog 是 Animal 类的实例吗? {isinstance(my_dog, Animal)}") # True (因为 Dog 是 Animal 的子类)
print(f"my_dog 是 Cat 类的实例吗? {isinstance(my_dog, Cat)}") # False
print(f"Dog 是 Animal 的子类吗? {issubclass(Dog, Animal)}") # True
print(f"Animal 是 Dog 的子类吗? {issubclass(Animal, Dog)}") # False
2
3
4
5
6
7
8
# 8. OOP 三大支柱之三:多态 (Polymorphism)
多态,字面意思为“多种形态”。在编程中,它指的是不同的对象在接收到同一个消息(调用同一个方法)时,能够表现出不同的行为。
想象一个“USB接口”,它就是一个统一的规范。无论你插入的是U盘、键盘还是鼠标(不同的对象),电脑(调用者)都通过同一个USB接口(统一的方法)与它们交互,但每个设备都会执行自己独特的功能(不同的行为)。这就是多态。
在 Python 中,多态的核心实现方式是鸭子类型。
# 8.1 核心思想:鸭子类型 (Duck Typing)
鸭子类型的核心思想是:
“如果一个东西走起来像鸭子,叫起来也像鸭子,那么它就是一只鸭子。”
换句话说,我们不关心一个对象的具体类型是什么,只关心它是否具有我们需要的行为(即方法)。如果两个类都有一个 .speak()
方法,那么我们就可以把它们都当作会“说话”的东西来对待,而无需关心一个是 Dog
,另一个是 Cat
。
Python 的多态是建立在动态类型之上的,它不要求强制的继承关系。只要对象“看起来”有我们需要的方法,就可以正常工作。
# 8.2 多态的实践
让我们来看一个具体的例子。我们将定义几个完全不相关的类,但它们都恰好有一个名为 make_sound
的方法。
# 定义几个不同的类
class Dog:
def make_sound(self):
print("汪!汪汪!")
class Cat:
def make_sound(self):
print("喵~")
class Car:
def make_sound(self):
print("嘀嘀嘀!")
# 定义一个统一的接口函数
# 这个函数不关心传入的 a_thing 是什么类型
# 它只假设 a_thing 有一个 .make_sound() 方法
def let_it_make_sound(a_thing):
a_thing.make_sound()
# 创建不同类的实例
dog = Dog()
cat = Cat()
car = Car()
# 将这些不同类型的对象传入同一个函数
let_it_make_sound(dog) # 输出: 汪!汪汪!
let_it_make_sound(cat) # 输出: 喵~
let_it_make_sound(car) # 输出: 嘀嘀嘀!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在这个例子中,let_it_make_sound
函数就是多态的体现。它接受任何有 .make_sound()
方法的对象,并成功地执行了操作,展示了代码的极高灵活性。
我们甚至可以对一个包含不同类型对象的列表进行迭代,并统一调用它们的方法:
entities = [Dog(), Cat(), Car()]
for entity in entities:
entity.make_sound()
2
3
4
# 8.3 多态的优势
- 代码更灵活:我们可以随时创建新的类,只要它实现了约定的方法(如
make_sound
),就能无缝地集成到现有代码中,而无需修改任何调用它的函数。 - 接口更统一:多态允许我们忽略对象的类型差异,编写出更通用、更抽象的代码。
- 可扩展性强:当需要添加新功能时,通常只需要增加一个新的、符合“鸭子类型”规范的类即可。
# 8.4 更安全的鸭子类型:hasattr
纯粹的鸭子类型依赖于“信任”,即我们假设传入的对象一定有我们需要的方法。如果传入一个没有该方法的对象,程序就会在运行时出错。
class Book:
def read(self):
print("正在阅读...")
# book = Book()
# let_it_make_sound(book) # 会报错:AttributeError: 'Book' object has no attribute 'make_sound'
2
3
4
5
6
为了让代码更健壮,我们可以使用 hasattr()
函数在调用前进行检查。
def safe_let_it_make_sound(a_thing):
if hasattr(a_thing, 'make_sound') and callable(a_thing.make_sound):
a_thing.make_sound()
else:
print(f"对象 {type(a_thing).__name__} 没有 make_sound 方法")
book = Book()
safe_let_it_make_sound(dog) # 输出: 汪!汪汪!
safe_let_it_make_sound(book) # 输出: 对象 Book 没有 make_sound 方法
2
3
4
5
6
7
8
9
# 8.5 抽象基类 (ABC) - 规范化的多态 (可选)
虽然鸭子类型非常灵活,但在大型项目或框架设计中,有时我们希望对“接口”有一个更严格的定义。这时可以使用 abc
(Abstract Base Classes) 模块。
抽象基类可以定义一组方法,并强制其所有子类必须实现这些方法。这就像是为“鸭子”制定了一个官方标准。
from abc import ABC, abstractmethod
# 定义一个抽象基类
class SoundMaker(ABC):
@abstractmethod
def make_sound(self):
pass
# Dog 继承自 SoundMaker,并且必须实现 make_sound
class Dog(SoundMaker):
def make_sound(self):
print("汪!")
# 如果一个类继承了但没有实现抽象方法,实例化时会报错
class BrokenCat(SoundMaker):
def meow(self): # 方法名不匹配
pass
d = Dog()
d.make_sound() # 正确执行
# b = BrokenCat() # 会报错: TypeError: Can't instantiate abstract class BrokenCat with abstract method make_sound
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用抽象基类,可以在保持多态灵活性的同时,增加代码的健壮性和可维护性。
# 9. 强大的魔术方法 (Magic Methods)
魔术方法,官方称为特殊方法 (Special Methods),是 Python 中一类以双下划线开头和结尾的预定义方法(例如 __init__
、__len__
)。它们不是让你手动调用的,而是作为响应特定操作的“钩子”,由 Python 解释器在特定时刻自动触发。
通过正确地实现这些方法,你可以让自定义对象无缝集成到 Python 的语言特性中,使其行为像内建类型(如列表、字典)一样自然、强大。
# 9.1 对象的字符串表示:__str__
vs __repr__
这两个方法都用于返回对象的字符串表示,但目标和用途完全不同。
__str__(self)
:- 目标用户: 普通用户。
- 触发时机: 当你对实例使用
print()
函数或str()
类型转换时。 - 目标: 可读性。应返回一个简洁、友好、易于理解的字符串。
__repr__(self)
:- 目标用户: 开发者。
- 触发时机: 当你在交互式控制台中直接输入变量名、使用
repr()
函数,或在调试器中查看对象时。 - 目标: 明确性和无歧义。理想情况下,
__repr__
返回的字符串应该是一个有效的 Python 表达式,可以通过eval()
重新创建出该对象。
- 最佳实践:
- 同时实现这两个方法。
- 如果
__str__
未被定义,Python 会自动使用__repr__
作为替代。因此,至少要实现__repr__
。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
# 对用户友好的输出
return f"坐标点 ({self.x}, {self.y})"
def __repr__(self):
# 对开发者友好的、明确的输出
# 这个字符串可以直接用来创建对象: Point(10, 20)
return f"Point(x={self.x}, y={self.y})"
# --- 使用 ---
p = Point(10, 20)
# print() 会触发 __str__
print(p) # 输出: 坐标点 (10, 20)
# str() 会触发 __str__
s = str(p)
print(s) # 输出: 坐标点 (10, 20)
# 在交互式控制台输入 p 或使用 repr() 会触发 __repr__
print(repr(p)) # 输出: Point(x=10, y=20)
# __repr__ 的结果可以用来重新创建对象
p_clone = eval(repr(p))
print(p_clone) # 输出: 坐标点 (10, 20)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 9.2 容器类魔术方法
通过实现这些方法,你可以让你的对象表现得像一个容器(如列表或字典)。
__len__(self)
: 响应len(obj)
操作。__getitem__(self, key)
: 响应obj[key]
(获取元素) 操作。__setitem__(self, key, value)
: 响应obj[key] = value
(设置元素) 操作。__delitem__(self, key)
: 响应del obj[key]
(删除元素) 操作。
class ShoppingCart:
def __init__(self):
# 使用字典来存储商品和数量
self._items = {}
def add_item(self, item_name, quantity=1):
self._items[item_name] = self._items.get(item_name, 0) + quantity
def __len__(self):
# 购物车的“长度”是商品的总数量
return sum(self._items.values())
def __getitem__(self, item_name):
# 允许通过 cart['苹果'] 的方式获取商品数量
return self._items.get(item_name, 0)
def __setitem__(self, item_name, quantity):
# 允许通过 cart['苹果'] = 5 的方式设置商品数量
if quantity <= 0:
# 如果数量小于等于0,则移除商品
if item_name in self._items:
del self._items[item_name]
else:
self._items[item_name] = quantity
def __str__(self):
if not self._items:
return "购物车是空的"
parts = [f"{name}: {qty}件" for name, qty in self._items.items()]
return f"购物车中有 {len(self)} 件商品: {', '.join(parts)}"
# --- 使用 ---
cart = ShoppingCart()
cart.add_item("苹果", 2)
cart.add_item("香蕉")
print(cart) # __str__ 被调用
# len() 触发 __len__
print(f"商品总数: {len(cart)}") # 输出: 3
# 索引访问触发 __getitem__
print(f"苹果的数量: {cart['苹果']}") # 输出: 2
print(f"西瓜的数量: {cart['西瓜']}") # 输出: 0
# 索引赋值触发 __setitem__
cart['香蕉'] = 5
cart['橙子'] = 10
print(f"更新后: {cart}")
# 赋值为0会移除商品
cart['橙子'] = 0
print(f"移除橙子后: {cart}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 9.3 比较运算符
__eq__(self, other)
: 响应==
__ne__(self, other)
: 响应!=
__lt__(self, other)
: 响应<
__gt__(self, other)
: 响应>
__le__(self, other)
: 响应<=
__ge__(self, other)
: 响应>=
class Team:
def __init__(self, name, points):
self.name = name
self.points = points
def __str__(self):
return f"球队 {self.name} ({self.points}分)"
def __eq__(self, other):
# 当两个 Team 对象的积分相同时,我们认为它们是“相等”的
if not isinstance(other, Team):
return NotImplemented
return self.points == other.points
def __gt__(self, other):
# 定义“大于”的关系为积分更高
if not isinstance(other, Team):
return NotImplemented
return self.points > other.points
team_a = Team("勇士", 105)
team_b = Team("湖人", 102)
team_c = Team("骑士", 105)
print(f"{team_a} > {team_b}: {team_a > team_b}") # True
print(f"{team_a} == {team_b}: {team_a == team_b}") # False
print(f"{team_a} == {team_c}: {team_a == team_c}") # True
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 9.4 可调用对象:__call__
实现 __call__(self, *args, **kwargs)
方法可以让你的类的实例像函数一样被调用。
class Adder:
def __call__(self, x, y):
print(f"调用 Adder 实例,计算 {x} + {y}")
return x + y
# 创建实例
add = Adder()
# 像调用函数一样调用实例
result = add(5, 10) # 这会触发 Adder.__call__(add, 5, 10)
print(f"结果: {result}") # 输出: ... 结果: 15
2
3
4
5
6
7
8
9
10
11
这在创建需要维护状态的函数式对象时非常有用,例如在机器学习框架中,层的对象通常是可调用的。
# 10. 面向对象的优势总结
- 封装 (Encapsulation): 保护数据,隐藏实现细节。用户只需与安全的接口交互,无需关心内部复杂逻辑。
- 继承 (Inheritance): 重用代码,构建清晰的层级关系(“is a”关系),减少冗余。
- 多态 (Polymorphism): 提高代码的灵活性和可扩展性。你可以编写适用于多种对象类型的通用代码,而无需担心它们的具体实现。
- 抽象 (Abstraction): 通过类来模拟现实世界的问题,使代码更贴近业务逻辑,更易于理解和设计。