9.1 类与对象
恭喜你走到了本教程最重要的一章。**面向对象(Object-Oriented Programming,OOP)**是几乎所有现代编程语言的核心组织方式——你以后要学的 Web 框架、爬虫库、游戏引擎,翻开源码全是类。这一章我们不背概念,从一个你用现有知识写着写着就会"失控"的例子开始。
先感受痛点:不用类,代码会变成什么样
假设要管理游戏里的多只宠物猫,每只猫有名字、年龄,会叫、会自我介绍。用学过的字典 + 函数来写:
cat1 = {"name": "咪咪", "age": 3}
cat2 = {"name": "大橘", "age": 5}
def meow(cat):
print(f"{cat['name']}:喵喵喵~")
def introduce(cat):
print(f"我叫{cat['name']},今年{cat['age']}岁")
meow(cat1)
introduce(cat2)
能跑,但毛病不少:
- 数据和功能是散开的。字典在一边,函数在另一边,谁属于谁全靠脑子记。项目一大,"处理猫的函数"和"处理订单的函数"混在一起,找都找不到。
- 字典防不住手滑。写成
{"nmae": "咪咪"}也不报错,等到cat['name']时才 KeyError;往猫字典里塞个"price": 998也没人拦着。 - 每建一只猫都要手写一遍完整字典,忘一个键,后面全崩。
我们想要的是:把"一只猫有什么数据、会做什么事"定义成一个模板,之后照着模板批量造猫。这个模板就是类(class),照模板造出来的每一只具体的猫就是对象(object)。
💡 类和对象的关系,就像图纸和实物:一张"猫"的图纸(类),能造出咪咪、大橘无数只具体的猫(对象)。图纸只有一张,实物各有各的数据。
其实你早就在跟对象打交道了:"abc" 是 str 类的对象,[1, 2] 是 list 类的对象,"abc".upper()、[1, 2].append(3) 都是在"调用对象的方法"。这一章只是让你从"用别人的类"升级到"造自己的类"。
定义一个类
把上面的猫改写成类:
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def meow(self):
print(f"{self.name}:喵喵喵~")
def introduce(self):
print(f"我叫{self.name},今年{self.age}岁")
新面孔很多,逐行拆:
class Cat: ← class + 类名 + 冒号,类名习惯大写开头(叫"大驼峰")
def __init__(self, name, age): ← 初始化方法:造对象时自动执行
self.name = name ← 把传进来的 name 存到"这只猫自己身上"
self.age = age
def meow(self): ← 类里定义的函数叫"方法",第一个参数必须是 self
print(f"{self.name}:喵喵喵~")
三个关键概念:
__init__:名字前后各两条下划线(读作 dunder init)。它是"出生仪式"——每造一个对象,Python 自动执行它一次,负责给新对象装上初始数据。- 属性:挂在对象身上的变量,如
self.name、self.age。每个对象的属性互相独立。 - 方法:挂在类里的函数,如
meow()。和普通函数唯一的区别:第一个参数必须是self。
self 到底是什么
self 是新手最大的心理关卡,其实一句话就能说透:self 就是"这只猫自己"。
def introduce(self):
print(f"我叫{self.name}")
同一个 introduce 方法,咪咪调用时 self 就是咪咪,大橘调用时 self 就是大橘。方法是全体猫共用的(图纸只有一张),靠 self 才知道"这次是谁在自我介绍"。
再对比这两行,体会 self 的作用:
def __init__(self, name, age):
self.name = name # self.name 是存到对象身上的属性,方法结束后一直在
# name 只是参数,是局部变量,方法一结束就销毁(5.3 学过)
不写 self.name = name 这一步,传进来的名字就丢了。
创建和使用对象
cat1 = Cat("咪咪", 3)
cat2 = Cat("大橘", 5)
print(cat1.name) # 咪咪
print(cat2.name) # 大橘 —— 两只猫数据互相独立
cat1.meow() # 咪咪:喵喵喵~
cat2.introduce() # 我叫大橘,今年5岁
cat1.age = 4 # 修改属性:直接赋值
print(cat1.age) # 4(大橘的 age 不受影响)
Cat("咪咪", 3) 这一行背后发生了三步,务必在脑子里过一遍:
- Python 造出一个空白的新对象
- 自动调用
__init__(self, "咪咪", 3)——self 就是这个新对象,你传的参数从第二个位置起排 __init__跑完,把装好数据的对象交给cat1
⚠️ 高频坑 1:创建对象时参数数量对不上。
cat = Cat("咪咪")# TypeError: __init__() missing 1 required positional argument: 'age'
__init__要几个参数(self 除外),创建时就得传几个——和 5.2 学的函数参数规则完全一样,默认参数也照常可用:def __init__(self, name, age=1)。
⚠️ 高频坑 2:定义方法忘写 self。
class Dog:def bark(): # ❌ 没写 selfprint("汪汪")d = Dog()d.bark()# TypeError: bark() takes 0 positional arguments but 1 was given这个报错相当迷惑:"我明明没传参数,哪来的 1 个?"——那 1 个就是 Python 自动传的对象自己。记住两条规矩:定义时第一个参数写 self,调用时不用传 self。
⚠️ 高频坑 3:属性名拼错,不报错但埋雷。
cat1.nmae = "汤圆" # 手滑拼错,Python 不报错!print(cat1.name) # 咪咪 —— 根本没改到给不存在的属性赋值,Python 会默默新建一个属性挂上去。改名没生效、还多了个垃圾属性
nmae。而读取不存在的属性才会报AttributeError: 'Cat' object has no attribute 'nmae'。赋值改属性后打印验证一下,是个好习惯。
让 print(对象) 说人话:__str__
直接打印对象,输出会让你一愣:
print(cat1) # <__main__.Cat object at 0x000001A2B3C4D5E0>
这串是"Cat 对象 + 内存地址",对人类毫无意义。给类加一个 __str__ 方法,告诉 Python "打印我时显示什么":
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Cat(名字={self.name}, 年龄={self.age})"
cat1 = Cat("咪咪", 3)
print(cat1) # Cat(名字=咪咪, 年龄=3)
注意 __str__ 必须 return 一个字符串,不是 print。像 __init__、__str__ 这类前后双下划线的方法叫魔法方法——你不直接调用它们,Python 在特定时机(创建时、打印时)自动调用。
完整实战:银行账户
把本节所有知识 + 前几章的异常处理串成一个有真实感的类:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def __str__(self):
return f"{self.owner} 的账户,余额 {self.balance} 元"
def deposit(self, amount):
"""存钱"""
if amount <= 0:
raise ValueError(f"存款金额必须大于 0,收到 {amount}") # 8.1 学的:尽早报错
self.balance += amount
print(f"存入 {amount} 元,余额 {self.balance} 元")
def withdraw(self, amount):
"""取钱"""
if amount > self.balance:
print("余额不足!")
return False
self.balance -= amount
print(f"取出 {amount} 元,余额 {self.balance} 元")
return True
account = BankAccount("小明", 100)
account.deposit(50) # 存入 50 元,余额 150 元
account.withdraw(200) # 余额不足!
account.withdraw(30) # 取出 30 元,余额 120 元
print(account) # 小明 的账户,余额 120 元
体会面向对象的核心好处:数据(balance)和操作数据的规则(怎么存、怎么取、什么算非法)打包在一起。外部只管调用 deposit(),"金额必须为正"这条规矩由类内部守着——不像字典方案,任何代码都能 cat1["balance"] = -999 乱改。这种"把细节包起来,只暴露安全接口"的思想有个名字:封装。
什么时候该写类
新手常见误区是学完类就啥都想套个类。经验法则:
- 一次性脚本、纯流程计算 → 函数就够了,硬写类是自找麻烦
- 出现了"一类东西"——既有数据又有围绕数据的行为,还要创建多个 → 用类(游戏里的多个角色、商城里的多个订单、题库里的多道题)
- 判断口诀:能自然说出"每个 XX 都有 __ 和 __,会 __",就适合建类
本节报错速查表
| 报错 | 人话翻译 |
|---|---|
TypeError: __init__() missing 1 required positional argument: 'age' | 创建对象时少传了参数 |
TypeError: bark() takes 0 positional arguments but 1 was given | 定义方法时忘写 self |
AttributeError: 'Cat' object has no attribute 'nmae' | 读取了不存在的属性(八成是拼错了) |
打印出 <__main__.Cat object at 0x...> | 不是报错,是没定义 __str__ |
练习
新建 practice91.py 完成:
1. 动手:定义 Student 类:属性姓名、成绩;方法 show() 打印"xx 的成绩是 xx 分",方法 is_pass() 成绩 ≥ 60 返回 True。创建两个学生验证。
2. 猜输出:先别运行,猜猜打印什么?
class Counter:
def __init__(self):
self.count = 0
def add(self):
self.count += 1
a = Counter()
b = Counter()
a.add()
a.add()
b.add()
print(a.count, b.count)
3. 修 bug:下面的代码报 AttributeError: 'Phone' object has no attribute 'brand',找出两处错误:
class Phone:
def __init__(self, brand, price):
brand = brand
self.price = price
def show():
print(f"{self.brand},售价 {self.price} 元")
p = Phone("华为", 5999)
p.show()
4. 挑战:写一个 Book 类:属性书名、总页数、当前读到的页数(初始 0);方法 read(pages) 往后读若干页(不能超过总页数,超过就停在最后一页);progress() 打印"《xx》已读 xx%"。定义 __str__。
点击查看答案
# 1
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def show(self):
print(f"{self.name} 的成绩是 {self.score} 分")
def is_pass(self):
return self.score >= 60
s1 = Student("小明", 90)
s2 = Student("小刚", 55)
s1.show() # 小明 的成绩是 90 分
print(s2.is_pass()) # False
2. 输出 2 1。a 和 b 是两个独立对象,各自有各自的 count:a 加了两次,b 加了一次,互不影响——这正是"每个对象的数据独立"。
3. 两处 bug:
class Phone:
def __init__(self, brand, price):
self.brand = brand # ① 原来写 brand = brand,只是参数自赋值,没存到对象上
self.price = price
def show(self): # ② 忘写 self
print(f"{self.brand},售价 {self.price} 元")
brand = brand 不报错但白干——方法一结束局部变量就没了,所以后面读 self.brand 才报 AttributeError。
# 4
class Book:
def __init__(self, title, total_pages):
self.title = title
self.total_pages = total_pages
self.current = 0
def __str__(self):
return f"《{self.title}》共 {self.total_pages} 页,当前第 {self.current} 页"
def read(self, pages):
self.current += pages
if self.current > self.total_pages:
self.current = self.total_pages
print("已经读完啦!")
def progress(self):
percent = self.current / self.total_pages * 100
print(f"《{self.title}》已读 {percent:.1f}%")
b = Book("小王子", 200)
b.read(50)
b.progress() # 《小王子》已读 25.0%
b.read(300) # 已经读完啦!
print(b) # 《小王子》共 200 页,当前第 200 页
本节小结
- 类是图纸,对象是实物;一个类能创建无数个数据互相独立的对象
__init__是出生仪式,创建对象时自动执行;self.xxx = ...才能把数据存到对象身上self就是"对象自己":定义方法时写,调用时不传- 给不存在的属性赋值不报错(默默新建),读取才报 AttributeError——拼错属性名是隐形坑
__str__定义对象的打印样式,必须 return 字符串- 类 = 数据 + 行为的打包(封装);不是所有代码都需要类,"一类东西 + 多个实例"才用
下一节:9.2 继承