综合练习:宠物管理系统
这是什么:一个宠物管理系统小项目,把类与实例、属性方法、继承多态、魔术方法全部用上。
为什么用:前四章都是单项技能,各练各的。真做一个能"开业营业"的宠物店,数据、能力、继承关系必须同时配合,才算真正学明白面向对象。
如何用:本章从需求拆解入手:定好宠物的公共骨架,再让狗猫继承它,最后用列表把宠物店管起来。
需求拆解
- 宠物有公共骨架:名字、年龄、自我介绍(
__str__) - 狗、猫继承宠物,叫声各不相同(继承 + 方法重写)
- 宠物店用列表管理宠物:能收宠、能点名、能报数量(
__len__)
完整代码
class Pet:
species = "宠物" # 类属性,等着被子类覆盖
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
print(f"{self.name}发出了声音")
def __str__(self):
return f"{self.species}「{self.name}」,{self.age}岁"
class Dog(Pet):
species = "小狗" # 覆盖类属性
def speak(self): # 重写
print(f"{self.name}:汪汪!")
class Cat(Pet):
species = "小猫"
def speak(self):
print(f"{self.name}:喵~")
class PetShop:
def __init__(self, shop_name):
self.shop_name = shop_name
self.pets = [] # 列表管理宠物
def admit(self, pet):
self.pets.append(pet)
print(f"欢迎新成员:{pet}") # 触发 __str__
def roll_call(self):
print(f"—— {self.shop_name} 点名 ——")
for pet in self.pets:
pet.speak() # 多态:各叫各的
def __len__(self):
return len(self.pets)
shop = PetShop("青蛙宠物店")
shop.admit(Dog("旺财", 3))
shop.admit(Cat("咪咪", 1))
shop.admit(Dog("小黑", 2))
shop.roll_call()
print(f"店内宠物共 {len(shop)} 只")
运行输出:
欢迎新成员:小狗「旺财」,3岁
欢迎新成员:小猫「咪咪」,1岁
欢迎新成员:小狗「小黑」,2岁
—— 青蛙宠物店 点名 ——
旺财:汪汪!
咪咪:喵~
小黑:汪汪!
店内宠物共 3 只
知识点全对上
| 代码 | 用到了哪一章 |
|---|---|
class Dog(Pet) |
继承:子类白拿父类的 __init__ 和 speak 默认版 |
species = "小狗" |
类属性:同一名,各家值不同 |
def speak(self) 三连 |
方法重写 + 多态:循环里各叫各的 |
def __str__(self) |
魔术方法:print(pet) 显示人话 |
self.pets = [] 与 append |
列表管理一堆对象 |
def __len__(self) |
len(shop) 直接报数量 |
❓ 常见问题
Q1:报 AttributeError: 'PetShop' object has no attribute 'pets'?
原因:__init__ 里忘了初始化 self.pets = [],名册压根没建,admit 一调 append 就扑空。属性必须先"登记"再使用。
修复三步:① 打开 PetShop 的 __init__;② 补上 self.pets = [];③ 确认 admit、roll_call、__len__ 里用的都是 self.pets。
举一反三:容器型对象(购物车、歌单、名册)的第一行永远是"备好空篮子"——new 对象先初始化容器。
Q2:报 AttributeError: 'list' object has no attribute 'speak'?
原因:把列表当宠物使唤了——写了 self.pets.speak()。pets 是装宠物的篮子,篮子本身不会叫,得挨个取出里面的宠物。
修复三步:① 定位报错行,看是不是直接在 pets 上调了方法;② 套上 for 循环:for pet in self.pets:;③ 循环体内调用 pet.speak()。
举一反三:凡见 AttributeError: 'list' object has no attribute 'xxx',都是"忘了循环"——先 for 再逐个处理。
Q3:报 TypeError: object of type 'PetShop' has no len()?
原因:PetShop 没定义 __len__ 魔术方法,len(shop) 找不到"数个数"的机关,只能报错。
修复三步:① 在 PetShop 里加 def __len__(self):;② 返回 len(self.pets);③ 之后 len(shop) 直接报店内数量。
举一反三:想让自定义对象支持某个内置函数(len、print、比较),就去定义对应的魔术方法——缺啥补啥。
Q4:报 NameError: name 'Dog' is not defined?
原因:用到 Dog 时它还没被定义——Python 从上往下逐行执行,类定义那几行还没轮到,名字自然不存在;也可能是类名拼错了。
修复三步:① 检查报错行里的类名拼写;② 确认 class Dog: 写在使用之前;③ 大文件里把类定义集中放前面,主程序放最后。
举一反三:"先定义、后使用"是 Python 文件的铁律,报 NameError 先看定义和使用谁在前。
Q5:find("旺财") 明明找到了,"查无此宠"也跟着打出来了?
原因:循环里找到之后忘了 return(或 break),代码继续把剩下的循环跑完,把不该出现的提示也带了出来;另外查找关键字前后多了空格,"旺财 " 和 "旺财" 并不相等,会白白错过。
修复三步:① 找到目标后立刻 return,别让循环继续跑;② 比较前用 .strip() 去掉多余空格更保险;③ 用"存在的名字"和"不存在的名字"各测一遍。
举一反三:写查找类功能,正例、反例都要测——只测找得到的,"查无此人"分支的 bug 永远藏得住。
⚠️ 新手翻车现场
翻车一:对着篮子喊
class PetShop:
def __init__(self):
self.pets = []
def roll_call(self):
print("—— 点名 ——")
self.pets.speak() # 对着篮子喊
shop = PetShop()
shop.roll_call()
AttributeError: 'list' object has no attribute 'speak'
逐行分析:错在第 7 行——pets 是"篮子",会叫的是篮子里的宠物。修正:for pet in self.pets: 套上循环,循环体内 pet.speak()。
翻车二:名册没建就 append
class PetShop:
def __init__(self, shop_name):
self.shop_name = shop_name # 只登记了店名
def admit(self, pet):
self.pets.append(pet) # 名册从没建立
shop = PetShop("青蛙宠物店")
shop.admit("旺财")
AttributeError: 'PetShop' object has no attribute 'pets'
逐行分析:错在第 6 行引爆,病根在第 2~3 行——__init__ 忘了 self.pets = [] 这句"备好空篮子",append 没有落脚点。补上初始化,名册才算开张。
翻车三:len 不认识你的店
class PetShop:
def __init__(self):
self.pets = []
shop = PetShop()
print(len(shop)) # 类里没有 __len__
TypeError: object of type 'PetShop' has no len()
逐行分析:报错在最后一行,但根子是类里压根没定义 __len__——len() 不认识这家店。加上 def __len__(self): return len(self.pets) 就通了。
📦 完整小示例
换皮实战:把"宠物店"套路套在图书馆上——图书类 + 管理类,收书、查书、报馆藏。
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"《{self.title}》({self.author} 著)"
class Library:
def __init__(self, name):
self.name = name
self.books = [] # 备好书架
def add(self, book):
self.books.append(book)
print(f"入库成功:{book}") # 触发 __str__
def find(self, keyword):
for book in self.books:
if keyword in book.title:
print(f"找到了:{book}")
return
print(f"查无此书:{keyword}")
def __len__(self):
return len(self.books)
lib = Library("青蛙图书馆")
lib.add(Book("小王子", "圣埃克苏佩里"))
lib.add(Book("活着", "余华"))
lib.add(Book("呱呱历险记", "佚名"))
print(f"馆藏 {len(lib)} 本")
lib.find("小王子")
lib.find("哈姆雷特")
你会看到:
入库成功:《小王子》(圣埃克苏佩里 著)
入库成功:《活着》(余华 著)
入库成功:《呱呱历险记》(佚名 著)
馆藏 3 本
找到了:《小王子》(圣埃克苏佩里 著)
查无此书:哈姆雷特
逐段中文精讲
- 第 1~6 行(图书图纸):Book 用
__init__登记书名和作者,__str__让每本书打印出来都是可读介绍——为后面"入库播报"铺路。 - 第 8~15 行(图书馆开张):
__init__里self.books = []先备好书架——容器初始化永远第一步;add 把书上架并播报,播报文字里直接放 book,自动触发__str__。 - 第 17~21 行(查找逻辑):循环逐本检查"关键字是否在书名里"(
in是基础篇学过的成员判断);找到就打印并立刻return,循环走完还没找到,才轮到"查无此书"。 - 第 23~24 行(魔术支撑):
__len__把书架交给 len 数数,len(lib)才有资格直接报馆藏量。 - 第 26~31 行(营业测试):三本入库、报馆藏、正反两个查找各来一遍——找得到和找不到都要测,两个分支都跑过才算验证完。
跑通后做个小实验:给 Library 加一个 remove(title) 方法:循环匹配到就 self.books.remove(book) 并打印"已借出",然后连借两本同名书,观察 len(lib) 的变化。
🎯 什么时候用它
- 学生选课/班级管理:Student 类管"一个学生",ClassRoom 类管"一群学生",收人、点名、数人数——和本章宠物店一个模子,换皮就能用。
- 仓库/库存管理:商品类存"一件货"的信息,仓库类管"一堆货"的进出,
__len__报库存,find 做查询,进销存的雏形。 - 记账小程序:Record 类存一笔账(金额、用途),AccountBook 类负责添加、按月统计——个人理财工具的核心就这几行。
📝 本章小结
- 写"管理系统"的固定套路:先设计小对象的类,再用容器类管理一堆小对象
- 容器型对象第一步永远是
self.xxx = []备好空篮子 - 对一堆对象做操作 = for 循环 + 逐个调用,别把列表当单个对象使
- 让 print、len 认识你的类,靠
__str__、__len__两个魔术方法 - 记忆锚点:管理系统就像宠物店——类是图纸、对象是宠物、列表是名册,方法就是店员手里那套流程。
动手改
- 新增
Hamster(Pet):species设为"仓鼠",speak打印"吱吱!",收进店里 - 给
PetShop加eldest()方法,返回年龄最大的宠物(提示:循环比较pet.age) - 给
PetShop加find(name)方法:按名字查找,找到就打印它的介绍,没找到打印"查无此宠"
面向对象毕业 🎉 下一站 Flask,让你的代码跑在网页上