第三章

封装:类与代码复用进阶

第四层 + 第五层 · 模块/类/继承/多态/泛型 书店开始卖杂志和文创,商品体系乱成一锅粥

一、王姐又来了:我们要卖杂志和文创

第三章开工第一天,王姐笑盈盈地走进仓库:"周哥啊,光卖书太单薄了。我打算加两个品类——杂志,还有文创(帆布袋、马克杯、书签)。你给系统加上呗?"

小陈打开代码,看着第二章的结构体陷入了沉思:

Book = namedtuple("Book", ["title", "author", "price", "stock"])

# 杂志呢?有刊号、有期数、还有定价
Magazine = namedtuple("Magazine", ["title", "issue_no", "price", "stock"])

# 文创呢?没有作者,有材质、有款式
Merch = namedtuple("Merch", ["name", "material", "style", "price", "stock"])

"师傅,三个结构体,字段还长得差不多……更麻烦的是,之前写的那些函数——apply_discountrestockcount_nodes——每个都只认 Book,现在杂志和文创全用不了,我还得复制三份?"

"哈,绕回来了。"老周说,"复制粘贴的问题又出现了,这次出现在'数据'上。 光靠结构体+函数这种'散装'写法,已经撑不住了。该上了。"


二、第四层:抽象与封装

2.1 类:把数据(字段)和操作(方法)打包

老周写下:

class Product:
    """商品:把'长什么样'(字段)和'能干什么'(方法)打包在一起。"""

    def __init__(self, name, price, stock):
        # __init__ 是"构造方法":对象出生时必须执行,保证它一出生就是合法的
        if price < 0:
            raise ValueError("价格不能是负数!")
        self.name = name
        self.price = price
        self.stock = stock

    def apply_discount(self, rate):
        """给自己打折。注意:数据和方法绑在一起了。"""
        self.price = self.price * rate

    def restock(self, amount):
        """补货。直接改自己的库存。"""
        self.stock += amount

    def sell(self, n):
        """卖出 n 件。"""
        if n > self.stock:
            raise ValueError("库存不足!")
        self.stock -= n

"这就是类(class)。"老周敲着黑板说,"它跟结构体的区别就一句话:结构体只装数据,类把数据(字段)和操作(方法)打包成一个整体。 从此,sell 是书自己的行为,restock 是书自己的能力——世界清爽了。"

小陈试用:

book = Product("三体", 29.9, 120)
book.sell(2)            # 卖 2 本
book.restock(50)        # 补 50 本
print(book.stock)       # 168

"注意 __init__,这是构造方法(constructor)。"老周特意加重语气,"它的意义是:对象出生即合法。看那行 if price < 0: raise ValueError——你根本不可能造出一个'负价格'的商品,因为出生那一刻就被拦下了。这解决了'对象没初始化就用了'的麻烦。"

2.2 模块 / 命名空间:给代码分房间

"现在代码多了,全堆在一个文件里,名字动不动就撞车。"老周把文件整理给他看:

shop/
├── models.py     ← 放 Product 类
├── pricing.py    ← 放折扣、促销逻辑
├── reports.py    ← 放盘点、报表逻辑
└── main.py       ← 主程序
# pricing.py
from models import Product          # 从 models 模块导入 Product

def weekend_sale(p: Product):
    p.apply_discount(0.92)          # 周末全场 92 折

"这叫模块(module),有的语言叫包(package)命名空间(namespace)。作用一句话:把相关函数和数据分组,避免名字冲突。 models.Productpricing.Product 就算重名也不打架,因为它们住在不同的'房间'里。"

2.3 访问控制:public / private / protected

"但问题来了。"老周把代码里 self.stock 圈出来,"现在谁都能直接改库存:product.stock = -999——一个负数库存的'三体'就这么诞生了。刚才构造方法好不容易堵住的漏洞,又被'外部直接改字段'撕开了。"

"所以类提供了访问控制:字段和方法可以标上'私密程度'。"

class Product:
    def __init__(self, name, price, stock):
        self.__price = price      # 双下划线开头:私有字段,外部不能直接碰
        self.__stock = stock

    def get_price(self):          # 公开的"读"接口
        return self.__price

    def restock(self, amount):    # 公开的"改"接口——要走正规流程
        if amount < 0:
            raise ValueError("补货量不能为负!")
        self.__stock += amount
  • public(公开):谁都能用(restockget_price)。
  • private(私有):只有类内部能用(__price),外部想读写必须走公开方法。
  • protected(受保护):自己和子类能用,外部不行。

"这就是封装(encapsulation)隐藏细节,暴露接口。 外面的代码只跟 restock 打交道,至于库存到底存在哪个变量里、怎么校验——那是商品自己的私事。你想改内部实现,外面一行都不用动。"

"哦!就像……"小陈想了想,"就像我把钱交给柜台,只管'存钱取钱',不用管保险柜里怎么摆。"

"对,就是这个意思。"


三、第五层:代码复用进阶

3.1 继承:IS-A 关系

"现在要上真正的硬菜了。"老周把三种商品摆在一起,"书、杂志、文创——它们都是商品。商品的共同点:都有名字、价格、库存,都能打折、补货、卖出。那为什么不先写一个通用的 Product,再让它们继承它?"

class Product:
    def __init__(self, name, price, stock):
        self.__name = name
        self.__price = price
        self.__stock = stock

    def sell(self, n):
        if n > self.__stock:
            raise ValueError("库存不足!")
        self.__stock -= n

    def describe(self):
        return f"商品:{self.__name}"

