类与对象入门
这一课你将能做到
- 用 class 定义带
__init__、实例方法、__str__ 的类,并创建多个互不干扰的对象
- 说清
self 是什么、实例属性与类属性的区别
- 写出基本的类继承,用
super().__init__() 复用父类初始化逻辑
为什么要学
前九课你写的是「函数 + 数据」——数据散在变量里,函数挨个收参数。当数据成组出现(每个用户都有名字、年龄、邮箱),传参就会越传越长。类把「数据 + 操作这组数据的方法」打包成一个类型:user = User("小明", 18),之后 user.greet() 自带上下文。标准库和第三方库几乎全是类:Path 对象、requests.Session、大模型客户端——不认识类,读文档都费劲。
人话:类是图纸,对象是照图纸造出来的房子。图纸定义「有哪些房间(属性)、能做什么事(方法)」,每栋房子数据各不相同、能力一样。造房子这个动作叫实例化。
先建立图像
class User: ← 图纸(类)
├─ name = ? ← 属性槽位:每栋房子填自己的值
└─ greet() ← 方法:所有房子共用的能力
u1 = User("小明") ← 造第一栋房子(实例化)
u2 = User("小红") ← 造第二栋房子
u1.name → "小明" ← 各自的数据
u2.name → "小红"
u1.greet is u2.greet → True ← 方法是共用的同一份
与 Java 对照:
// Java:构造器与方法分语法位,属性需声明类型
public class User {
private String name;
public User(String name) { this.name = name; }
public void greet() { System.out.println("hi " + name); }
}
# Python:__init__ 即构造器,self 显式写在参数首位
class User:
def __init__(self, name):
self.name = name
def greet(self):
print(f"hi {self.name}")
跟着做
步骤 1:定义第一个类并实例化
新建 oop_demo.py:
class User:
def __init__(self, name, age): # 构造方法:创建对象时自动调用
self.name = name # 实例属性:贴在对象身上的数据
self.age = age
def greet(self): # 实例方法:第一个参数固定 self
return f"我是 {self.name},{self.age} 岁"
def birthday(self): # 方法可以修改自己的属性
self.age += 1
u1 = User("小明", 18) # 实例化:类名(参数) → 自动传给 __init__
u2 = User("小红", 20)
print(u1.greet()) # 我是 小明,18 岁
print(u2.greet()) # 我是 小红,20 岁
u1.birthday() # 只影响 u1
print(u1.age, u2.age) # 19 20
输出:
我是 小明,18 岁
我是 小红,20 岁
19 20
关键体感:u1.greet() 实际是 User.greet(u1) 的语法糖——self 接到的就是 u1。方法不需要手动传 self,Python 替你传。
步骤 2:self 到底是什么
class Cat:
def __init__(self, name):
self.name = name
def who(self):
print(id(self)) # 对象内存身份
mimi = Cat("咪咪")
print(id(mimi)) # 和下面打印的 id 相同
mimi.who()
# 等价的手动调用形式:
Cat.who(mimi) # 显式把对象当参数传入
2198712345671
2198712345671
2198712345671
结论:self 就是「当前对象本身」。调用 mimi.who() 时 Python 自动把 mimi 塞给 self。名字叫 self 只是惯例,技术上叫别的也能跑——但没有任何理由不叫 self。
步骤 3:实例属性 vs 类属性
class Dog:
species = "犬科" # 类属性:所有实例共享
def __init__(self, name):
self.name = name # 实例属性:每个对象独有
d1, d2 = Dog("旺财"), Dog("小黑")
print(d1.species, d2.species) # 犬科 犬科(共享)
Dog.species = "Canidae"
print(d1.species, d2.species) # Canidae Canidae(跟着类变)
d1.species = "特殊犬" # ⚠ 这是在 d1 身上新建实例属性,
print(d1.species, d2.species) # 特殊犬 Canidae(d2 不受影响)
犬科 犬科
Canidae Canidae
特殊犬 Canidae
规则:读属性先找实例、再找类;给 实例.类属性 赋值 = 新建同名实例属性遮蔽类属性。所有实例共享的数据放类属性,独有数据放实例属性。
步骤 4:__str__ 让 print 出人话
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(3, 5)
print(p) # 默认输出:<__main__.Point object at 0x000001...> 没人话
<__main__.Point object at 0x0000021A5F3B7D90>
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 5)
print(p) # Point(3, 5)
__str__ 是「魔术方法」(双下划线命名)之一:print() 触发它。同类成员还有 __repr__(调试表示)、__eq__(== 比较)、__len__ 等——入门阶段先掌握 __str__ 和 __eq__。
步骤 5:继承与 super()
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} 发出声音"
class Dog(Animal): # 括号里写父类
def __init__(self, name, breed):
super().__init__(name) # 复用父类初始化
self.breed = breed # 追加自己的属性
def speak(self): # 重写(override)父类方法
return f"{self.name}:汪汪!"
class Cat(Animal):
def speak(self):
return f"{self.name}:喵~"
dog = Dog("旺财", "柴犬")
cat = Cat("咪咪")
print(dog.speak()) # 旺财:汪汪!
print(cat.speak()) # 咪咪:喵~
print(isinstance(dog, Animal)) # True:狗是动物
# 多态:同一个调用,不同子类各自表现
for a in (dog, cat):
print(a.speak())
旺财:汪汪!
咪咪:喵~
True
旺财:汪汪!
咪咪:喵~
步骤 6:小实战——用类管理银行账户
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款必须为正数") # 第 11 课细讲异常
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("余额不足")
self.balance -= amount
return self.balance
def __str__(self):
return f"{self.owner} 的账户,余额 {self.balance}"
acc = Account("小明", 100)
acc.deposit(50)
print(acc) # 小明 的账户,余额 150
acc.withdraw(30)
print(acc) # 小明 的账户,余额 120
小明 的账户,余额 150
小明 的账户,余额 120
这个例子展示类的本质价值:数据(balance)和规则(不能负存款、不能透支)绑在一个类型里,外部只能通过方法操作,逻辑不会被绕过。
讲清楚要点
- 类是模板,对象是实例:
User(...) 调用触发 __init__,self 指向新创建的对象。实例属性在 __init__ 里用 self.x = ... 挂上,每个对象一份。
- self 必须是实例方法的第一个参数:定义时写上、调用时省略(Python 自动传)。遗漏 self 是初学者最高频的 TypeError 来源。
__init__ 是初始化不是分配内存:内存分配由 __new__ 完成(入门不必深究),__init__ 只负责把属性填好。它不需要 return(返回非 None 反而报错)。
- 实例属性遮蔽类属性:读取时先实例后类;赋值
obj.attr = ... 永远作用于实例。共享常量放类属性,注意别误给「实例.类属性」赋值。
- 魔术方法让自定义类型融入语法:
__str__ 管 print、__eq__ 管 ==、__repr__ 管调试输出。Python 不用实现接口,实现对应魔术方法即可参与内建语法。
- 继承 + super 复用:子类重写方法即多态;
super().__init__() 保证父类逻辑先跑。组合优于继承的老话先记着:能用「类里装另一个类的对象」就别硬造继承链。
- Python 没有 private 关键字:下划线前缀
_name 只是约定「内部使用」,语言不强制。这是 Python「我们都是成年人」的哲学。
你可能会懵的地方
-
方法里忘写 self 为什么报错?
def greet(): return f"hi {self.name}" 调用 u1.greet() 时 Python 会传一个参数进去,而函数不收参数 → TypeError: greet() takes 0 positional arguments but 1 was given。报错里「但 1 was given」指的就是自动传入的 self。
-
__init__ 和 Java 构造器的区别?
形式上:Python 构造调用就是「类名(...)」,__init__ 不用 new 关键字;Java 构造器可以重载多个,Python 靠默认参数/类方法工厂模拟重载。语义上两者都是「新建对象后初始化属性」。
-
什么时候用类,什么时候用函数就够了?
数据只有一两个字段、操作彼此独立 → 函数 + 参数足够;数据成组、方法和这组数据强相关、需要多份实例 → 类。别为了「面向对象」硬包一层,也别在传参超过四个时还硬扛着不建类。
-
继承多层的 super() 到底找谁?
沿「方法解析顺序(MRO)」找,单继承链就是逐级向上;多继承按 C3 线性化算法决定顺序(ClassName.__mro__ 可查看)。入门阶段记住「super 调用的是 MRO 上的下一个」即可。
-
@property 是什么?
把方法伪装成属性:@property def age(self): return self._age 之后 obj.age 像读属性一样调用方法,可用于加校验、保持兼容。第 10 课之后进阶再学,见到代码里 @ 开头的行是「装饰器」语法。
错法对照
| 错法 | 问题 | 正解 |
|---|
def greet(): 忘写 self 参数 | TypeError: 1 was given | def greet(self): |
__init__ 里 name = name | 创建了局部变量,实例属性没挂上 | self.name = name |
在 __init__ 里 return self | TypeError: 返回必须为 None | __init__ 不写 return |
d1.species = "x" 想改类属性 | 只是新建实例属性遮蔽 | Dog.species = "x" 改类 |
子类重写 __init__ 忘调 super | 父类属性没初始化 | super().__init__(...) |
| 所有东西都塞进类 | 过度设计 | 无状态工具用函数 |
print(u1) 直接看属性 | 打出对象地址 | 实现 __str__ |
卡点急救
- TypeError: takes 0 positional arguments but 1 was given:实例方法漏了 self,补上第一个参数。
- AttributeError: 'X' object has no attribute 'y':属性没在
__init__ 里初始化,或拼错名字。检查 self.y 赋值语句。
- 所有对象共享了同一份数据:把可变默认值或实例数据写成了类属性(
class C: items = [])。移进 __init__:self.items = []。
- 子类实例缺父类的属性:重写
__init__ 忘了 super().__init__(),补上。
- print 对象显示一串地址:没实现
__str__,按步骤 4 加上。
__init__ 报 TypeError: init() should return None:里面写了 return 值,删掉。
面试常问
-
解释 self,为什么方法第一个参数必须是它?
self 指向当前实例。obj.method(x) 等价于 Class.method(obj, x)——Python 把方法当「绑定了实例的普通函数」处理,实例作为第一个实参显式传入。这是 Python 显式优于隐式哲学的体现(对比 Java/C++ 的隐式 this)。
-
__init__ 与 __new__ 的区别?
__new__ 负责创建并返回实例(静态方法,真正的「分配」),__init__ 负责初始化刚创建的实例(不返回值)。调用顺序:先 __new__ 后 __init__。自定义不可变类型或实现单例时才需要动 __new__。
-
实例属性、类属性的区别与查找顺序?
实例属性存放在实例 __dict__,每个对象独立;类属性存放在类 __dict__,所有实例共享。读取按「实例 → 类 → 父类(MRO)」查找;通过实例赋值只会创建同名实例属性,不会修改类属性。
-
Python 如何实现多态?和 Java 的区别?
Python 是鸭子类型:不要求共同父类或接口,只要对象有同名方法就能多态调用(「走起来像鸭子就叫它鸭子」)。Java 需要显式继承/接口约束。Python 的继承与魔术方法是锦上添花,不是多态的门槛。
-
__str__ 和 __repr__ 的区别?
__str__ 面向用户,print()/str() 调用,追求可读;__repr__ 面向开发者,交互式回显/repr() 调用,追求无歧义(理想是能重建对象)。只实现一个时通常先实现 __repr__(str 缺失时会退回 repr)。
自测三问
u1.greet() 和 User.greet(u1) 是什么关系?这解释了 self 的什么机制?
- 类属性和实例属性在「读」和「写」两个方向上行为有什么不同?
- 写出 Dog 继承 Animal 的
__init__ 骨架,并说明 super() 那一行的作用。
小结一句话
类 = 数据 + 方法的图纸,__init__ 挂属性、self 指对象、__str__ 管打印、继承配 super 实现复用与多态——认识类的第一天就要能读懂标准库里的对象。
延伸