PY-07 类与对象
07 · 类与对象(Classes & Objects)
📅 预计 100 分钟 | ⭐ = 高区分度考点
✍️ 本讲配套练习:本地python grader.py 07,或网页练习
引子:为什么要有"类"?
前面几讲是"面向过程"——数据(变量)和操作(函数)分开。系统一复杂(学生管理、银行、游戏)就乱套:stu1_name、stu1_score、stu2_name……变量越来越多,函数参数传来传去,改一处全局跟着改。
面向对象(OOP)把数据和操作它的函数打包:先做"模板"(类 class),再用模板批量造"实物"(对象 object / instance)。属性 + 方法封装在一个整体里,结构一下清晰了。
7.1 类就是图纸,对象就是造出来的实物 ⭐
盖房子打比方:类 = 图纸(规定了几间房、什么颜色,图纸本身不能住人);对象 = 按图纸盖的房子(能住人,同一张图纸能盖很多套,每套独立)。
class Dog:
"""狗:一张「狗」的图纸"""
def __init__(self, name): # 构造方法:造实例时自动执行
self.name = name # self.name 是实例属性(每只狗的名字)
def bark(self): # 实例方法:狗的行为
return f"{self.name}在叫"
d1 = Dog("旺财") # 按图纸造一只狗(旺财)
d2 = Dog("大黄") # 再造一只(大黄)
print(d1.bark()) # 旺财在叫
print(d2.name) # 大黄
- 类名用大驼峰 PascalCase(
BankAccount)是惯例;定义时不写括号,创建对象才写括号。 d1、d2是独立对象,改d1.name不影响d2.name。
⚠️ 常见错误
- 实例方法少写
self:def bark():调用d1.bark()报TypeError: ... takes 0 positional arguments but 1 was given。 - 创建实例忘加括号:
d = Dog得到"类本身"而非对象,调用方法报错。
7.2 init 与 self:对象出生的那一刻 ⭐
__init__ 叫构造方法(constructor),是对象出生时自动执行的"初始化程序",创建时传的参数原样传进来:
class Student:
def __init__(self, name, score):
self.name = name # 把参数"钉"到对象身上
self.score = score
s = Student("小明", 85) # 自动执行 __init__("小明", 85)
print(s.name, s.score) # 小明 85
self 是"当前正在操作的这个对象自己"——执行 s = Student("小明", 85) 时 self 就指 s。第一参数约定叫 self。类不需要初始化属性时可以省略 __init__。
⚠️ 常见错误
__init__里写return返回值:报TypeError: __init__() should return None。想返回值用普通方法。- 漏写
self:def __init__(name)→ 参数错位,直接报错。 - 属性没建好:忘了
self.score = score,一访问s.score就AttributeError。好习惯:所有属性一次性在__init__里建好。
7.3 属性:实例变量 vs 类变量 ⭐
- 实例变量:每造一个对象复制一份,各管各的,用
self.xxx定义。 - 类变量:写在类体里、所有实例共享一份。
class Dog:
species = "犬科" # 类变量:所有狗共享
def __init__(self, name):
self.name = name # 实例变量:每只狗独立
print(Dog.species) # 犬科(通过类名直接访问)
d = Dog("旺财")
print(d.species) # 犬科(实例也能读到类变量)
d.species = "猫科" # ⚠️ 只是在 d 上新建了一个实例变量
print(Dog.species) # 犬科(类变量一点没变)
最经典的坑:d.species = "猫科" 表面像"改了类变量",实际只是给 d 新建了一个同名实例变量,把类变量"遮住"了。
类变量最实用的场景是给所有实例记账——统计创建了多少个对象:
class Student:
total = 0 # 类变量:计数器
def __init__(self, name):
Student.total += 1 # 关键:用 类名.total,不是 self.total
self.name = name
s1 = Student("小明")
s2 = Student("小红")
print(Student.total) # 2
print(s1.name, s2.name) # 小明 小红
若误写 self.total += 1:每个实例把类变量当前值 +1 后赋给实例,于是计数器永远 1、1、1。改类变量必须用 类名.变量名。
⚠️ 常见错误
- 用
self.xxx改类变量:改不进去,只会新建同名实例变量。 - 可变默认值陷阱(高频):
def __init__(self, items=[])的默认列表在定义类时只创建一次,所有没传参的对象共用它:
class Bug:
def __init__(self, items=[]):
self.items = items
b1 = Bug(); b1.items.append(1)
b2 = Bug()
print(b2.items) # [1]?!共享了
class Fixed:
def __init__(self, items=None):
self.items = items if items is not None else [] # 正确写法
- 同名类变量和实例变量混用:一会儿
self.一会儿类名.,容易绕晕。选一种,全程一致。
7.4 方法:对象的"行为" ⭐
方法是"属于对象的函数",第一个参数永远是 self,调用时自动传入:
class Counter:
def __init__(self):
self.count = 0
def increment(self): # 行为:加一
self.count += 1
def value(self): # 行为:读出当前值
return self.count
c = Counter()
c.increment()
c.increment()
print(c.value()) # 2
调用方法要加括号:c.value 拿的是"方法本身",c.value() 才是执行。
⚠️ 常见错误
- 忘写括号:
print(c.value)输出<bound method ...>,不是数值。 - 访问属性忘了
self:方法里写count而不是self.count→NameError。对象身上存的都要self.xxx。 - 方法间互相调用忘写
self.:self.reset()别写成reset()。
7.5 魔法方法:让对象"像内置类型" ⭐
以双下划线开头结尾的方法叫魔法方法(magic method / dunder),不用手动调,Python 在特定时刻自动调用:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): # print(对象) / str(对象) 时调用 ⭐
return f"({self.x}, {self.y})"
def __repr__(self): # 交互式终端显示对象时调用
return f"Point({self.x}, {self.y})"
def __len__(self): # len(对象) 时调用,必须返回非负整数 ⭐
return 2
def __eq__(self, other): # == 比较时调用 ⭐
return self.x == other.x and self.y == other.y
def __add__(self, other): # + 运算时调用(运算符重载)
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2); p2 = Point(1, 2)
print(p1) # (1, 2) 调 __str__
print(p1 == p2) # True 调 __eq__
print(p1 + p2) # (2, 4) 调 __add__
__str__面向人(给用户看);__repr__面向开发者(理想是"能复现对象的代码")。列表打印内部元素用__repr__,所以print([p1])显示[Point(1, 2)]。两个都写,调试方便。__eq__:不定义时p1 == p2比的是"内存地址",内容一样也不相等;定义后按"值"比较。__len__:必须返回非负整数。运算符重载:__add__让+有了新含义。
⚠️ 常见错误
__str__不返回字符串:写return self.x(整数)→TypeError: __str__ returned non-string。__eq__忘了other参数:签名必须def __eq__(self, other),还要提防other是别的类型。__len__返回负数/浮点数:如return -1或return 2.0→ 报错。注意bool也是整数。- 只写
__str__不写__repr__:把对象放进列表再打印,还是"内存地址"。
7.6 封装:把"私有"藏起来 ⭐
封装(encapsulation):属性一般不让外部直接改,通过方法读写,这样能加校验、保持内部一致。Python 没有真正意义的私有,靠下划线约定:
_x:单下划线,"受保护,别乱碰",纯约定,硬要访问也不拦。__x:双下划线,触发 name mangling(名称改写),变成_类名__x,主要防继承命名冲突,别当安全锁。
class Account:
def __init__(self):
self._balance = 0 # 受保护:外部不要直接改
def deposit(self, n):
if n > 0:
self._balance += n # 在方法里做校验
def withdraw(self, n):
if 0 < n <= self._balance:
self._balance -= n
return n
return 0
def get_balance(self):
return self._balance
acc = Account()
acc.deposit(100)
acc.withdraw(30)
print(acc.get_balance()) # 70
更现代的写法用 @property,把 getter/setter 伪装成"普通属性":
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # 走下面的 setter
@property
def celsius(self): # getter:读 t.celsius 时调用
return self._celsius
@celsius.setter
def celsius(self, value): # setter:写 t.celsius 时调用
if value < -273.15: # 绝对零度,物理上不可能
value = -273.15 # 校验不过就钳制
self._celsius = value
@property
def fahrenheit(self): # 只读属性:没有 setter
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.celsius = 0
print(t.fahrenheit) # 32.0
t.fahrenheit 读起来像属性(没括号),背后是方法——想加校验、缓存、计算都行。只写 @property 不写 @xxx.setter 的属性是只读的,赋值报 AttributeError。
⚠️ 常见错误
- 以为
_x是强制私有:外部照样acc._balance = 999不报错,只是"君子协定"。真防改动用@property做成只读。 - setter 里没做校验:写了
@property却让外部随便赋值,等于没封装。 - property 和同名属性冲突:
@property def celsius下面又写self.celsius = value→ 无限递归RecursionError。对外属性名与内部存储名分开(统一存self._celsius)。
7.7 继承:子类站在父类的肩膀上 ⭐
继承(inheritance):父类写公共部分,子类写自己的特色,不重复造轮子。
class Animal:
"""动物基类"""
def __init__(self, name):
self.name = name
def sound(self):
return "..."
class Dog(Animal): # 括号里写父类
def __init__(self, name):
super().__init__(name) # ⭐ 先调父类构造,把 name 初始化好
self.legs = 4 # 再补自己的属性
def sound(self): # 重写 override:覆盖父类方法
return "汪汪"
class Cat(Animal):
def sound(self):
return "喵"
d = Dog("旺财")
print(d.name) # 旺财(来自父类的 __init__)
print(d.sound()) # 汪汪(子类重写后的)
- 子类自动继承父类的所有方法和属性。
- 重写(override):子类定义同名方法覆盖父类的。
super()拿到父类"代理",super().__init__(name)是"让父类先初始化该初始化的"。子类__init__若需要父类属性,必须调它,否则self.name不存在——这是最高频考点。
多态(polymorphism):不同子类重写同一方法,调用方只调 sound(),各叫各的:
for a in [Dog("旺财"), Cat("咪咪")]:
print(a.sound()) # 旺财 → 汪汪;咪咪 → 喵
类型判断:isinstance(obj, Animal)(obj 是不是 Animal 的实例,含子类实例);issubclass(Dog, Animal)(Dog 是不是 Animal 的子类)。
⚠️ 常见错误
- 子类
__init__忘了super().__init__():父类属性(如self.name)没建立,一访问就AttributeError。 - 重写后想用父类方法:要写
super().方法名(),直接写方法名()会找不存在的全局函数。 - 多继承的坑:
class C(A, B)的查找顺序(MRO)是C → A → B → object。做题先画继承链,沿链从子类往父类找。 isinstance方向搞反:Dog(...)是Animal类型 →True;Animal(...)不是Dog类型 →False。
7.8 类方法 @classmethod 与静态方法 @staticmethod
实例方法操作"一个对象"(有 self)。有些功能跟"类"有关、跟具体对象无关,用两种装饰器:
class Dog:
species = "犬科"
@classmethod # 类方法:操作类 / 造对象
def create(cls, name): # 第一个参数是类 cls(约定名)
return cls(name) # 相当于 Dog(name)
@staticmethod # 静态方法:与类无关的工具函数
def info(): # 没有 self 也没有 cls
return "狗是人类的朋友"
d = Dog.create("旺财") # 用类方法造对象(工厂模式)
print(d.name) # 旺财
print(Dog.info()) # 狗是人类的朋友
@classmethod:第一参数cls(类本身)。典型用途是工厂方法(如"用直径创建圆")和读写类变量。@staticmethod:不接收self/cls,是"放在类里的普通函数",放逻辑上属于该类的工具函数(如Calculator.add)。- 两者都不强制通过实例调用;习惯上用类名调用。
⚠️ 常见错误
- 类方法忘了写
cls:def create(name):→ 调用Dog.create("旺财")报错(跟self忘写同理)。 - 静态方法里写了
self:@staticmethod没有self,想用类变量就用Dog.species。 - 分不清两者:需要类变量 / 要返回
cls(...)→@classmethod;完全不碰类 →@staticmethod。
7.9 组合:一个类"装"着另一个类
继承是 IS-A(狗是动物),组合是 HAS-A(车有引擎)。组合 = 把一个类的对象作为另一个类的属性:
class Engine:
def __init__(self, power):
self.power = power
def describe(self):
return f"{self.power} 马力"
class Car:
def __init__(self, brand, engine):
self.brand = brand
self.engine = engine # 组合:Car 持有 Engine 对象
def describe(self):
return f"{self.brand},搭载 {self.engine.describe()}"
engine = Engine(200)
car = Car("奥迪", engine)
print(car.describe()) # 奥迪,搭载 200 马力
组合的现实意义:引擎可以独立存在、可以换(换引擎不用重新设计车),比继承更灵活。设计原则常说"多用组合,少用继承"。
⚠️ 常见错误
- 忘传组合对象:
Car("奥迪")少了engine→TypeError: missing 1 required positional argument。 - 方法调用链写错:
self.engine.describe()别漏中间的.engine。 - 组合继承硬混:B 只是"有一个" A 就别让 B 继承 A。
7.10 综合案例:把本讲串起来的 TodoList
一个"待办清单"系统,几乎用到本讲所有知识点——组合(清单装任务)、魔法方法(__len__/__str__)、实例状态、方法:
class Task:
"""单个任务"""
def __init__(self, title):
self.title = title
self.done = False # 实例属性:完成状态
def mark_done(self):
self.done = True
def __str__(self): # 完成打勾,未完成打空框
return ("✓ " if self.done else "□ ") + self.title
class TodoList:
"""待办清单:组合了一组 Task"""
def __init__(self):
self.tasks = [] # 组合:清单里装一堆任务
def add(self, title):
self.tasks.append(Task(title))
def complete(self, index):
self.tasks[index].mark_done()
def pending_count(self):
return sum(1 for t in self.tasks if not t.done)
def __len__(self):
return len(self.tasks)
def __str__(self):
return "\n".join(str(t) for t in self.tasks)
todo = TodoList()
todo.add("写作业")
todo.add("背单词")
todo.complete(0)
print(len(todo)) # 2
print(todo.pending_count()) # 1
print(todo)
# 输出:
# ✓ 写作业
# □ 背单词
⚠️ 常见错误
__str__里用print而不是return:__str__必须返回字符串,在里头print会导致print(todo)输出两遍。__len__忘了return。- 组合类里忘了
self.tasks:直接写tasks.append(...)→NameError。
7.11 常见题型与考试套路
- 写出运行结果(白给分题):给出类定义 + 调用,写出
print结果。顺序:先看__str__/__repr__(决定print(对象)显示什么)→ 再盯self.xxx赋值(画"对象属性表")→ 最后看类变量有没有被改(只有类名.xxx = ...才改到类变量)。 - 设计类:给需求(学生/账户/图书/形状)让你补全。按"属性 → 方法 → 魔法方法"顺序写。
- 继承调用顺序:问
obj.method()实际调谁。沿继承链从子类往父类找,子类先命中先用子类的。
💡 记忆口诀
- 建类三步走:
class大写开头、__init__里建属性、方法第一参数写self。 - self 三问:声明要写、访问属性要写、调用兄弟方法要写——方法里啥都要
self.。 - 类变量改名用类名:
Student.total += 1,别用self.total。 - 魔法方法记触发时机:打印找
__str__,比较找__eq__,求长找__len__,相加找__add__。 - 继承两个必答:子类要父类属性先
super().__init__();要父类方法用super().方法名()。
📌 双语术语表(本讲)
| 中文 | English |
|---|---|
| 类 / 对象 | class / object(instance) |
| 实例 | instance |
| 属性 | attribute |
| 方法 | method |
| 构造方法 | constructor(__init__) |
| 继承 | inheritance |
| 重写 | override |
| 多态 | polymorphism |
| 封装 | encapsulation |
| 魔法方法 | magic method / dunder |
| 类变量 | class variable |
| 实例变量 | instance variable |
| 属性装饰器 | property |
| 类方法 | class method |
| 静态方法 | static method |
| 组合 | composition |
| 运算符重载 | operator overloading |
⭐ 本讲考点清单
__init__+self(忘写 self / return 值)- 类变量 vs 实例变量(
类名.xxx才改类变量、可变默认值坑) - 继承 +
super().__init__()+ 方法重写 - 多态:同一方法不同子类不同行为
- 魔法方法
__str__/__repr__/__eq__/__len__/__add__ - 封装:单下划线
_x+ getter/setter +@property @classmethod(工厂方法)/@staticmethod(工具函数)- 组合(HAS-A):一个类持有另一个类的对象
isinstance/issubclass- 综合设计:属性 → 方法 → 魔法方法