# ---------- 以下是子类 ----------

class Book(Product):                      # Book 继承 Product
    def __init__(self, name, author, price, stock):
        super().__init__(name, price, stock)   # 先让父类把通用部分初始化好
        self.author = author

    def describe(self):                   # 覆盖父类方法(Override)
        return f"图书《{self.name}》作者:{self.author}"

class Magazine(Product):
    def __init__(self, name, issue_no, price, stock):
        super().__init__(name, price, stock)
        self.issue_no = issue_no

class Merch(Product):
    def __init__(self, name, material, price, stock):
        super().__init__(name, price, stock)
        self.material = material

"继承(inheritance)解决的核心是 IS-A 关系:猫是动物,杂志是商品。Book 自动获得了 Productsellrestock——不用复制粘贴,通用逻辑就复用上了。子类只需要写自己特有的部分。"

"派生出的概念:单继承(一个子类一个父类,Python/Java 默认)、多继承(C++/Python 支持多个父类,容易出'菱形问题')、接口(interface)(只规定'必须会什么',不规定'怎么实现'——Java 里常用)。"

3.2 多态:同一接口,不同实现

"现在写盘账程序。要把店里所有商品挨个打印一行描述。试试用父类的写法统一处理:"

inventory = [
    Book("三体", "刘慈欣", 29.9, 120),
    Magazine("读者", 2026, 12.0, 300),
    Merch("帆布袋", "帆布", 39.9, 50),
]

for item in inventory:          # 全都是 Product 类型……
    print(item.describe())      # ……但各自输出自己的版本

输出:

图书《三体》作者:刘慈欣
商品:读者
商品:帆布袋

"看明白了吗?循环里只写了一份 item.describe(),没有任何 if-else 判断'这是书还是杂志',但每个对象输出了自己的描述。 这就是多态(polymorphism):同一个接口(describe()),不同的实现(书、杂志、文创各说各话)。"

"引用老段子:动物.叫() → 猫喵 / 狗汪。你不需要问'你是什么动物',直接叫它叫,它自然会用正确的方式叫。"

小陈感叹:"这要是还用第二章那套散装结构体 + if-else……我得写一长串 if type == "book": ... elif type == "magazine": ...,加一个新品类就得改这段代码。"

"对!多态的意义就是:加新品类,旧代码不用动。 这就是所谓的'开闭原则'——对扩展开放,对修改关闭。"

3.3 泛型 / 模板:写一套,服务所有类型

"最后一个:泛型(generics)/ 模板(template)。"老周说,"王姐要一个'库存排行'功能:对任意一种商品列表,按库存排序取前 N 名。"

"你要是给 Book 写一个排序函数,给 Magazine 再写一个,给 Merch 再写一个——又复制粘贴了。但排序的逻辑明明跟'是什么商品'无关,跟'库存字段'有关。能不能写一套,通吃?"

from typing import TypeVar

T = TypeVar("T")                 # 类型参数:占个位

def top_n(items: list[T], key, n: int) -> list[T]:
    """对任意类型 T 的商品列表,按 key 排序取前 n。"""
    return sorted(items, key=key, reverse=True)[:n]

# 用法:传什么类型都行
top_books = top_n(book_list, key=lambda p: p.stock, n=10)
top_merch = top_n(merch_list, key=lambda p: p.stock, n=5)

"泛型就是:写一套逻辑,支持多种类型。衍生概念:类型参数(T 是占位符)、约束(限定 T 必须是 Product 的子类)、特化(对特定类型给特殊实现)。Java/C#/Rust 都有,C++ 里叫模板(template)。"

"注意泛型和多态的区别:多态是'运行时'同一个接口不同实现;泛型是'编译时'一套代码适配多种类型。一个靠'对象自己会做',一个靠'编译器帮我们展开'。"


四、章末:老周的"第四、五层"总结

白板上又多了两层:

第四层:抽象与封装(隐藏细节,暴露接口)
纯文本
结构体
├── 模块 / 命名空间 → 把相关代码分组,避免名字冲突
└── 类(class)
    ├── 数据(字段)+ 操作(方法)打包
    ├── 访问控制(public/private/protected)→ 防外部乱改
    ├── 构造方法 → 对象出生即合法
    └── 析构方法 → 对象死亡前清理
第五层:代码复用进阶(不复制代码也能支持多种类型)
纯文本
类
├── 继承 → IS-A 关系,复用通用逻辑(单/多继承、接口)
├── 多态 → 同一接口,不同实现(加新品类不改旧代码)
└── 泛型 / 模板 → 一套逻辑支持多种类型(类型参数、约束、特化)

老周指着"析构方法"四个字:"这一章我没细讲析构方法(destructor)——对象死亡前要做的清理。因为在云间书店现在的 Python 代码里,它显得没那么重要。但你要是回到 C 语言的世界,'对象死了谁负责释放内存',那可是要命的哲学问题。这个坑,咱们下一章专门跳。"

小陈眼睛一亮:"内存管理!我听说 C 语言写程序内存泄漏是常态……"

"何止常态,我年轻时在上一家公司,就因为这个出过大事。"老周眼神一暗,"那年双十一,我们的服务器在凌晨三点全部宕机——就是内存泄漏。来,泡杯茶,我给你讲讲那年的事。"

✌ 语言