码上问学问代码之理,学编程之道

第 6 章 · 面向对象

当程序越来越复杂,“一堆零散变量 + 函数”会越来越难管。面向对象(OOP)把“数据”和“操作数据的方法”打包成类,让我们像搭积木一样组织大系统。这一章讲类与实例、封装、继承、多态,并用一个“图书管理”小系统落到实处。

类与实例:先画“图纸”,再造“实物”

类是图纸,实例是按图纸造出的实物:一份图纸,多个实例class Student:(图纸)属性:name, scores方法:avg(), add_score()描述「学生长什么样、会什么」类本身不存具体数据Student(...)Student(...)s1 实例name='李白' scores=[92,88]s2 实例name='杜甫' scores=[75,91]每个实例有自己的一份数据(self),共用同一套方法self = 「当前这个实例自己」——s1.avg() 时 self 就是 s1
先讲为什么“学生”是一个概念(类),“张三”是一个具体的人(实例)。类定义“长什么样、能干什么”,实例是真正参与程序运行的个体。这样同类的对象共享逻辑、各自持有数据。
class.pypython可运行
class Student:
    def __init__(self, name, score):   # 构造方法:创建时自动调用
        self.name = name
        self.score = score

    def level(self):                    # 方法:附着在实例上的函数
        return "优秀" if self.score >= 90 else "加油"

s = Student("张三", 92)     # 用类“造”一个实例
print(s.name, s.level())    # 张三 优秀
运行输出Python 运行结果
运行结果: 张三 优秀
讲透
  • self 代表“当前这个实例”,方法里通过它访问自己的数据。
  • __init__ 是构造器:每次 Student(...) 都会执行它来初始化。
  • s.level() 看起来像函数,其实“知道自己是张三、92 分”——这就是数据和行为绑在一起。
避坑调用方法时忘记加括号,会打印出 <bound method ...> 而不是结果。方法名后一定要写 (),哪怕它不需要参数。

再举一个生活化例子:每只宠物都是一个实例,调用方法就“自我介绍”。

pet.pypython可运行
class Pet:
    def __init__(self, name, kind):
        self.name = name
        self.kind = kind

    def introduce(self):
        return f"我是{self.kind} {self.name}"

cat = Pet("咪咪", "猫")
dog = Pet("旺财", "狗")
print(cat.introduce())
print(dog.introduce())
运行输出Python 运行结果
运行结果: 我是猫 咪咪 我是狗 旺财

贴近真实项目:把“商品”也做成一个类,状态(库存)与方法(购买)绑在一起,方便在购物车 / 库存系统里复用。

product.pypython可运行
class Product:
    def __init__(self, name, price, stock):
        self.name = name
        self.price = price
        self.stock = stock

    def buy(self, n):
        if n <= self.stock:
            self.stock -= n
            return f"购买{n}件{self.name},单价{self.price}元"
        return "库存不足"

p = Product("笔记本", 5, 3)
print(p.buy(2))
print("剩余库存:", p.stock)
运行输出Python 运行结果
运行结果: 购买2件笔记本,单价5元 剩余库存: 1

封装:把“内部细节”藏起来

encap.pypython可运行
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance      # 双下划线:私有,外部不能直接改

    def deposit(self, n):
        if n > 0:
            self.__balance += n

    def get_balance(self):
        return self.__balance

a = Account("李四", 100)
a.deposit(50)
print(a.get_balance())     # 150
# a.__balance = 9999      # 这样改不了,保护数据
运行输出Python 运行结果
运行结果: 150
讲透封装的意义是“只暴露该暴露的,保护不该被乱改的”。余额必须通过 deposit 这类受控方法修改,避免被随意篡改——这是大型系统少出 bug 的关键习惯。
避坑双下划线 __balance 并不是绝对私有。Python 只是把它改名为 _Account__balance,外部仍能(但不该)这样访问。封装靠的是约定 + 受控方法,而不是强制锁死;单下划线同样拦不住外部读写。

生活化例子:用单下划线标记“内部属性”,通过方法去读和改。

person.pypython可运行
class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age          # 单下划线:约定“别随便改”

    def get_age(self):
        return self._age

    def grow(self):
        self._age += 1

p = Person("小明", 10)
p.grow()
print(p.name, "今年", p.get_age(), "岁")
运行输出Python 运行结果
运行结果: 小明 今年 11 岁

贴近真实项目:注册用户用双下划线保护密码,只允许通过受控方法设置与校验。

user.pypython可运行
class User:
    def __init__(self, username):
        self.username = username
        self.__password = None     # 双下划线“私有”

    def set_password(self, pwd):
        if len(pwd) >= 6:
            self.__password = pwd
            return "密码设置成功"
        return "密码太短,至少 6 位"

    def login(self, pwd):
        return pwd == self.__password

u = User("admin")
print(u.set_password("123"))
print(u.set_password("abc123"))
print(u.login("abc123"))
print(u.login("wrong"))
运行输出Python 运行结果
运行结果: 密码太短,至少 6 位 密码设置成功 True False

