到目前为止我们写的都是”函数式 + 过程式”的代码。真实项目里更常用面向对象编程(OOP)——用”类”把数据和行为打包在一起。Python 的 OOP 有一点特别:语法非常轻量,但概念完整,比 Java/C++ 更贴近”写脚本”的直觉。这一篇讲最基础的 class:类的定义、实例化、属性、方法、__init__、self,以及一个新时代的强力工具 @dataclass。
一、为什么需要类
看下面这段”过程式”代码:
1 2 3 4 5 6 7 8 9 10
| users = []
def add_user(users, name, age): users.append({"name": name, "age": age})
def get_name(user): return user["name"]
def celebrate_birthday(user): user["age"] += 1
|
问题:
user 只是普通字典,字段拼错不会报错;
- 函数散落各处,跟数据没有关联;
- 加一个字段(如 email)要修改多处;
- 逻辑跟数据没有归属感。
类把这些解决了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class User: def __init__(self, name, age): self.name = name self.age = age
def celebrate_birthday(self): self.age += 1
def __repr__(self): return f"User({self.name!r}, {self.age})"
u = User("咖飞", 3) u.celebrate_birthday() print(u)
|
- 数据(
name、age)与行为(celebrate_birthday)绑在一起;
- IDE 能自动补全
u. 后的方法;
- 拼错字段立刻报错(
u.nmae → AttributeError)。
二、定义一个类
1 2 3 4 5 6 7 8 9 10 11 12
| class ClassName: class_var = "shared"
def __init__(self, x, y): self.x = x self.y = y
def method(self): print(self.x, self.y)
|
命名规范:
- 类名用 PascalCase:
User、OrderService、HTTPClient;
- 方法和属性用 snake_case。
三、self 到底是什么
Python 里方法的第一个参数惯例叫 self——它就是”当前实例“本身:
1 2 3 4 5 6 7 8
| class Dog: def bark(self): print(f"{self.name} 在叫")
d = Dog() d.name = "旺财" d.bark()
|
self 不是 Python 关键字,只是约定俗成。你写 this 也能跑,但没人会那么写。
四、__init__ 构造方法
__init__ 在实例被创建后立即调用,用来做初始化:
1 2 3 4 5 6 7
| class Point: def __init__(self, x, y): self.x = x self.y = y
p = Point(3, 4) print(p.x, p.y)
|
没有显式定义 __init__ 时会用 object 的默认版本(不接收参数):
1 2 3 4 5
| class Foo: pass
Foo() Foo(1)
|
五、实例属性 vs 类属性
1 2 3 4 5 6 7 8 9 10 11
| class Dog: kind = "canine"
def __init__(self, name): self.name = name
d1 = Dog("旺财") d2 = Dog("小白")
print(d1.kind, d2.kind) print(d1.name, d2.name)
|
修改:
1 2 3 4 5 6 7
| d1.name = "阿黄"
d1.kind = "labrador" print(d2.kind)
Dog.kind = "犬" print(d1.kind, d2.kind)
|
规则:读取时先找实例,再找类;写入时永远是给实例。理解这一点能避免大部分”值为什么这样”的困惑。
⚠️ 陷阱:可变类属性
1 2 3 4 5 6 7
| class Team: members = []
t1 = Team() t2 = Team() t1.members.append("咖飞") print(t2.members)
|
修复:在 __init__ 里初始化:
1 2 3
| class Team: def __init__(self): self.members = []
|
六、实例方法调用的两种写法
1 2 3
| d = Dog("旺财") d.bark() Dog.bark(d)
|
前者是标准写法,后者只在某些内省/元编程场景用。
七、__repr__ 与 __str__
打印实例默认是 <__main__.Foo object at 0x...>,不友好。定义 __repr__ 让它好读:
1 2 3 4 5 6 7 8
| class Point: def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Point({self.x}, {self.y})"
print(Point(3, 4))
|
__repr__:给”程序员”看的,最好能”复制回来能重建对象”;
__str__:给”用户”看的,print(x) 走这个,没定义就 fallback 到 __repr__。
至少定义 __repr__。日志、调试都会自动用到它。
八、@dataclass:写类的现代方式
Python 3.7 起,dataclasses 模块让你用一行装饰器自动生成 __init__、__repr__、__eq__:
1 2 3 4 5 6 7 8 9 10 11
| from dataclasses import dataclass
@dataclass class User: name: str age: int email: str = ""
u = User("咖飞", 3) print(u) u == User("咖飞", 3)
|
比手写省一大堆样板代码。大部分”只装数据 + 少量方法”的类都推荐用 dataclass。
其他常用参数:
1 2 3
| @dataclass(frozen=True) @dataclass(order=True) @dataclass(slots=True)
|
九、类可以有方法
方法就是”定义在类里的函数”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): if amount <= 0: raise ValueError("金额必须 > 0") self.balance += amount
def withdraw(self, amount): if amount > self.balance: raise ValueError("余额不足") self.balance -= amount
def __repr__(self): return f"BankAccount({self.owner!r}, ¥{self.balance})"
a = BankAccount("咖飞", 100) a.deposit(50) a.withdraw(30) print(a)
|
方法总是至少一个参数 self(除非是 staticmethod/classmethod,下节讲)。
十、”私有”变量
Python 没有真正的”私有”,靠命名约定:
1 2 3 4 5 6 7 8 9 10
| class Foo: def __init__(self): self.public = 1 self._protected = 2 self.__private = 3
f = Foo() print(f.public) print(f._protected) print(f._Foo__private)
|
- 单下划线
_x:约定 私有,能访问但读代码的人知道”别碰”;
- 双下划线
__x(前双后无):name mangling,被改写成 _ClassName__x,主要用来避免子类意外覆盖。
日常写业务用单下划线就够了。双下划线只在写库或明确不想被覆盖时用。
十一、类里的常量
1 2 3 4 5 6 7 8 9 10
| class Circle: PI = 3.14159
def __init__(self, r): self.r = r
def area(self): return self.PI * self.r ** 2
print(Circle(3).area())
|
约定:常量用大写。
十二、类的生命周期钩子
除了 __init__,还有几个特殊方法:
1 2 3 4 5 6 7 8 9 10 11 12
| class Foo: def __new__(cls, *args, **kwargs): """创建实例(一般不用重写,除非做单例/元类)。""" return super().__new__(cls)
def __init__(self, x): """初始化实例(更常见)。""" self.x = x
def __del__(self): """垃圾回收时调用(一般不用)。""" print(f"删除 {self}")
|
__new__ 用得极少,只在单例、不可变类、元类里需要;
__init__ 是最常用的;
__del__ 不保证被调用,不要依赖它做资源清理,用 with 上下文管理器。
十三、一个完整例子:图书类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| """一个简单的图书类,展示 OOP 基础。"""
from dataclasses import dataclass
@dataclass class Book: title: str author: str year: int price: float stock: int = 0
def is_available(self) -> bool: return self.stock > 0
def sell(self, qty: int = 1) -> None: if qty > self.stock: raise ValueError(f"库存不足:只有 {self.stock} 本") self.stock -= qty
def restock(self, qty: int) -> None: if qty <= 0: raise ValueError("补货数量必须 > 0") self.stock += qty
def discounted_price(self, rate: float) -> float: """rate 是折扣率 0-1,如 0.2 表示 8 折。""" return self.price * (1 - rate)
if __name__ == "__main__": b = Book("Python 编程", "Guido", 2020, 59.9, stock=10) print(b) b.sell(3) print(f"卖出后库存 {b.stock}") print(f"打八折 ¥{b.discounted_price(0.2):.2f}")
|
十四、isinstance 与 type
判断一个对象是不是某类的实例:
1 2 3 4 5 6 7
| u = User("咖飞", 3)
isinstance(u, User) isinstance(u, object)
type(u) == User type(u) is User
|
永远用 isinstance,它支持继承(下一篇 继承与多态 讲)。
十五、常见陷阱
- 忘了
self:def method(x): 少了 self,调用时报错。
- 可变类属性共享:默认参数陷阱的兄弟,永远在
__init__ 里初始化可变属性。
self.x vs x:函数里 x = 5 是局部变量,不是实例属性。
__init__ 有返回值:__init__ 必须返回 None,写 return 值会 TypeError。
- 在类外访问
__private:Python 用 name mangling 阻止直接访问,除非硬走 _ClassName__private。
- 重写
__eq__ 忘了 __hash__:定义 __eq__ 会自动让 __hash__ = None(不可哈希)。dataclass 会自动处理。
十六、小结与延伸阅读
- 类是”数据 + 行为”的打包工具;
class Foo: + def __init__(self, ...): 是最基础组合;
self 是当前实例,方法第一个参数总是它;
- 实例属性在
__init__ 里赋值,类属性写在类体;
- 可变类属性会共享,用
__init__ 初始化;
- 至少定义
__repr__ 便于调试;
- 优先用
@dataclass 写数据类。
延伸阅读:
下一篇 属性、方法与 self 我们把类里的”细节”讲深。