Python & AI Tutorials Logo
Python 编程

30. 类与对象入门

30.1) 面向对象编程理念(构建你自己的类型)

在本书的学习过程中,你一直在使用 Python 的内置类型:整数、字符串、列表、字典等等。每种类型都把数据(比如字符串中的字符)与可以对这些数据执行的操作(比如 .upper().split())打包在一起。这种数据与行为的组合非常强大——它让你可以把字符串看作具备自身能力的完整实体,而不仅仅是原始的字符序列。

面向对象编程(object-oriented programming, OOP)扩展了这个思想:它让你创建自己的自定义类型,称为类(class),把与你的问题领域相关的数据与行为打包在一起。就像 Python 提供 str 类型用于处理文本、提供 list 类型用于处理序列一样,你也可以创建一个 BankAccount 类型来管理金融交易、一个 Student 类型来跟踪学业记录,或为库存系统创建一个 Product 类型。

为什么要创建你自己的类型?

想象你要在一个学校系统里管理学生信息。不使用类时,你可能会用一些独立变量或字典:

python
# 使用独立变量 - 很快就会变得混乱
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}

这种方法在简单场景下可行,但它有局限:

  1. 没有验证:没有任何东西能阻止你把 gpa 设置成 -5.0"excellent" 这种无效值
  2. 没有相关行为:像计算荣誉状态或格式化学生信息这样的操作会成为散落在代码各处的独立函数
  3. 没有类型检查:代表学生的字典看起来和任何其他字典都一样——Python 无法帮助你捕获错误,例如你本来需要学生字典却不小心传入了商品字典

类通过让你定义一个全新的类型来解决这些问题,这个类型准确表示“学生是什么”以及“对学生来说哪些操作是合理的”:

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 的特定数据。

Student 类
蓝图

alice 实例
Name: Alice Johnson
ID: S12345
GPA: 3.8

bob 实例
Name: Bob Smith
ID: S12346
GPA: 3.5

carol 实例
Name: Carol Davis
ID: S12347
GPA: 3.9

你可以从一个类创建任意多个实例,就像建筑师可以用一张蓝图建造许多房子。每个实例都有自己的数据(Alice 的 GPA 和 Bob 不同),但它们都共享由类定义的结构与能力。

你将在本章学到什么

本章介绍 Python 中面向对象编程的核心概念:

  1. 使用 class 关键字定义类
  2. 创建实例并访问它们的属性
  3. 添加方法来操作实例数据
  4. 理解 self 以及方法如何访问实例数据
  5. 使用 __init__ 方法初始化实例
  6. 使用 __str____repr__ 控制字符串表示
  7. 从同一个类创建多个相互独立的实例

在本章结束时,你将能够设计并实现自己的自定义类型,让程序更有组织、更易维护、表达力更强。我们会在第 31 章基于这些基础学习更高级的类特性,并在第 32 章学习继承与多态。

30.2) 使用 class 定义简单类

让我们先创建最简单的类——它仅定义一个新类型,但还没有任何数据或行为。

class 关键字

你用 class 关键字来定义类,后面跟类名和冒号:

python
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 的类名遵循一种特定约定,称为 CapWordsPascalCase:每个单词以大写字母开头,单词之间不使用下划线:

python
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)区分开来。

创建实例

从类创建实例看起来就像调用函数——你使用类名并加上一对括号:

python
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... 这部分)不同,确认它们是内存中的不同对象。

为什么从空类开始?

你可能会疑惑为什么我们从“什么都不做”的类开始。有两个原因:

  1. 概念清晰:理解类只是一个新类型,独立于其数据和行为,有助于你在加入复杂性之前先掌握基本概念。

  2. 实际用途:即使空类也可以作为标记或占位符使用。例如,你可以定义自定义异常类型:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# 这些空类用作不同的错误类型

不过,在真实代码中空类很少见。让我们给类添加一些数据,使它们更有用。

30.3) 创建实例并访问属性

当类能够持有数据时就变得有用了。在 Python 中,你可以随时通过简单赋值为实例添加属性(attribute)(附加在实例上的数据)。

为实例添加属性

你可以使用点号语法为实例添加属性:

python
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))上使用的语法相同——那是在访问这些对象的方法与属性。

每个实例都有自己的属性