继承与多态:复用与扩展

继承:子类自动拥有父类的能力,再「加点自己的」或「改掉不合用的」class Animal:eat() sleep()class Dog(Animal):新增 bark();eat() 可覆写会 eat + sleep + barkclass Cat(Animal):新增 meow();eat() 可覆写会 eat + sleep + meow多态:同一个 animal.eat() 调用,Dog 走狗的吃法、Cat 走猫的吃法——调用方不用关心是谁原则:子类是父类的一种(is-a)才继承;只是「用了」不等于「是」
先讲为什么“研究生”是“学生”的一种,本该自动拥有学生的所有能力,再额外加点自己的。继承让你“站在父类肩膀上”扩展;多态让“同一句调用”对不同对象表现不同。
inherit.pypython可运行
class Animal:
    def speak(self):
        return "..."

class Dog(Animal):              # 继承 Animal
    def speak(self):            # 重写(多态)
        return "汪汪"

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

for a in [Dog(), Cat()]:
    print(a.speak())            # 同一调用,不同表现
运行输出Python 运行结果
运行结果: 汪汪 喵喵
讲透Dog(Animal) 表示“狗是一种动物”。多态让循环里不必关心具体是狗还是猫,只管调用 speak()——系统自己选对版本。这种“面向接口而非实现”的思想,是写出可扩展程序的核心。
避坑子类如果重写了 __init__ 却忘了调用 super().__init__(),父类的属性就不会被初始化,之后访问会抛出 AttributeError。记住:在子类构造函数开头加一句 super().__init__(...)。

生活化例子:员工是基类,经理 / 实习生继承并重写 work(),多态统一调用。

employee.pypython可运行
class Employee:
    def __init__(self, name):
        self.name = name

    def work(self):
        return f"{self.name} 在工作"

class Manager(Employee):
    def work(self):
        return f"{self.name} 在开会安排任务"

class Intern(Employee):
    def work(self):
        return f"{self.name} 在帮忙打杂"

for e in [Manager("王总"), Intern("小李"), Employee("小张")]:
    print(e.work())
运行输出Python 运行结果
运行结果: 王总 在开会安排任务 小李 在帮忙打杂 小张 在工作

贴近真实项目:承接章末“图书管理”,把 Book 当作基类,纸质书 / 有声书各自重写 read()。

book_types.pypython可运行
class Book:
    def __init__(self, title):
        self.title = title

    def read(self):
        return f"读《{self.title}》"

class PaperBook(Book):
    def read(self):
        return f"翻开《{self.title}》纸质书阅读"

class AudioBook(Book):
    def read(self):
        return f"播放《{self.title}》有声书"

for b in [PaperBook("Python 编程"), AudioBook("数据结构"), Book("未分类")]:
    print(b.read())
运行输出Python 运行结果
运行结果: 翻开《Python 编程》纸质书阅读 播放《数据结构》有声书 读《未分类》

实战:用类搭一个“图书管理”小系统

综合 类 / 封装 / 列表

把前面零散的概念收进一个可运行的小系统:Book 类封装一本书,Library 管理多本书的增删与查询。

library.pypython可运行
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.borrowed = False

    def borrow(self):
        if not self.borrowed:
            self.borrowed = True
            return f"《{self.title}》借出成功"
        return "已被借走"

class Library:
    def __init__(self):
        self.books = []

    def add(self, book):
        self.books.append(book)

    def list_available(self):
        return [b.title for b in self.books if not b.borrowed]

lib = Library()
lib.add(Book("Python 编程", "张三"))
lib.add(Book("数据结构", "李四"))
print("可借:", lib.list_available())
print(lib.books[0].borrow())
print("借后剩余可借:", lib.list_available())
运行输出Python 运行结果
运行结果: 可借: ['Python 编程', '数据结构'] 《Python 编程》借出成功 借后剩余可借: ['数据结构']
讲透Library 持有 books 列表(组合关系),Book 自己管“是否被借”的状态。当你开始把现实事物拆成“对象 + 行为”,你就从“写脚本”迈入了“做系统”。

第 6 章小结

动手练习

1定义一个基类 Shape,其 area() 返回 0;再定义子类 Circle(r) 和 Square(side),分别重写 area() 计算面积(圆面积用 3.14 × r²,注意用 round(x, 2) 处理小数)。最后把它们放进一个列表,循环调用 area() 并打印结果。

提示:子类在 __init__ 里记下半径 / 边长,area() 里返回计算结果;浮点结果用 round(..., 2) 固定两位小数。

answer.pypython可运行
class Shape:
    def area(self):
        return 0

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return round(3.14 * self.r * self.r, 2)

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side * self.side

for s in [Circle(2), Square(5), Circle(1)]:
    print(type(s).__name__, "面积:", s.area())
运行输出Python 运行结果
运行结果: Circle 面积: 12.56 Square 面积: 25 Circle 面积: 3.14