类是蓝图,实例是照着蓝图造出来的产物。这篇把「怎么定义类、怎么用类」讲透,而不是背诵术语。
你将学到
- 用
class定义类,理解__init__和self到底在干什么 - 分清实例属性与类属性,知道可变类属性为什么是坑
- 掌握实例方法、
@classmethod、@staticmethod的区别与用途 - 用继承与
super()复用代码,对 MRO 有个直观认识 - 会用
@property、__repr__等入门魔术方法,并记住「组合优于继承」
前置知识
假定你已经会写函数、会用列表和字典、见过缩进和 for 循环。这一篇回到语言本身,不依赖前面任何一章。
类是蓝图,实例是产物
class Dog:
"""一只狗的类。"""
def __init__(self, name): # 实例创建后会自动调用
self.name = name # self 就是"正在创建的实例",给它挂属性
def bark(self): # 第一个参数永远是实例本身,约定叫 self
return "汪汪!"
d1 = Dog("旺财") # Dog(...) 就是"调用类",产生一个实例
d2 = Dog("小黑")
print(d1 is d2) # 输出: False,每次都是全新对象
print(d1.bark()) # 输出: 汪汪!
print(d1.name) # 输出: 旺财
class Dog: 定义蓝图,Dog(...) 照蓝图造实例,d1 与 d2 彼此独立。几个要点:
self不是关键字,只是第一个参数的约定名,写成别的也能跑,但别这么干。d1.bark()等价于Dog.bark(d1),Python 自动把d1塞进self。- 带
self.的才是实例属性;不带self.的只是局部变量,函数结束就没了。
实例属性 vs 类属性
挂在 self 上的是实例属性,挂在类上的是类属性。
class Circle:
pi = 3.14159 # 类属性,所有实例共享
def __init__(self, r):
self.r = r # 实例属性,每个实例各一份
c1, c2 = Circle(1), Circle(2)
print(c1.pi, c2.pi) # 输出: 3.14159 3.14159
c1.pi = 3 # 通过实例赋值,会给 c1 新建实例属性,遮蔽类属性
print(c1.pi, Circle.pi) # 输出: 3 3.14159 ← 类属性本身没变
查找顺序是:先找实例属性,找不到再找类属性。注意别把可变对象(列表、字典)当类属性:class Team: members = [] 会被所有实例共享,a.members.append("小明") 之后 b.members 里也会冒出"小明"。正确做法是在 __init__ 里 self.members = [],给每个实例建独立的一份。
三种方法:实例方法 / 类方法 / 静态方法
class Demo:
def instance_method(self): # 实例方法:能拿到 self
return "要实例才能调"
@classmethod
def class_method(cls): # 类方法:第一个参数是类本身 cls
return "类就能调"
@staticmethod
def static_method(): # 静态方法:不碰 self 也不碰 cls
return "只是挂在类里的普通函数"
print(Demo.class_method()) # 输出: 类就能调
print(Demo.static_method()) # 输出: 只是挂在类里的普通函数
print(Demo().instance_method()) # 输出: 要实例才能调
选择原则:需要读写 self 用实例方法;@classmethod 最典型的用途是"备用构造器",比如 Date.from_string("2026-10-07") 返回一个 cls(...);@staticmethod 适合逻辑上属于该类、但不需要对象状态的工具函数。用 cls 而非写死类名,子类继承后也能正确创建。
继承与 super()
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Cat(Animal): # Cat 继承 Animal
def speak(self): # 覆盖(override)父类方法
return "喵"
class Dog(Animal):
def speak(self):
return "汪"
for a in [Cat("咪咪"), Dog("旺财")]:
print(a.name, a.speak())
# 输出: 咪咪 喵
# 输出: 旺财 汪
要在子类里调用父类的实现,用 super()。
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # 先让父类初始化 name
self.color = color # 再加自己的属性
c = Cat("咪咪", "橘")
print(c.name, c.color) # 输出: 咪咪 橘
MRO 一句话版:一个类可以继承多个父类,方法查找顺序叫 MRO(方法解析顺序),由 C3 算法算出一套唯一顺序,直观理解是"先自己的类,再按继承列表从左到右、每个分支先深后广"。用 D.__mro__ 能看到确切顺序,例如 D(B, C) 会得到 [D, B, C, A, object]。日常配合 super() 用就好,别刻意搞复杂多继承。
封装:下划线约定
Python 没有 private 关键字,靠命名约定表达可见性:
name:公开。_name:单下划线,表示"内部使用,别碰"。__name:双下划线开头,会触发名字改写(name mangling),实际变成_ClassName__name。
class Account:
def __init__(self, balance):
self._balance = balance # 约定为内部属性
def deposit(self, amount):
if amount <= 0:
raise ValueError("金额必须为正")
self._balance += amount
a = Account(100)
a.deposit(50)
print(a._balance) # 输出: 150
约定不是强制,靠的是"大家都懂规矩"。
@property:把方法变成属性
想对外提供只读值,或让赋值经过校验,用 @property。
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self): # 像属性一样访问,不用加括号
return self._celsius * 9 / 5 + 32
@celsius.setter # 定义 celsius 的 setter
def celsius(self, value):
if value < -273.15:
raise ValueError("低于绝对零度了")
self._celsius = value
t = Temperature(25)
print(t.fahrenheit) # 输出: 77.0
t.celsius = 30 # 走 setter
print(t._celsius) # 输出: 30
魔术方法入门
双下划线包起来的方法(dunder)让对象接入 Python 的语法。入门先掌握三个:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # 给开发者看,调试时显示
return f"Point({self.x}, {self.y})"
def __str__(self): # 给用户看,print 时显示
return f"({self.x}, {self.y})"
def __eq__(self, other): # 定义 == 的比较规则
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
p = Point(1, 2)
print(p) # 输出: (1, 2) 走 __str__
print(repr(p)) # 输出: Point(1, 2) 走 __repr__
print(p == Point(1, 2)) # 输出: True
注意:一旦定义 __eq__,对象默认就不可哈希了,想放进 set 或当 dict 键,得再定义 __hash__。
组合优于继承
继承表达 "is-a"(猫是动物),组合表达 "has-a"(车有引擎)。现实里 "has-a" 更常见。当你发现继承层次超过两三层、或子类被迫实现一堆用不到的方法,就该换成组合。
class Engine:
def start(self):
return "引擎启动"
class Car:
def __init__(self):
self.engine = Engine() # 组合:Car 拥有一个 Engine
def start(self):
return f"汽车:{self.engine.start()}"
print(Car().start()) # 输出: 汽车:引擎启动
一个完整小例子:银行账户
把前面的知识点串起来。
class InsufficientFunds(Exception):
"""余额不足时抛出。"""
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须大于 0")
self._balance += amount
return self._balance
def withdraw(self, amount):
if amount > self._balance:
raise InsufficientFunds(f"余额不足:{self._balance}")
self._balance -= amount
return self._balance
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self._balance})"
acc = BankAccount("小明", 100)
acc.deposit(50)
print(acc.balance) # 输出: 150
try:
acc.withdraw(500)
except InsufficientFunds as e:
print("失败:", e) # 输出: 失败: 余额不足:150
print(acc) # 输出: BankAccount(owner='小明', balance=150)
常见坑
❌ 忘记写 self.,属性没挂到实例上;✅ 加上 self.。
class Counter:
def __init__(self):
count = 0 # ❌ 局部变量,实例上什么都没有
self.count = 0 # ✅ 这才是实例属性
❌ 子类 super().__init__() 漏传父类需要的参数;✅ 传对参数。
class Cat(Animal):
def __init__(self, name, color):
# super().__init__() # ❌ 父类需要 name,这里没传,稍后会报错
super().__init__(name) # ✅
self.color = color
❌ 用可变对象当类属性;✅ 在 __init__ 里创建。
class Cart:
items = [] # ❌ 所有实例共用同一个列表
def __init__(self):
self.items = [] # ✅ 每个实例各自一份
小结
class是蓝图,Foo()是产物;self指向实例自己。- 用
self.x = ...才是实例属性;类属性共享,当心可变对象的坑。 - 方法分实例方法、
@classmethod、@staticmethod,按是否需要self/cls选。 - 继承用
super()复用父类;MRO 决定多继承的查找顺序,日常别滥用。 _x/__x是约定式封装;@property用起来像属性,实则走方法。__repr__/__str__/__eq__让对象融入 Python 语法,__repr__尽量别省。- 能用组合就别硬套继承。
延伸阅读
- 熟练篇《数据处理 json / csv / dataclasses》里的
@dataclass能几行生成__init__/__repr__/__eq__,写"数据类"时优先用它,别手写。 - 大师篇《元类与类创建机制》会讲类本身是怎么被造出来的,读完回头看这篇会豁然开朗。
上一篇:网络请求与 Web 基础 · 下一篇:模块与包
文章回复
0 条公开回复