30. 类与对象入门
30.1) 面向对象编程理念(构建你自己的类型)
在本书的学习过程中,你一直在使用 Python 的内置类型:整数、字符串、列表、字典等等。每种类型都把数据(比如字符串中的字符)与可以对这些数据执行的操作(比如 .upper() 或 .split())打包在一起。这种数据与行为的组合非常强大——它让你可以把字符串看作具备自身能力的完整实体,而不仅仅是原始的字符序列。
面向对象编程(object-oriented programming, OOP)扩展了这个思想:它让你创建自己的自定义类型,称为类(class),把与你的问题领域相关的数据与行为打包在一起。就像 Python 提供 str 类型用于处理文本、提供 list 类型用于处理序列一样,你也可以创建一个 BankAccount 类型来管理金融交易、一个 Student 类型来跟踪学业记录,或为库存系统创建一个 Product 类型。
为什么要创建你自己的类型?
想象你要在一个学校系统里管理学生信息。不使用类时,你可能会用一些独立变量或字典:
# 使用独立变量 - 很快就会变得混乱
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
# 或者使用字典 - 更好,但仍然受限
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}这种方法在简单场景下可行,但它有局限:
- 没有验证:没有任何东西能阻止你把
gpa设置成-5.0或"excellent"这种无效值 - 没有相关行为:像计算荣誉状态或格式化学生信息这样的操作会成为散落在代码各处的独立函数
- 没有类型检查:代表学生的字典看起来和任何其他字典都一样——Python 无法帮助你捕获错误,例如你本来需要学生字典却不小心传入了商品字典
类通过让你定义一个全新的类型来解决这些问题,这个类型准确表示“学生是什么”以及“对学生来说哪些操作是合理的”:
# 我们会逐步构建到这里 - 一个把数据和行为打包在一起的 Student 类
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def display_info(self):
status = "Honors" if self.is_honors() else "Regular"
return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
# 现在我们可以创建学生对象
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.display_info()) # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors()) # Output: True本章会教你如何从零开始构建这样的类。我们将从最简单的类开始,逐步添加功能,直到你能够创建丰富而实用的自定义类型。
类 vs 实例:蓝图类比
理解类(class)与实例(instance)的区别,是面向对象编程的基础:
-
类就像蓝图或模板。它定义一种对象会保存哪些数据、能执行哪些操作。类本身不是某个具体学生——它是“成为一个学生”意味着什么的定义。
-
实例(也叫对象(object))是由蓝图创建出来的具体示例。当你创建
alice = Student("Alice Johnson", "S12345", 3.8)时,你是在创建一个具体的学生实例,里面包含 Alice 的特定数据。
你可以从一个类创建任意多个实例,就像建筑师可以用一张蓝图建造许多房子。每个实例都有自己的数据(Alice 的 GPA 和 Bob 不同),但它们都共享由类定义的结构与能力。
你将在本章学到什么
本章介绍 Python 中面向对象编程的核心概念:
- 使用
class关键字定义类 - 创建实例并访问它们的属性
- 添加方法来操作实例数据
- 理解
self以及方法如何访问实例数据 - 使用
__init__方法初始化实例 - 使用
__str__和__repr__控制字符串表示 - 从同一个类创建多个相互独立的实例
在本章结束时,你将能够设计并实现自己的自定义类型,让程序更有组织、更易维护、表达力更强。我们会在第 31 章基于这些基础学习更高级的类特性,并在第 32 章学习继承与多态。
30.2) 使用 class 定义简单类
让我们先创建最简单的类——它仅定义一个新类型,但还没有任何数据或行为。
class 关键字
你用 class 关键字来定义类,后面跟类名和冒号:
class Student:
pass # 暂时是空类
# 创建一个实例
alice = Student()
print(alice) # Output: <__main__.Student object at 0x...>
print(type(alice)) # Output: <class '__main__.Student'>即使这个最小的类也很有用——它创建了一个名为 Student 的新类型。当你用 alice = Student() 创建实例时,Python 会创建一个类型为 Student 的新对象。输出显示 alice 的确是一个 Student 对象,尽管它目前还做不了什么有趣的事情。
类命名约定
Python 的类名遵循一种特定约定,称为 CapWords 或 PascalCase:每个单词以大写字母开头,单词之间不使用下划线:
class BankAccount: # 好:CapWords
pass
class ProductInventory: # 好:CapWords
pass
class HTTPRequest: # 好:缩写全大写
pass
# 避免对类使用这些风格:
# class bank_account: # Wrong: snake_case is for functions/variables
# class bankaccount: # Wrong: hard to read
# class BANKACCOUNT: # Wrong: ALL_CAPS is for constants这个约定有助于在阅读代码时,将类与函数、变量(它们使用 snake_case)区分开来。
创建实例
从类创建实例看起来就像调用函数——你使用类名并加上一对括号:
class Product:
pass
# 创建三个不同的产品实例
item1 = Product()
item2 = Product()
item3 = Product()
# 每个实例都是独立对象
print(item1) # Output: <__main__.Product object at 0x...>
print(item2) # Output: <__main__.Product object at 0x...>
print(item3) # Output: <__main__.Product object at 0x...>
# 它们是不同对象,即使它们属于同一类型
print(item1 is item2) # Output: False
print(type(item1) is type(item2)) # Output: True每次调用 Product() 都会创建一个新的、相互独立的实例。内存地址(0x... 这部分)不同,确认它们是内存中的不同对象。
为什么从空类开始?
你可能会疑惑为什么我们从“什么都不做”的类开始。有两个原因:
-
概念清晰:理解类只是一个新类型,独立于其数据和行为,有助于你在加入复杂性之前先掌握基本概念。
-
实际用途:即使空类也可以作为标记或占位符使用。例如,你可以定义自定义异常类型:
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# 这些空类用作不同的错误类型不过,在真实代码中空类很少见。让我们给类添加一些数据,使它们更有用。
30.3) 创建实例并访问属性
当类能够持有数据时就变得有用了。在 Python 中,你可以随时通过简单赋值为实例添加属性(attribute)(附加在实例上的数据)。
为实例添加属性
你可以使用点号语法为实例添加属性:
class Student:
pass
# 创建一个实例
alice = Student()
# 添加属性
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
# 访问属性
print(alice.name) # Output: Alice Johnson
print(alice.student_id) # Output: S12345
print(alice.gpa) # Output: 3.8点号(.)运算符用于访问属性:alice.name 的意思是“获取 alice 对象的 name 属性”。这与你一直在字符串(比如 text.upper())和列表(比如 numbers.append(5))上使用的语法相同——那是在访问这些对象的方法与属性。
每个实例都有自己的属性
同一个类的不同实例拥有相互独立的属性:
class Student:
pass
# 创建两个学生
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# 每个实例都有自己的数据
print(alice.name) # Output: Alice Johnson
print(bob.name) # Output: Bob Smith
# 修改其中一个不会影响另一个
alice.gpa = 3.9
print(alice.gpa) # Output: 3.9
print(bob.gpa) # Output: 3.5 (unchanged)这种独立性至关重要:alice 和 bob 是具有各自数据的独立对象。修改 alice.gpa 不会影响 bob.gpa。
属性可以是任何类型
属性并不限于简单类型——它可以保存任何 Python 值:
class Student:
pass
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90] # 列表属性
student.contact = { # 字典属性
"email": "carol@example.com",
"phone": "555-0123"
}
student.is_active = True # 布尔属性
# 访问嵌套数据
print(student.grades[0]) # Output: 95
print(student.contact["email"]) # Output: carol@example.com这种灵活性让你可以用丰富的数据结构来建模复杂的现实世界实体。
访问不存在的属性
尝试访问不存在的属性会抛出 AttributeError:
class Student:
pass
student = Student()
student.name = "David Lee"
print(student.name) # Output: David Lee
# print(student.age) # AttributeError: 'Student' object has no attribute 'age'这个错误很有用——它能捕获拼写错误和逻辑错误:你以为某个属性存在,但实际上并不存在。
手动赋值属性的问题
虽然你可以在创建实例之后手动添加属性,但这种方式有严重缺点:
class Student:
pass
# 很容易忘记属性或把它们拼错
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# 忘了设置 gpa!
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346" # 拼写错误:stuent 而不是 student
bob.gpa = 3.5
# 现在 alice 缺少 gpa,而 bob 有拼写错误
# print(alice.gpa) # AttributeError
# print(bob.student_id) # AttributeError这既容易出错又繁琐。你需要一种方式确保每个实例一开始就具有正确的属性。这就是 __init__ 方法的用途,我们会在 30.5 节中介绍它。但在此之前,让我们先学习方法——属于类的函数。
30.4) 添加实例方法:理解 self
方法(method)是在类内部定义的函数,用来操作实例数据。它让你的类不仅有数据,还有行为。
定义一个简单方法
让我们给 Student 类添加一个方法:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# 创建实例并添加属性
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# 调用方法
alice.display_info() # Output: Alice Johnson - GPA: 3.8方法 display_info 在类内部用 def 定义,就像普通函数一样。关键区别在于第一个参数:self。
理解 self
self 参数是方法访问其正在操作的特定实例的方式。当你调用 alice.display_info() 时,Python 会自动把 alice 作为方法的第一个参数传入。在方法内部,self 指向 alice,因此 self.name 访问的是 alice.name,self.gpa 访问的是 alice.gpa。
下面是幕后发生的事情:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# 这两个调用是等价的:
alice.display_info() # 常规写法
Student.display_info(alice) # Python 实际执行的形式
# 两者都会输出:Alice Johnson - GPA: 3.8当你写 alice.display_info() 时,Python 会把它转换成 Student.display_info(alice)。实例(alice)在方法内部成为 self 参数。
为什么叫 "self"?
self 是一个约定俗成的名字,不是关键字。技术上你可以用任何名字:
class Student:
def display_info(this): # 可以运行,但别这么做
print(f"{this.name} - GPA: {this.gpa}")但是,始终使用 self。这是通用的 Python 约定,能让你的代码对其他 Python 程序员来说更易读。使用其他名字会让读者困惑,并违背社区规范。
多个实例的方法调用
当你有多个实例时,self 的威力就很明显:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# 创建两个学生
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# 相同的方法,不同的数据
alice.display_info() # Output: Alice Johnson - GPA: 3.8
bob.display_info() # Output: Bob Smith - GPA: 3.5当你调用 alice.display_info() 时,self 是 alice。当你调用 bob.display_info() 时,self 是 bob。同一段方法代码能适用于任意实例,因为 self 会适配到调用它的那个实例。
方法可以接收额外参数
方法除了 self 之外还可以接收其他参数:
class Student:
def update_gpa(self, new_gpa):
self.gpa = new_gpa
print(f"Updated {self.name}'s GPA to {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
alice.update_gpa(3.9) # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa) # Output: 3.9当你调用 alice.update_gpa(3.9) 时,Python 将 alice 作为 self 传入,将 3.9 作为 new_gpa 传入。方法签名是 def update_gpa(self, new_gpa),但调用时你只传一个参数——Python 会自动处理 self。
方法可以返回值
方法可以像普通函数一样返回值:
class Student:
def is_honors(self):
return self.gpa >= 3.5
def get_status(self):
if self.is_honors():
return "Honors Student"
else:
return "Regular Student"
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
print(alice.get_status()) # Output: Honors Student
print(bob.get_status()) # Output: Regular Student注意 get_status 是如何使用 self.is_honors() 调用另一个方法(is_honors)的。方法可以调用同一实例上的其他方法。
方法 vs 函数:何时使用哪个
你可能会想,什么时候该用方法,什么时候该用独立函数。这里有一个指导原则:
当操作满足以下条件时,使用方法:
- 需要访问实例数据(
self.name、self.gpa等) - 逻辑上属于该类型(这是 Student 做的事或 具有的特性)
- 会修改实例状态
当操作满足以下条件时,使用独立函数:
- 不需要实例数据
- 能处理多种类型
- 是通用工具
class Student:
# 方法:需要实例数据
def is_honors(self):
return self.gpa >= 3.5
# 函数:通用工具,适用于任何 GPA 值
def calculate_letter_grade(gpa):
if gpa >= 3.7:
return "A"
elif gpa >= 3.0:
return "B"
elif gpa >= 2.0:
return "C"
else:
return "D"
alice = Student()
alice.gpa = 3.8
# 使用方法进行与实例相关的检查
print(alice.is_honors()) # Output: True
# 使用函数进行通用计算
print(calculate_letter_grade(alice.gpa)) # Output: A
print(calculate_letter_grade(2.5)) # Output: C常见的方法模式
下面是一些你会频繁使用的常见模式:
Getter 方法(获取计算得到的信息):
class Student:
def get_full_info(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"Setter 方法(带验证地修改属性):
class Student:
def set_gpa(self, new_gpa):
if 0.0 <= new_gpa <= 4.0:
self.gpa = new_gpa
else:
print("Invalid GPA: must be between 0.0 and 4.0")Query 方法(回答是/否问题):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0Action 方法(执行操作):
class Student:
def add_grade(self, grade):
self.grades.append(grade)
# 根据所有成绩重新计算 GPA
self.gpa = sum(self.grades) / len(self.grades)30.5) 使用 __init__ 初始化实例
在创建实例后手动设置属性既繁琐又容易出错。__init__ 方法通过允许你在创建实例时就用数据初始化来解决这个问题。
__init__ 方法
__init__ 方法(读作 “dunder init” 或 “init”)是一个特殊方法,Python 会在你创建新实例时自动调用它:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# 创建带初始数据的实例
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.name) # Output: Alice Johnson
print(bob.gpa) # Output: 3.5当你写 Student("Alice Johnson", "S12345", 3.8) 时,Python 会:
- 创建一个新的空
Student实例 - 以该实例作为
self并带上你的参数调用__init__ - 返回已初始化的实例
__init__ 方法不会显式返回值——它通过设置属性在原地修改实例。如果你尝试从 __init__ 返回一个值,Python 会抛出 TypeError。
class Student:
def __init__(self, name):
self.name = name
# 不要从 __init__ 返回任何东西
# return self # Wrong! TypeError: __init__() should return None, not 'Student'__init__ 如何工作
让我们逐步拆解发生了什么:
class Student:
def __init__(self, name, student_id, gpa):
print(f"Initializing student: {name}")
self.name = name
self.student_id = student_id
self.gpa = gpa
print(f"Initialization complete")
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
print(alice.name) # Output: Alice Johnsonself 之后的参数(name、student_id、gpa)会成为创建实例时的必需参数。如果你不提供它们,Python 会抛出 TypeError:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# student = Student() # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice") # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8) # Correct这比手动赋值属性好得多——Python 会强制每个实例都从必需的数据开始。
在 __init__ 中使用默认参数值
你可以在 __init__ 中使用默认参数值,就像普通函数一样:
class Student:
def __init__(self, name, student_id, gpa=0.0):
self.name = name
self.student_id = student_id
self.gpa = gpa
# GPA 是可选的,默认值为 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346") # 使用默认 gpa=0.0
print(alice.gpa) # Output: 3.8
print(bob.gpa) # Output: 0.0这对那些有合理默认值、但在需要时也能自定义的属性很有用。
在 __init__ 中进行验证
你可以在 __init__ 中验证输入,以确保实例从一开始就处于有效状态:
class Student:
def __init__(self, name, student_id, gpa):
if not name:
print("Error: Name cannot be empty")
self.name = "Unknown"
else:
self.name = name
self.student_id = student_id
if 0.0 <= gpa <= 4.0:
self.gpa = gpa
else:
print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
self.gpa = 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa) # Output: 3.8
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name) # Output: Unknown
print(bob.gpa) # Output: 0.0这能确保即使有人传入无效数据,实例最终也会处于一个合理状态。
30.6) 使用 __str__ 和 __repr__ 的字符串表示
当你用 print() 打印一个实例,或在交互式 shell 中查看它时,Python 需要把它转换为字符串。默认情况下,你会得到一些没什么帮助的内容:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: <__main__.Student object at 0x...>默认输出显示类名和内存地址,但没有 Alice 的实际数据。你可以用 __str__ 和 __repr__ 这两个特殊方法来自定义它。
__str__ 方法
__str__ 方法定义了实例如何被 print() 和 str() 转换为字符串:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice)) # Output: Alice Johnson (S12345) - GPA: 3.8__str__ 方法应该返回对最终用户来说可读、信息丰富的字符串。把它当作“友好”的表示形式。
__repr__ 方法
__repr__ 方法定义实例的“官方”字符串表示,被 REPL 和 repr() 使用:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice)) # Output: Student('Alice Johnson', 'S12345', 3.8)在 REPL 中:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)__repr__ 方法应该返回一个看起来像有效 Python 代码的字符串,用于重建对象。把它当作“开发者”的表示形式——它应该清晰无歧义,并且对调试有用。
同时使用 __str__ 与 __repr__
你可以为不同目的同时定义这两个方法:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
# 友好、可读的格式
return f"{self.name} - GPA: {self.gpa}"
def __repr__(self):
# 无歧义、像代码一样的格式
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # 使用 __str__
# Output: Alice Johnson - GPA: 3.8
print(repr(alice)) # 使用 __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)在 REPL 中:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice) # Uses __str__
Alice Johnson - GPA: 3.8什么时候定义哪个方法
这里是指导原则:
- 总是定义
__repr__:它被 REPL 和调试工具使用。如果你只定义一个,就定义这个。 - 当你需要用户友好格式时定义
__str__:如果你的类会面向最终用户被打印出来,就提供一个可读的__str__。 - 如果你只定义
__repr__:Python 会在repr()中使用它,而str()也会回退使用__repr__(因此print()也会使用它)。 - 如果你只定义
__str__:print()会使用__str__,但repr()和 REPL 会使用默认的__repr__(显示内存地址)。这就是为什么定义__repr__通常更重要。
# 只定义了 __repr__
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product('{self.name}', {self.price})"
item = Product("Laptop", 999.99)
print(item) # 回退使用 __repr__
# Output: Product('Laptop', 999.99)
print(repr(item)) # 使用 __repr__
# Output: Product('Laptop', 999.99)集合中的字符串表示
当实例位于集合(列表、字典等)中时,Python 会使用 __repr__ 来显示它们,而不是 __str__:
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __str__(self):
return f"{self.name}: {self.gpa}"
def __repr__(self):
return f"Student('{self.name}', {self.gpa})"
students = [
Student("Alice", 3.8),
Student("Bob", 3.5),
Student("Carol", 3.9)
]
# 打印列表会对每个学生使用 __repr__
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
# 打印单个学生会使用 __str__
for student in students:
print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9这就是为什么 __repr__ 应该无歧义——它能帮助你在调试时理解数据结构中有什么内容。当你打印列表时,Python 本质上会对每个元素调用 repr(),以清晰地展示结构。
30.7) 创建多个相互独立的实例
类最强大的方面之一是,你可以创建许多彼此独立的实例,每个实例都有自己的数据。让我们深入探索这一点。
每个实例都有自己的数据
当你从同一个类创建多个实例时,每个实例都会维护自己独立的属性:
class BankAccount:
def __init__(self, account_number, holder_name, balance=0.0):
self.account_number = account_number
self.holder_name = holder_name
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
return True
else:
print(f"Insufficient funds. Balance: ${self.balance:.2f}")
return False
def __str__(self):
return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
# 创建三个相互独立的账户
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
# 对某个账户的操作不会影响其他账户
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
# 每个账户维护自己的余额
print(alice_account) # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account) # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account) # Output: Carol Davis's account (ACC-003): $2000.00这种独立性是面向对象编程的根本。每个实例都是具有自身状态的独立实体。
集合中的实例
你可以把实例存储在列表、字典或任何其他集合中:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
# 创建一个学生列表
students = [
Student("Alice Johnson", "S12345", 3.8),
Student("Bob Smith", "S12346", 3.2),
Student("Carol Davis", "S12347", 3.9),
Student("David Lee", "S12348", 3.4)
]
# 找出所有荣誉学生
honors_students = []
for student in students:
if student.is_honors():
honors_students.append(student)
print("Honors students:")
for student in honors_students:
print(f" {student.name}: {student.gpa}")
# Output:
# Honors students:
# Alice Johnson: 3.8
# Carol Davis: 3.9
# 计算平均 GPA
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}") # Output: Average GPA: 3.58这是一个常见模式:创建多个实例,把它们存入集合,然后用循环与推导式处理它们。
实例可以引用其他实例
实例可以拥有引用其他实例的属性,从而在对象之间建立关系:
class Course:
def __init__(self, course_code, course_name):
self.course_code = course_code
self.course_name = course_name
def __str__(self):
return f"{self.course_code}: {self.course_name}"
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.courses = [] # Course 实例的列表
def enroll(self, course):
self.courses.append(course)
print(f"{self.name} enrolled in {course.course_name}")
def list_courses(self):
print(f"{self.name}'s courses:")
for course in self.courses:
print(f" {course}")
# 创建课程
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
# 创建学生并为他们选课
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
# 列出每个学生的课程
alice.list_courses()
# Output:
# Alice Johnson's courses:
# CS101: Introduction to Python
# CS102: Data Structures
bob.list_courses()
# Output:
# Bob Smith's courses:
# CS101: Introduction to Python
# CS103: Web Development注意,Alice 和 Bob 都选了 python_course——它们引用的是同一个 Course 实例。这建模了现实世界中的关系:多个学生可以选同一门课。
实例标识与相等性
每个实例都是一个唯一对象,即使它和另一个实例拥有相同的数据:
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
# 不同对象,即使数据完全相同
print(alice1 is alice2) # Output: False
print(id(alice1) == id(alice2)) # Output: False默认情况下,== 也会检查标识(它们是否是同一个对象),而不是检查它们是否有相同的数据。在第 31 章,我们将学习如何用 __eq__ 特殊方法自定义相等性比较。
本章向你介绍了 Python 面向对象编程的基础。你已经学会了如何定义类、创建实例、添加方法、用 __init__ 初始化实例、控制字符串表示,以及处理多个相互独立的实例。这些概念构成了我们将在第 31 章和第 32 章探索的更高级 OOP 特性的基础。