一、王姐又来了:我们要卖杂志和文创
第三章开工第一天,王姐笑盈盈地走进仓库:"周哥啊,光卖书太单薄了。我打算加两个品类——杂志,还有文创(帆布袋、马克杯、书签)。你给系统加上呗?"
小陈打开代码,看着第二章的结构体陷入了沉思:
Book = namedtuple("Book", ["title", "author", "price", "stock"])
# 杂志呢?有刊号、有期数、还有定价
Magazine = namedtuple("Magazine", ["title", "issue_no", "price", "stock"])
# 文创呢?没有作者,有材质、有款式
Merch = namedtuple("Merch", ["name", "material", "style", "price", "stock"])
"师傅,三个结构体,字段还长得差不多……更麻烦的是,之前写的那些函数——apply_discount、restock、count_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.Product 和 pricing.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(公开):谁都能用(
restock、get_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 自动获得了 Product 的 sell、restock——不用复制粘贴,通用逻辑就复用上了。子类只需要写自己特有的部分。"
"派生出的概念:单继承(一个子类一个父类,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 语言写程序内存泄漏是常态……"
"何止常态,我年轻时在上一家公司,就因为这个出过大事。"老周眼神一暗,"那年双十一,我们的服务器在凌晨三点全部宕机——就是内存泄漏。来,泡杯茶,我给你讲讲那年的事。"