🐸 青蛙编程网
首页 / Python / 面向对象 / 5. 综合练习

综合练习:宠物管理系统

这是什么:一个宠物管理系统小项目,把类与实例、属性方法、继承多态、魔术方法全部用上。

为什么用:前四章都是单项技能,各练各的。真做一个能"开业营业"的宠物店,数据、能力、继承关系必须同时配合,才算真正学明白面向对象。

如何用:本章从需求拆解入手:定好宠物的公共骨架,再让狗猫继承它,最后用列表把宠物店管起来。

需求拆解

  • 宠物有公共骨架:名字、年龄、自我介绍(__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__ 两个魔术方法
  • 记忆锚点:管理系统就像宠物店——类是图纸、对象是宠物、列表是名册,方法就是店员手里那套流程。

动手改

  1. 新增 Hamster(Pet):species 设为"仓鼠",speak 打印"吱吱!",收进店里
  2. 给 PetShop 加 eldest() 方法,返回年龄最大的宠物(提示:循环比较 pet.age)
  3. 给 PetShop 加 find(name) 方法:按名字查找,找到就打印它的介绍,没找到打印"查无此宠"

面向对象毕业 🎉 下一站 Flask,让你的代码跑在网页上

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。