在前几篇中,我们用 “函数 + 数据” 的方式写程序,这种方式适合简单任务,但面对复杂项目(比如开发一个电商系统,需要管理用户、商品、订单等多种实体)时,会遇到两个问题:一是数据和操作数据的函数分散在各处,难以维护;二是新增功能时,容易影响已有代码。这篇就带你学习 “面向对象编程(OOP)”—— 一种把 “数据” 和 “操作数据的函数” 封装在一起的思想,让代码更像现实世界的逻辑,更易扩展和复用。

一、面向对象的核心概念:类与对象

先理解两个最基础的概念,用生活中的例子类比:

-** 类(Class):是 “模板” 或 “蓝图”,描述某一类事物的共同特征和行为。比如 “学生类”,共同特征(属性)有 “姓名、年龄、成绩”,共同行为(方法)有 “上课、考试”。- 对象(Object)**:是 “类的实例”,根据类创建的具体个体。比如根据 “学生类” 创建 “张三”“李四” 这两个具体的学生对象,每个对象有自己的属性值(张三年龄 18,李四年龄 19)。

类比: 类就像 “汽车设计图纸”,对象就像根据图纸造出来的 “具体汽车”(张三的车、李四的车),图纸规定了汽车必须有轮子、能行驶(共同特征和行为),但每辆车的颜色、品牌可以不同(属性值不同)。

二、定义类:用 class 关键字创建 “模板”

在 Python 中,用class关键字定义类,格式如下:

class 类名:
    # 属性(数据)
    # 方法(操作数据的函数)

1. 最简单的类:包含属性和方法

以 “学生类(Student)” 为例,包含属性(姓名、年龄、成绩)和方法(显示信息、考试得分)。

class Student:
    # 初始化方法:创建对象时自动调用,用来设置属性
    def __init__(self, name, age, score=0):
        # self代表“当前对象”,self.属性名 表示对象的属性
        self.name = name  # 姓名属性
        self.age = age    # 年龄属性
        self.score = score  # 成绩属性(默认0)
    
    # 方法:显示学生信息
    def show_info(self):
        print(f"姓名:{self.name},年龄:{self.age},成绩:{self.score}")
    
    # 方法:考试得分(修改成绩属性)
    def get_score(self, points):
        self.score += points  # 成绩增加points分
        print(f"{self.name}考试得了{points}分,现在成绩是{self.score}")

-__init__方法:特殊的 “初始化方法”(构造函数),创建对象时必须调用,用来初始化属性。self是必须的第一个参数,代表当前对象。-** 方法 **:类中的函数,第一个参数必须是self,通过self.属性访问对象的属性。

2. 创建对象:用类实例化具体个体

定义类后,通过 “类名 (参数)” 创建对象(实例化),参数对应__init__方法中的参数(除了 self)。

# 创建学生对象(实例化Student类)
stu1 = Student("张三", 18)  # 成绩用默认值0
stu2 = Student("李四", 19, 80)  # 成绩设为80

# 调用对象的方法
stu1.show_info()  # 输出:姓名:张三,年龄:18,成绩:0
stu2.show_info()  # 输出:姓名:李四,年龄:19,成绩:80

# 调用方法修改属性
stu1.get_score(90)  # 输出:张三考试得了90分,现在成绩是90
stu2.get_score(5)   # 输出:李四考试得了5分,现在成绩是85

# 直接访问对象的属性(不推荐,最好通过方法操作)
print(f"{stu1.name}的最新成绩:{stu1.score}")  # 输出:张三的最新成绩:90

优势体现:每个对象的属性独立存储,调用方法时只影响当前对象(stu1 的成绩变化不会影响 stu2),数据和操作更 “内聚”。

三、面向对象的三大特性:封装、继承、多态

面向对象编程有三大核心特性,掌握它们能让代码更灵活、更易扩展。

1. 封装:隐藏内部细节,只暴露接口

封装就是 “把数据和操作数据的方法包装在类里”,并控制外部对内部属性的访问(比如不允许直接修改成绩,必须通过考试得分的方法)。

示例:限制成绩只能通过方法修改

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.__score = 0  # 用双下划线开头的属性是“私有属性”,外部不能直接访问
    
    # 提供方法访问私有属性
    def get_score(self):
        return self.__score
    
    # 提供方法修改私有属性(可以在方法里加验证)
    def add_score(self, points):
        if points < 0:
            print("错误:分数不能为负数!")
            return
        self.__score += points
        print(f"{self.name}的成绩增加了{points}分,现在是{self.__score}")

# 测试
stu = Student("张三", 18)
print("当前成绩:", stu.get_score())  # 输出:0

# 尝试直接修改私有属性(会失败)
stu.__score = -50  # 这行代码不会报错,但实际上修改的是一个新的公有属性,不是私有属性
print("直接修改后(错误方式):", stu.get_score())  # 输出:0(私有属性没变)

# 通过方法修改(正确方式)
stu.add_score(80)  # 输出:张三的成绩增加了80分,现在是80
stu.add_score(-10) # 输出:错误:分数不能为负数!
print("最终成绩:", stu.get_score())  # 输出:80

-** 私有属性 :用__属性名定义,外部不能直接访问或修改,只能通过类内部的方法操作,保证数据安全。- 封装的意义 **:比如成绩不能为负,这个规则封装在add_score方法里,外部不用关心实现细节,只需调用方法即可,减少错误。

2. 继承:子类复用父类的代码

继承允许 “子类(派生类)” 继承 “父类(基类)” 的属性和方法,还能新增自己的属性和方法,或重写父类的方法,实现 “代码复用”。

示例:学生类派生出小学生类和大学生类

# 父类:Student
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def study(self):
        print(f"{self.name}在学习")

# 子类:Pupil(小学生),继承自Student
class Pupil(Student):
    # 新增属性:grade(年级)
    def __init__(self, name, age, grade):
        # 调用父类的__init__方法,复用代码
        super().__init__(name, age)
        self.grade = grade
    
    # 重写父类的study方法
    def study(self):
        print(f"{self.name}({self.grade}年级小学生)在学习语文和数学")
    
    # 新增方法:做早操
    def do_morning_exercise(self):
        print(f"{self.name}在做早操")

# 子类:CollegeStudent(大学生),继承自Student
class CollegeStudent(Student):
    def __init__(self, name, age, major):
        super().__init__(name, age)
        self.major = major  # 新增属性:专业
    
    # 重写父类的study方法
    def study(self):
        print(f"{self.name}({self.major}专业大学生)在学习专业课程")

# 测试
pupil = Pupil("小明", 10, 4)
pupil.study()  # 输出:小明(4年级小学生)在学习语文和数学
pupil.do_morning_exercise()  # 输出:小明在做早操

college = CollegeStudent("小红", 20, "计算机")
college.study()  # 输出:小红(计算机专业大学生)在学习专业课程

-** 继承语法 class 子类名(父类名):-super()函数 **:在子类中调用父类的方法(比如super().__init__()调用父类的初始化方法),避免代码重复。-** 重写(Override)**:子类定义和父类同名的方法,会覆盖父类的方法,实现子类特有的逻辑。

3. 多态:同一操作,不同表现

多态指 “不同对象调用同名方法时,表现出不同的行为”,这是继承的自然结果,能让代码更灵活。

示例:用统一的方式调用不同子类的方法

# 定义一个函数,接收Student类型的对象(或其子类对象)
def let_study(student):
    student.study()  # 调用study方法,具体行为由对象类型决定

# 创建不同的对象
pupil = Pupil("小明", 10, 4)
college = CollegeStudent("小红", 20, "计算机")

# 调用同一个函数,传入不同对象,表现出不同行为
let_study(pupil)   # 输出:小明(4年级小学生)在学习语文和数学
let_study(college) # 输出:小红(计算机专业大学生)在学习专业课程

多态的好处是:函数let_study不需要关心传入的是 “小学生” 还是 “大学生”,只需调用study方法即可,新增 “中学生” 类时,无需修改let_study函数,符合 “开闭原则”(对扩展开放,对修改关闭)。

四、实战:用面向对象重构学生管理系统

把之前的 “学生信息管理系统” 用面向对象的方式重构,用Student类表示学生,StudentManager类管理学生集合(添加、查询、保存等),让代码结构更清晰。

完整代码:

import csv

# 学生类:封装学生的属性和方法
class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
    
    # 把对象转换为字典(方便保存到CSV)
    def to_dict(self):
        return {
            "name": self.name,
            "age": self.age,
            "score": self.score
        }

# 学生管理类:封装管理学生的功能
class StudentManager:
    def __init__(self, filename):
        self.filename = filename  # 数据文件路径
        self.students = []  # 存储学生对象的列表
        self.load_students()  # 初始化时加载数据
    
    # 加载学生数据(从CSV)
    def load_students(self):
        try:
            with open(self.filename, "r", encoding="utf-8") as f:
                reader = csv.DictReader(f)
                for row in reader:
                    # 创建Student对象并添加到列表
                    student = Student(
                        name=row["name"],
                        age=int(row["age"]),
                        score=float(row["score"])
                    )
                    self.students.append(student)
            print(f"已加载{len(self.students)}条学生数据")
        except FileNotFoundError:
            print(f"数据文件不存在,将创建新文件:{self.filename}")
    
    # 保存学生数据(到CSV)
    def save_students(self):
        with open(self.filename, "w", encoding="utf-8", newline="") as f:
            fieldnames = ["name", "age", "score"]
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            # 调用每个学生对象的to_dict方法
            writer.writerows([s.to_dict() for s in self.students])
        print(f"已保存{len(self.students)}条学生数据")
    
    # 添加学生
    def add_student(self, name, age, score):
        # 检查是否重名
        for s in self.students:
            if s.name == name:
                print(f"错误:学生{name}已存在!")
                return
        # 创建新学生对象并添加
        new_student = Student(name, age, score)
        self.students.append(new_student)
        print(f"已添加学生:{name}")
    
    # 显示所有学生
    def show_all(self):
        if not self.students:
            print("暂无学生数据")
            return
        print("所有学生信息:")
        for i, s in enumerate(self.students, 1):
            print(f"{i}. 姓名:{s.name},年龄:{s.age},成绩:{s.score}")

# 主程序
def main():
    manager = StudentManager("students_oop.csv")
    
    while True:
        print("\n=== 面向对象版学生管理系统 ===")
        print("1. 添加学生")
        print("2. 显示所有学生")
        print("3. 保存并退出")
        
        choice = input("请选择操作:")
        if choice == "1":
            name = input("请输入姓名:")
            try:
                age = int(input("请输入年龄:"))
                score = float(input("请输入成绩:"))
                manager.add_student(name, age, score)
            except ValueError:
                print("输入错误:年龄和成绩必须是数字!")
        elif choice == "2":
            manager.show_all()
        elif choice == "3":
            manager.save_students()
            print("退出系统,再见!")
            break
        else:
            print("输入错误,请重新选择")

if __name__ == "__main__":
    main()

重构后的优势

  • 数据和操作分离:Student类只关心单个学生的属性,StudentManager类只关心学生集合的管理,职责清晰;
  • 可扩展性强:如果需要给学生添加 “性别” 属性,只需修改Student类的__init__to_dict方法,不影响管理逻辑;
  • 代码复用率高:比如to_dict方法封装了对象转字典的逻辑,避免在保存数据时重复写转换代码。

下一篇预告:Python 常用库实战,解锁数据分析与自动化

学会了面向对象编程,你已经具备开发复杂项目的基础了。但 Python 的强大之处在于丰富的第三方库 —— 用几行代码就能实现数据分析、自动化办公、爬虫等高级功能。下一篇会带你实战几个最常用的库:pandas(数据分析)、openpyxl(操作 Excel)、requests(网络请求),让你快速解锁 Python 的实用技能,解决实际问题。

如果这篇的面向对象知识让你对代码结构有了新的理解,欢迎在评论区分享你的实践:比如你用类封装了什么功能,或者对继承、多态有什么疑问,我们一起交流~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