类与对象基础

到目前为止我们写的都是”函数式 + 过程式”的代码。真实项目里更常用面向对象编程(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) # User('咖飞', 4)
  • 数据(nameage)与行为(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)

命名规范:

  • 类名用 PascalCaseUserOrderServiceHTTPClient
  • 方法和属性用 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() # 旺财 在叫
# 等价于 Dog.bark(d)

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) # 3 4

没有显式定义 __init__ 时会用 object 的默认版本(不接收参数):

1
2
3
4
5
class Foo:
pass

Foo() # 可以
Foo(1) # TypeError

五、实例属性 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) # canine canine
print(d1.name, d2.name) # 旺财 小白

修改:

1
2
3
4
5
6
7
d1.name = "阿黄"           # 只改 d1

d1.kind = "labrador" # 给 d1 创建实例属性 kind(不影响 d2)
print(d2.kind) # canine

Dog.kind = "犬" # 通过类改,所有实例都变(除了 d1,因为它有实例属性覆盖)
print(d1.kind, d2.kind) # labrador 犬

规则:读取时先找实例,再找类;写入时永远是给实例。理解这一点能避免大部分”值为什么这样”的困惑。

⚠️ 陷阱:可变类属性

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() # 隐式,Python 把 d 作为 self 传入
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)) # 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) # User(name='咖飞', age=3, email='')
u == User("咖飞", 3) # True

比手写省一大堆样板代码。大部分”只装数据 + 少量方法”的类都推荐用 dataclass

其他常用参数:

1
2
3
@dataclass(frozen=True)        # 不可变(属性不能改)
@dataclass(order=True) # 生成 <, <=, >, >= 比较方法
@dataclass(slots=True) # 3.10+,减少内存

九、类可以有方法

方法就是”定义在类里的函数”:

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) # BankAccount('咖飞', ¥120)

方法总是至少一个参数 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) # 1
print(f._protected) # 2,可访问但不建议
print(f._Foo__private) # 3,改写后的名字
  • 单下划线 _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
# book.py
"""一个简单的图书类,展示 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}")

十四、isinstancetype

判断一个对象是不是某类的实例:

1
2
3
4
5
6
7
u = User("咖飞", 3)

isinstance(u, User) # True
isinstance(u, object) # True,所有类都继承 object

type(u) == User # True,但不建议这么写
type(u) is User # 同上

永远用 isinstance,它支持继承(下一篇 继承与多态 讲)。

十五、常见陷阱

  1. 忘了 selfdef method(x): 少了 self,调用时报错。
  2. 可变类属性共享:默认参数陷阱的兄弟,永远在 __init__ 里初始化可变属性。
  3. self.x vs x:函数里 x = 5 是局部变量,不是实例属性。
  4. __init__ 有返回值__init__ 必须返回 None,写 return 值会 TypeError。
  5. 在类外访问 __private:Python 用 name mangling 阻止直接访问,除非硬走 _ClassName__private
  6. 重写 __eq__ 忘了 __hash__:定义 __eq__ 会自动让 __hash__ = None(不可哈希)。dataclass 会自动处理。

十六、小结与延伸阅读

  • 类是”数据 + 行为”的打包工具;
  • class Foo: + def __init__(self, ...): 是最基础组合;
  • self 是当前实例,方法第一个参数总是它;
  • 实例属性在 __init__ 里赋值,类属性写在类体;
  • 可变类属性会共享,用 __init__ 初始化;
  • 至少定义 __repr__ 便于调试;
  • 优先用 @dataclass 写数据类。

延伸阅读:

下一篇 属性、方法与 self 我们把类里的”细节”讲深。