同一个类的不同实例拥有相互独立的属性:

python
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)

这种独立性至关重要:alicebob 是具有各自数据的独立对象。修改 alice.gpa 不会影响 bob.gpa

属性可以是任何类型

属性并不限于简单类型——它可以保存任何 Python 值:

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

python
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'

这个错误很有用——它能捕获拼写错误和逻辑错误:你以为某个属性存在,但实际上并不存在。

手动赋值属性的问题

虽然你可以在创建实例之后手动添加属性,但这种方式有严重缺点:

python
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 类添加一个方法:

python
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.nameself.gpa 访问的是 alice.gpa

下面是幕后发生的事情:

python
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 是一个约定俗成的名字,不是关键字。技术上你可以用任何名字:

python
class Student:
    def display_info(this):  # 可以运行,但别这么做
        print(f"{this.name} - GPA: {this.gpa}")

但是,始终使用 self。这是通用的 Python 约定,能让你的代码对其他 Python 程序员来说更易读。使用其他名字会让读者困惑,并违背社区规范。

多个实例的方法调用

当你有多个实例时,self 的威力就很明显:

python
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() 时,selfalice。当你调用 bob.display_info() 时,selfbob。同一段方法代码能适用于任意实例,因为 self 会适配到调用它的那个实例。

alice.display_info

self = alice

bob.display_info

self = bob

访问 alice.name
alice.gpa

访问 bob.name
bob.gpa

方法可以接收额外参数

方法除了 self 之外还可以接收其他参数:

python
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

方法可以返回值

方法可以像普通函数一样返回值:

python
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.nameself.gpa 等)
  • 逻辑上属于该类型(这是 Student 的事或 具有的特性)
  • 会修改实例状态

当操作满足以下条件时,使用独立函数:

  • 不需要实例数据
  • 能处理多种类型
  • 是通用工具
python
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 方法(获取计算得到的信息):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Setter 方法(带验证地修改属性):

python
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 方法(回答是/否问题):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Action 方法(执行操作):

python
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 会在你创建新实例时自动调用它:

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 会:

  1. 创建一个新的空 Student 实例
  2. 以该实例作为 self 并带上你的参数调用 __init__
  3. 返回已初始化的实例

__init__ 方法不会显式返回值——它通过设置属性在原地修改实例。如果你尝试从 __init__ 返回一个值,Python 会抛出 TypeError

python
class Student:
    def __init__(self, name):
        self.name = name
        # 不要从 __init__ 返回任何东西
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

__init__ 如何工作

让我们逐步拆解发生了什么:

python
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 Johnson

self 之后的参数(namestudent_idgpa)会成为创建实例时的必需参数。如果你不提供它们,Python 会抛出 TypeError

python
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__ 中使用默认参数值,就像普通函数一样:

python
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__ 中验证输入,以确保实例从一开始就处于有效状态:

python
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 需要把它转换为字符串。默认情况下,你会得到一些没什么帮助的内容:

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() 转换为字符串:

python
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() 使用:

python
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 中:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

__repr__ 方法应该返回一个看起来像有效 Python 代码的字符串,用于重建对象。把它当作“开发者”的表示形式——它应该清晰无歧义,并且对调试有用。

同时使用 __str____repr__

你可以为不同目的同时定义这两个方法:

python
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 中:

python
>>> 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__ 通常更重要。
python
# 只定义了 __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__

python
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) 创建多个相互独立的实例

类最强大的方面之一是,你可以创建许多彼此独立的实例,每个实例都有自己的数据。让我们深入探索这一点。

每个实例都有自己的数据

当你从同一个类创建多个实例时,每个实例都会维护自己独立的属性:

python
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

这种独立性是面向对象编程的根本。每个实例都是具有自身状态的独立实体。

集合中的实例

你可以把实例存储在列表、字典或任何其他集合中:

python
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

这是一个常见模式:创建多个实例,把它们存入集合,然后用循环与推导式处理它们。

实例可以引用其他实例

实例可以拥有引用其他实例的属性,从而在对象之间建立关系:

python
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 实例。这建模了现实世界中的关系:多个学生可以选同一门课。

实例标识与相等性

每个实例都是一个唯一对象,即使它和另一个实例拥有相同的数据:

python
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 特性的基础。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai