跳转至

第5章 面向对象


本章学习目标

学完本章后,你应该能够: 1. 解释对象、类、封装、继承、多态等基本概念 2. 使用 class 定义一个简单的 Python 类 3. 区分类属性与实例属性 4. 理解并使用 __init__()__str__() 等常见魔法方法 5. 编写类方法与静态方法 6. 使用继承、方法重写和多态组织代码

目录

  1. 面向对象编程介绍
  2. 类与对象
  3. 魔法方法
  4. 类方法和静态方法
  5. 继承、重写与多态
  6. 本章总结与练习

1. 面向对象编程介绍

面向对象编程(Object-Oriented Programming, OOP)是一种组织程序的思想。
它强调:把现实世界中的事物抽象成“对象”,再让对象之间协作完成任务。

在面向对象思想中,我们不只是写“步骤”,而是更关注:

  • 这个系统里有哪些“对象”?
  • 每个对象有什么“属性”?
  • 每个对象能做什么“行为”?
  • 对象之间是什么关系?

例如,在“学生管理系统”中,我们可以把“学生”看作一个对象: - 属性:姓名、学号、年龄、成绩 - 行为:学习、选课、打印成绩单

1.1 面向对象的主要概念

(1)对象

万物皆对象。
任何一个操作或业务逻辑,最终都需要由某个“实体”来完成,这个实体就是对象。

对象通常由两部分组成: - 属性:描述对象“是什么”,偏静态
例如:姓名、年龄、颜色、重量 - 方法:描述对象“能做什么”,偏动态
例如:吃饭、奔跑、打印信息

(2)类

类是对一群具有相同特征和行为的事物的抽象。
可以把类理解成“模板”或“蓝图”,而对象是根据类创建出来的“实例”。

例如: - 类:Person - 对象:张三、李四、王五

也就是说: - 解决“这一类东西长什么样、会做什么” - 对象解决“具体是哪一个”

1.2 面向对象的三个特征

1)封装(Encapsulation)

把属性和方法组织到一个类中,对外暴露必要接口,隐藏内部实现细节。

作用: - 保护数据 - 降低耦合 - 便于维护

2)继承(Inheritance)

子类可以复用父类已有的属性和方法。

作用: - 代码复用 - 结构清晰 - 便于扩展

3)多态(Polymorphism)

同样的方法调用,作用于不同对象时,可以表现出不同的行为。

作用: - 提高程序灵活性 - 让程序更容易扩展

1.3 一个直观类比

以“动物”为例:

  • 类:Animal
  • 对象:猫、狗、鸟
  • 属性:名字、年龄、颜色
  • 方法:叫、跑、吃

如果继续细分: - Dog 可以继承 Animal - Cat 也可以继承 Animal - 它们都可以有 speak() 方法 - 但 Dog.speak()Cat.speak() 的表现不同

这就是后面要讲的继承与多态。

2. 类与对象

2.1 创建类与实例对象

Python 中定义类的基本语法如下:

class 类名:
    属性
    方法

创建对象(实例)的语法如下:

实例名 = 类名(参数)

创建完成后,可以通过 . 访问对象的属性和方法:

实例名.属性名
实例名.方法名()
class Animal:
    name = '动物'

    def introduce(self):
        print("我是一只动物。")

a = Animal()
print(a.name)
a.introduce()
动物
我是一只动物。

讲解

上面的代码中:

  • Animal 是一个类
  • aAnimal 的一个实例对象
  • name 是类中的一个属性
  • introduce() 是类中的一个方法
  • self 表示“当前对象本身”

在调用 a.introduce() 时,Python 会自动把 a 作为第一个参数传给 self

2.2 类属性与实例属性

(1)类属性

类属性是直接定义在类中的属性,属于“整个类”,通常被所有实例共享。

例如,上面的 name = '动物' 就是类属性。

当修改类属性时,所有未单独覆盖该属性的实例都会看到变化。

class Animal:
    name = '动物'

    def introduce(self):
        print("我是一只动物。")

a = Animal()
b = Animal()
b.name = "dog"

print("修改前:", Animal.name, a.name, b.name)

Animal.name = "猫"

print("修改后:", Animal.name, a.name, b.name)
修改前: 动物 动物 dog
修改后: 猫 猫 dog

观察与思考

因为 a.nameb.name 默认都会去类中查找 name
所以当我们修改 Animal.name 后,两个实例看到的值也一起变化了。

这说明:类属性属于类本身,被实例共享。

2.3 实例属性

实例属性属于“某一个具体对象”,通常通过: - 在 __init__() 中定义 - 或者直接通过 对象名.属性 = 值 动态增加

下面先看动态增加属性的方式。

class Animal:
    __name = '动物'   # 私有类属性

    def introduce(self):
        print(f"我是一只{self.__name}。")

a = Animal()
a.weight = 2   # 动态给实例添加属性

print("a.weight =", a.weight)
a.introduce()
a.weight = 2
我是一只动物。

讲解

这里的 weight 只属于对象 a,如果我们再创建一个对象 b,它默认并没有这个属性。

b = Animal()

print("a 是否有 weight?", hasattr(a, "weight"))
print("b 是否有 weight?", hasattr(b, "weight"))
a 是否有 weight? True
b 是否有 weight? False

2.4 公有属性与私有属性

在 Python 中,约定上:

  • 普通名字:通常视为公有属性
  • 以两个下划线开头:会触发名称重整(name mangling),常用于“私有化”处理

例如:

class Animal:
    __name = "动物"

这表示不希望在类外部直接访问 __name

class Animal:
    __name = "动物"

    def introduce(self):
        print(f"我是一只{self.__name}。")

a = Animal()
a.introduce()
# print(a.__name)
# 直接访问私有属性会报错,先演示安全写法
print("对象字典:", dir(a)[:10], "...")
我是一只动物。
对象字典: ['_Animal__name', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__'] ...

注意:Python 的“私有”更准确地说是一种访问限制机制/命名机制
不是像某些语言那样的绝对私有。

3. 魔法方法

Python 中有一类特殊方法,名字形如:

__方法名__

这类方法通常在特定场景下被 Python 自动调用,因此常称为: - 魔法方法 - 特殊方法 - 双下划线方法

常见例子: - __init__():初始化对象 - __str__():定义对象的字符串显示形式 - __len__():定义 len(对象) 的行为 - __del__():对象销毁时调用(一般不建议依赖)

3.1 __init__() —— 构造方法

当我们创建对象时,__init__() 会自动执行。
它最常见的作用就是:给实例属性赋初值

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        print(f"大家好,我叫{self.name},今年{self.age}岁。")

s1 = Student("小明", 18)
s2 = Student("小红", 19)

s1.introduce()
s2.introduce()
大家好,我叫小明,今年18岁。
大家好,我叫小红,今年19岁。

讲解

这里: - self.nameself.age 是实例属性 - 每创建一个对象,都可以拥有自己的姓名和年龄 - 这也是实例属性最常见的创建方式

3.2 __str__() —— 字符串表示方法

如果直接打印一个普通对象,通常会看到类似内存地址的信息,不够友好。
我们可以通过 __str__() 自定义对象被 print() 时的显示内容。

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __str__(self):
        return f"《{self.title}》- 价格:{self.price}元"

b = Book("Python程序设计", 59)
print(b)
《Python程序设计》- 价格:59元

讲解

当执行 print(b) 时,Python 会自动调用 b.__str__()

这使得对象输出更直观,也更适合调试和展示。

3.3 再看一个更完整的类设计

下面我们把 __init__()__str__() 结合起来,构造一个更规范的类。

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

    def eat(self):
        print(f"{self.name}正在吃东西。")

    def __str__(self):
        return f"Animal(name={self.name}, weight={self.weight})"

cat = Animal("小花猫", 4.2)
print(cat)
cat.eat()
Animal(name=小花猫, weight=4.2)
小花猫正在吃东西。

4. 类方法和静态方法

除了普通实例方法外,Python 还支持: - 类方法 @classmethod - 静态方法 @staticmethod

它们和普通方法的区别,核心在于:它们依赖谁来调用、能够访问什么数据。

4.1 实例方法、类方法、静态方法对比

1)实例方法

  • 第一个参数是 self
  • 由对象调用
  • 可以访问实例属性,也可以访问类属性

2)类方法

  • 使用 @classmethod
  • 第一个参数通常写成 cls
  • 可以由类直接调用
  • 可以访问类属性,但不能直接访问某个具体实例的属性

3)静态方法

  • 使用 @staticmethod
  • 不需要 selfcls
  • 更像是“写在类里的普通函数”
  • 通常用于和类相关、但又不依赖实例或类状态的功能
class Teacher:
    school = "示例大学"

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

    def instance_method(self):
        print(f"实例方法:我是{self.name},来自{self.school}")

    @classmethod
    def class_method(cls):
        print(f"类方法:学校是{cls.school}")

    @staticmethod
    def static_method():
        print("静态方法:这是一段与具体对象无关的工具逻辑")

t = Teacher("王老师")
t.instance_method()
t.class_method()
t.static_method()

print("-" * 30)
Teacher.class_method()
Teacher.static_method()
实例方法:我是王老师,来自示例大学
类方法:学校是示例大学
静态方法:这是一段与具体对象无关的工具逻辑
------------------------------
类方法:学校是示例大学
静态方法:这是一段与具体对象无关的工具逻辑

4.2 类方法示例:统计创建了多少个对象

类方法常用于操作类级别的数据,比如统计对象数量。

class User:
    count = 0

    def __init__(self, name):
        self.name = name
        User.count += 1

    @classmethod
    def show_count(cls):
        print(f"当前共创建了 {cls.count} 个用户对象")

u1 = User("张三")
u1.show_count()
u2 = User("李四")
u1.show_count()
u3 = User("王五")


# User.show_count()
u1.show_count()
当前共创建了 1 个用户对象
当前共创建了 2 个用户对象
当前共创建了 3 个用户对象

4.3 静态方法示例:工具函数

静态方法适合封装一些“逻辑上属于这个类,但不依赖实例状态”的功能。

class MathTool:
    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def is_even(n):
        return n % 2 == 0

print(MathTool.add(3, 5))
print(MathTool.is_even(10))

5. 继承、重写与多态

这是面向对象中非常重要的一部分。
它们能让程序结构更清晰,也更容易扩展。

5.1 继承

继承的语法:

class 子类(父类):
    类体

子类会继承父类的公有属性和公有方法。
这样可以减少重复代码,并且让“同一类事物”的层次关系更加清晰。

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

    def eat(self):
        print(f"{self.name}正在吃东西。")

class Dog(Animal):
    def bark(self):
        print(f"{self.name}正在汪汪叫。")

d = Dog("旺财")
d.eat()     # 继承自父类
d.bark()    # 子类自己的方法
旺财正在吃东西。
旺财正在汪汪叫。

讲解

在这个例子中: - Animal 是父类 - Dog 是子类 - Dog 自动拥有了 Animal__init__()eat() - 同时 Dog 还能扩展自己的 bark()

5.2 方法重写(Override)

如果父类中的某个方法不完全适合子类,
就可以在子类中定义同名方法进行重写。

class Animal:
    def speak(self):
        print("动物会发出声音。")

class Dog(Animal):
    def speak(self):
        print("小狗:汪汪汪!")

class Cat(Animal):
    def speak(self):
        print("小猫:喵喵喵!")

a = Animal()
d = Dog()
c = Cat()

a.speak()
d.speak()
c.speak()
动物会发出声音。
小狗:汪汪汪!
小猫:喵喵喵!

讲解

虽然这三个类都有 speak() 方法,但它们的行为不同。
这正是后面“多态”的基础。

5.3 super() 的使用

在子类中,如果既想保留父类原有逻辑,又想增加自己的逻辑,
可以使用 super() 调用父类方法。

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

    def introduce(self):
        print(f"我是动物,我叫{self.name}。")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # 调用父类构造方法
        self.breed = breed

    def introduce(self):
        super().introduce()      # 调用父类方法
        print(f"我是一只{self.breed}。")

dog = Dog("豆豆", "金毛")
dog.introduce()
我是动物,我叫豆豆。
我是一只金毛。

5.4 多态

多态强调的是:

相同的方法调用,面对不同对象,可以产生不同结果。

这使得我们在写程序时,可以面向“统一接口”编程,而不是对每种对象都写一套逻辑。

class Dog:
    def speak(self):
        print("狗:汪汪汪")

class Cat:
    def speak(self):
        print("猫:喵喵喵")

class Bird:
    def speak(self):
        print("鸟:叽叽喳喳")

def animal_speak(animal):
    animal.speak()

animals = [Dog(), Cat(), Bird()]
for animal in animals:
    animal_speak(animal)
狗:汪汪汪
猫:喵喵喵
鸟:叽叽喳喳

讲解

这里的 animal_speak() 并不关心传进来的到底是狗、猫还是鸟。
它只要求:这个对象有 speak() 方法

这就是多态带来的灵活性。

5.5 一个综合示例:图形面积计算

下面用“图形”来演示继承 + 重写 + 多态的组合。

class Shape:
    def area(self):
        raise NotImplementedError("子类必须重写 area() 方法")

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

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

    def area(self):
        return 3.14 * self.radius * self.radius

def print_area(shape):
    print(f"面积 = {shape.area():.2f}")

shapes = [Rectangle(3, 4), Circle(2)]
for s in shapes:
    print_area(s)
面积 = 12.00
面积 = 12.56

教学提示

这个例子很适合课堂总结: - Shape 代表抽象父类 - RectangleCircle 重写了 area() - print_area() 不需要区分到底是哪种图形 - 这正是多态的实际价值

6. 本章总结

本章围绕 Python 面向对象编程展开,核心内容如下:

6.1 关键概念回顾

  • 对象:具有属性和行为的实体
  • :创建对象的模板
  • 封装:把数据与操作组织在一起,对外提供接口
  • 继承:子类复用父类代码
  • 多态:同一调用在不同对象上表现不同

6.2 技术点回顾

  • 使用 class 定义类
  • 使用对象访问属性和方法
  • 区分类属性与实例属性
  • 使用 __init__() 初始化对象
  • 使用 __str__() 优化输出
  • 理解类方法和静态方法
  • 通过继承、重写和多态提升代码复用性和扩展性

6.3 一句话理解本章

面向对象编程,就是把程序拆成一个个“有状态、能行动”的对象,让它们协作完成任务。

7. 课堂练习

练习 1:创建一个学生类

要求: 1. 定义 Student 类 2. 包含姓名、年龄两个属性 3. 定义 introduce() 方法输出个人信息

可以先自己写,再参考答案。

# 练习 1 参考答案
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        print(f"我叫{self.name},今年{self.age}岁。")

stu = Student("小华", 20)
stu.introduce()

练习 2:设计一个汽车类

要求: 1. 定义 Car 类 2. 属性:品牌、颜色 3. 方法:run() 输出“某品牌汽车正在行驶”

试着补全下面代码。

# 练习 2 参考答案
class Car:
    def __init__(self, brand, color):
        self.brand = brand
        self.color = color

    def run(self):
        print(f"{self.color}{self.brand}汽车正在行驶。")

car = Car("比亚迪", "白色")
car.run()

练习 3:继承与重写

要求: 1. 定义父类 Animal 2. 定义子类 DogCat 3. 每个子类都重写 speak() 方法

# 练习 3 参考答案
class Animal:
    def speak(self):
        print("动物发出声音")

class Dog(Animal):
    def speak(self):
        print("汪汪汪")

class Cat(Animal):
    def speak(self):
        print("喵喵喵")

for animal in [Dog(), Cat()]:
    animal.speak()

8. 课后思考题

  1. 类属性和实例属性的本质区别是什么?
  2. 为什么 __init__() 中通常使用 self.属性名 = 值
  3. 类方法和静态方法分别适合什么场景?
  4. 为什么说多态能提高程序的扩展性?
  5. 在实际项目中,什么时候应该使用继承,什么时候更适合组合?