昨天写的类,有个隐患你注意到没有:
s.score = 60这种写法,外面的代码想怎么改就怎么改,改成负数、改成字符串都没人拦。对象是你造出来的,数据却像摆在路边没人看管。今天解决这件事:封装,把数据锁进对象里,只留几个"窗口"让人操作。顺便再学第二个设计:继承——先画一张通用的图纸,再画新的类时直接说"它也是这种",旧图上的东西就全有了,不用重画。两个概念都从生活例子讲起,不绕。
昨天留了个隐患:数据随便被人改
昨天的代码里,s.score 想改就改:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s = Student("小明", 90)
s.score = -100 # 外面直接改,没人拦
能跑,但很危险。想想银行账户:如果外面的人能随手 acc.balance = -100,余额变负数都没人拦。但如果只能通过 deposit()(存钱)、withdraw()(取钱)这两个"窗口"操作,窗口里就可以设规矩——金额必须大于 0、余额不足不能取。数据就安全了。
这就是封装(encapsulation):把"数据"和"操作这些数据的方法"打包在同一个类里,外部只能通过方法间接操作数据,不能直接乱动。
封装:给对象装上玻璃窗
封装的比喻,最好用的是银行柜台的玻璃窗:你的钱(数据)放在里面,外面的人不能伸手乱拿,只能通过柜台窗口(方法)办业务,取多少、存多少都要过一遍规矩。
Python 里的"锁"分三层:
| 写法 | 含义 | 能访问吗 |
|---|---|---|
self.balance |
公开属性,外面随便改 | 能,但不建议 |
self._balance |
约定"内部属性,别乱动"(下划线是提醒) | 能,但约定不允许 |
self.__balance |
真正的私有(Python 会做名字改写,外面访问会报错) | 不能直接访问 |
本周重点用第二种:_ 开头的属性是"君子协定",告诉读代码的人"这是我的内部数据,请走方法操作"。听懂会用即可,第三种先知道有这回事。
看一个带"窗口规矩"的账户类:
class Account:
def __init__(self):
self._balance = 0 # _ 开头:内部属性,别乱动
def deposit(self, money): # 存钱窗口
if money > 0:
self._balance += money
else:
print("金额必须大于 0")
def show(self): # 查看余额窗口
print(f"余额:{self._balance}")
acc = Account()
acc.deposit(100) # 存 100
acc.deposit(-5) # 金额必须大于 0 ← 窗口拦住了非法操作
acc.show() # 余额:100
输出长这样:
金额必须大于 0
余额:100
注意 deposit(-5) 被拦下来了,报了个"金额必须大于 0"。判断条件写在方法里,而不是靠外面的人自觉——这就是封装起作用的地方。
三个容易误解的点:
_balance只是"约定",Python 并不真正禁止acc._balance = -100。但你要养成习惯:内部数据一律加_,并且只在方法里改。- 封装不是"给每个属性都写个方法"。只给需要规则的属性开窗口,
name这种随便读的属性没必要包一层。 - 方法名用动词(deposit 存钱、withdraw 取钱、show 显示),读代码的人不用看实现就知道能干什么——封装顺便带来了"接口清晰"。
为什么要费这个事
封装不是让代码变复杂,它换回的是三件实在的东西:
- 安全:数据不能随便被改成非法值(余额不为负、年龄不为负数)。
- 好维护:改数据规则时只改方法里那一处,不用到处找"谁在改这个属性"。以后想加"存钱要手续费",只改
deposit一处就完事。 - 好分工:使用者只关心"窗口"(方法)怎么用,不关心内部实现。就像你用遥控器,不用懂电视机内部电路。
没封装有多乱,对比一下就知道了:
# 没有封装:谁都能乱改,规则散落各处
class Account:
def __init__(self):
self.balance = 0
acc = Account()
acc.balance = -100 # 直接改成负数,没人拦
acc.balance = "一百块" # 甚至改成字符串,后面一加钱直接崩溃
# 有封装:规则只在 deposit / withdraw 里
class Account:
def __init__(self):
self._balance = 0
def deposit(self, money):
if money > 0:
self._balance += money
else:
print("金额必须大于 0")
别把封装理解成"物理锁死"。本周的 _ 版本任何人都能访问,封装的意义在于引导大家走方法。练习里会让你故意直接改 acc._balance = -100 看看——目的是让你直观感受"能改 vs 应该改"的区别,那只是演示,不是推荐写法。
继承:子类白捡父类的能力
继承(inheritance) 用一句大白话说:新写一个类时,让它"拥有"另一个类的全部属性和方法,再补充自己的新东西。
- 父类(parent class):被继承的类,也叫基类。
- 子类(child class):继承别人的类,也叫派生类。子类是"父类的升级版",父类有的它全有,还额外多出自己的。
- 写法:
class 子类(父类):——括号里写父类名,就完成了"认亲"。
生活里的例子:Animal(动物)是所有动物的"通用图纸",会吃、会跑。Cat(猫)是"动物 + 会喵喵叫"。写 Cat 的时候,不用把 eat 重新抄一遍,直接继承 Animal 就有了:
class Animal: # 父类:通用图纸
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 在吃东西")
class Cat(Animal): # 子类:继承 Animal
def meow(self): # 子类自己的新方法
print(f"{self.name}:喵~")
c = Cat("咪咪") # 用父类的 __init__ 装名字
c.eat() # 咪咪 在吃东西 ← 继承来的
c.meow() # 咪咪:喵~ ← 自己新增的
输出长这样:
咪咪 在吃东西
咪咪:喵~
注意一个细节:Cat("咪咪") 创建对象时,Cat 没写自己的 __init__,会自动使用父类 Animal 的 __init__——名字照样装得上去。这是继承的默认行为:子类没定义的方法,去父类找。
继承的方向最容易搞反:
- 子类对象能调用父类的方法(
c.eat()没问题),但父类对象不能调用子类的方法(Animal("x").meow()会报 AttributeError)。能力是往下传的,不是往上冒的。 - 子类不写
__init__,自动用父类的;子类自己写了__init__又想保留父类的属性,就得用super()(下一节),把父类的初始化也跑一遍。 - 本周只学单继承(一个子类一个父类);继承还能嵌套:
class 金毛(狗)、class 狗(动物),一层层往上找方法。
super():叫一声"父类,来帮忙"
super() 是用来"调用父类方法"的钥匙。最常见的场景:子类重写了 __init__,但不想把父类那套初始化(比如装 name)重抄一遍,就调 super().__init__(...) 让父类先干它那份活。
什么时候必须用它?子类有自己的 __init__,并且希望保留父类装好的属性时。例子:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, age):
super().__init__(name) # 让父类装 name(不用自己再写一遍 self.name = name)
self.age = age # 子类新增的属性自己装
d = Dog("旺财", 3)
print(d.name) # 旺财 ← 父类装的
print(d.age) # 3 ← 子类装的
输出长这样:
旺财
3
这里 super().__init__(name) 等价于把父类 __init__ 里的 self.name = name 执行了一遍。今天的要求:看得懂、会照着写就行,原理不用深究。
对比一下就明白它为什么必要:如果 Dog 的 __init__ 里不写 super().__init__(name),只写 self.age = age,那 d.name 会报 AttributeError: 'Dog' object has no attribute 'name'——父类初始化被你的 __init__"覆盖"掉了,没机会执行。这是继承里最容易踩的坑。
写的时候注意三点:
- 子类写了
__init__就会覆盖父类的__init__,父类不再自动执行。需要父类属性时,第一行就写super().__init__(...)。 super().__init__(...)传的参数要跟父类__init__对齐(Animal.__init__(self, name)→ 只传 name,self 不用传)。super()不只用在__init__里,任何方法里都能用它调父类的同名方法。本周用到最多的还是__init__。
方法重写:同名的,各干各的
方法重写(override):子类定义了和父类同名的方法,调用时子类对象用的就是子类自己的版本,父类那版被"换掉"了。
动物都会"叫",但狗叫"汪汪"、猫叫"喵"。于是父类写一个通用的 speak,子类各自重写:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} 发出声音")
class Dog(Animal):
def speak(self): # 重写父类的方法
print(f"{self.name}:汪汪!")
class Cat(Animal):
def speak(self): # 也重写
print(f"{self.name}:喵~")
d = Dog("旺财")
c = Cat("咪咪")
d.speak() # 旺财:汪汪! ← 用 Dog 自己的版本
c.speak() # 咪咪:喵~ ← 用 Cat 自己的版本
输出长这样:
旺财:汪汪!
咪咪:喵~
同一个方法名,不同对象干不同的事。三个要点:
- 重写只影响"调用时用哪份代码",父类自己的方法不受影响——
Animal("x").speak()还是打印「发出声音」。 - 重写后还想用父类版本的逻辑,方法里可以
super().speak()先调父类的,再补自己的内容。 - 重写方法的参数最好和父类保持一致(都叫 speak,参数个数一样),否则调用时容易传错参数。
随堂练习
练习一:体会封装(先做这个,感受最深)
- 抄写并运行上面的 Account 类,确认
deposit(-5)被拦住。 - 然后故意在文件末尾加一行
acc._balance = -100,再运行acc.show(),观察输出——思考:代码能跑吗?这说明了 Python 封装的什么特点?
练习二:给 Account 加"取钱"窗口
给 Account 加一个 withdraw(money) 取钱方法,规则:
- 金额必须大于 0,否则打印「金额必须大于 0」
- 余额不足时打印「余额不足」
- 都通过才扣钱
提示:withdraw 的结构和 deposit 对称,但要多一个 if money > self._balance: 的判断。写完后测试:存 100 → 取 30(成功)→ 取 200(余额不足)→ 取 -5(被拦)。
进阶题 ★:继承练习——会飞的鸟
- 建一个
Bird(Animal)子类(父类用上面的 Animal),加一个fly()方法打印「XX 在飞」。 - 创建鸟对象,分别调用
eat()(继承来的)和fly()(自己的),确认都能用。 - 再挑战:给 Bird 写自己的
__init__(加一个wings翅膀数属性),用super().__init__(name)保留父类的名字属性,最后打印「XX 有 X 只翅膀」。
让 AI 给你讲封装和继承
两个概念要是还糊着,或者想让 AI 检查你写的类,用这段:
我是 Python 零基础初学者。请用大白话和生活中的比喻,给我讲清楚:
1. 什么是封装?为什么要用 _ 开头表示内部属性?
2. 什么是继承?super() 是干什么的?
每个概念给我一个不超过 15 行的完整代码例子,并逐行注释。最后告诉我新手最容易犯的 3 个错误。
报错速查
| 报错信息 | 什么意思 | 怎么办 |
|---|---|---|
AttributeError: 'Dog' object has no attribute 'name' |
子类写了 __init__ 但没调 super().__init__(),父类没来得及装属性 |
子类 __init__ 第一行写上 super().__init__(父类需要的参数) |
TypeError: __init__() missing 1 required positional argument: 'name' |
调用 super().__init__(...) 时少传了父类需要的参数 |
数一下父类 __init__(self, ...) 的参数,逐个传齐 |
AttributeError: 'Animal' object has no attribute 'meow' |
用父类对象调用子类独有的方法 | 记住继承方向:父类没有子类的新方法,只能用子类对象调 |
TypeError: speak() takes 1 positional argument but 2 were given |
某个方法定义时漏了 self(常见于重写时照抄漏掉) |
所有方法第一个参数永远是 self |
没有报错,但 acc._balance 变成了 -100 |
你直接改内部属性,Python 默认不拦 | 记住约定:_ 开头的属性只在方法里通过 self 修改 |
明后两天的主角变了
今天两件事:封装把数据锁进对象,只留方法当窗口,_ 开头是"内部数据"的约定;继承让新类白捡旧类的能力,class Cat(Animal): 一写,认亲完成,子类没定义的方法自动去父类找,想保留父类的初始化就用 super().__init__(...)。
这两天学的类,明天就要派上用场了——不过明天的主角不是代码,而是怎么把需求说给 AI 听。明后天学"给 AI 提需求"的两板斧:四要素和报错三步法,这是比任何语法都值钱的技能,也是周五图书管理项目的吃饭手艺。今天要是还有哪段代码没敲熟,今晚补一补,明天用得上。