1. 认识
类(class)是创建对象的一种模板。它把一组相关的数据和处理这些数据的函数放在一起,形成一个更完整的模型。
例如,一本书有标题、作者这些数据,也可以有介绍自己、修改标题这些行为。用类描述后,每一本具体的书都是一个对象:
01class Book:02def __init__(self, title, author):03self.title = title04self.author = author0506def describe(self):07return f"《{self.title}》的作者是{self.author}。"0809first_book = Book("Python 入门", "小林")10second_book = Book("FastAPI 实战", "小周")1112print(first_book.describe())13print(second_book.describe())
Book 是类,first_book 和 second_book 是两个实例。它们使用同一套结构,但各自保存不同的标题和作者。
类适合描述「有状态、有行为」的对象。如果只是临时组合几个字段,字典往往更直接;当数据和操作逐渐变多时,类可以让代码的职责更清晰。
2. 定义
使用 class 定义类,类名通常采用大驼峰命名,例如 Book、ChatSession。类体通过缩进表示:
1class User:2pass34user = User()5print(user)
pass 表示暂时没有实现任何内容。User() 会创建一个 User 实例,调用类的过程也叫实例化。
通常会在类中定义 __init__(),为实例准备初始数据:
1class User:2def __init__(self, name, email):3self.name = name4self.email = email56user = User("小明", "xiaoming@example.com")7print(user.name) # 小明8print(user.email) # xiaoming@example.com
__init__() 会在实例创建后自动调用。self 代表当前实例,self.name 和 self.email 是这个实例自己的属性。调用 User("小明", ...) 时,传入的参数会交给 __init__(),不需要手动传 self。
很多教程会把 __init__() 口语化地称为「构造函数」,但从 Python 的严格机制看,它是初始化方法。真正负责创建实例的是 __new__()。调用 User(...) 时,可以把过程简化理解为:先由 __new__() 创建并返回实例,再由 __init__() 给这个实例填充初始数据。
01class User:02def __new__(cls, name):03print("1. __new__: 创建实例")04instance = super().__new__(cls)05return instance0607def __init__(self, name):08print("2. __init__: 初始化实例")09self.name = name1011user = User("小明")12print(user.name)
输出顺序是先执行 __new__(),再执行 __init__()。__new__() 接收类本身 cls,必须返回一个实例;__init__() 接收已经创建好的实例 self,不需要返回实例。
| 方法 | 调用时机 | 第一个参数 | 主要职责 | 日常使用 |
|---|---|---|---|---|
__new__() | 实例创建之前 | cls | 创建并返回实例 | 很少重写 |
__init__() | 实例创建之后 | self | 初始化实例属性 | 经常使用 |
如果 __new__() 没有返回当前类的实例,Python 不会继续调用对应的 __init__()。重写 __new__() 常见于继承 str、tuple 等不可变类型,或实现单例等特殊创建逻辑;普通业务类通常只需要编写 __init__()。
每个实例可以拥有不同的属性值,也可以在创建后增加属性:
01class Account:02def __init__(self, owner):03self.owner = owner04self.balance = 00506account = Account("小明")07account.balance = 10008account.currency = "CNY"0910print(account.owner)11print(account.balance)12print(account.currency)
不过,业务需要的属性最好在 __init__() 中统一初始化。这样,其他代码只要看到类的定义,就能知道一个实例通常包含哪些数据。
3. 方法
写在类中的函数叫作方法。实例方法的第一个参数通常是 self,通过它读取或修改当前实例:
01class Counter:02def __init__(self):03self.value = 00405def increase(self, amount=1):06self.value += amount0708def current(self):09return self.value1011counter = Counter()12counter.increase()13counter.increase(4)14print(counter.current()) # 5
调用 counter.increase(4) 时,Python 会自动把 counter 作为 self 传给方法。可以把它近似理解为 Counter.increase(counter, 4),所以方法内部才能访问 self.value。
方法不仅可以读取属性,也可以返回计算结果。把和对象状态有关的操作放进方法,调用方就不需要了解内部实现:
01class Order:02def __init__(self, price, quantity):03self.price = price04self.quantity = quantity0506def total(self):07return self.price * self.quantity0809def summary(self):10return f"共 {self.quantity} 件,合计 {self.total()} 元"1112order = Order(30, 2)13print(order.summary()) # 共 2 件,合计 60 元
如果希望 print(order) 时显示更有意义的内容,可以实现 __str__():
1class User:2def __init__(self, name):3self.name = name45def __str__(self):6return f"User(name={self.name})"78print(User("小明")) # User(name=小明)
以双下划线包围的 __init__、__str__ 等方法叫特殊方法。它们通常由 Python 在特定时机自动调用,不需要像普通方法那样直接使用。
类中还可以定义类方法和静态方法。它们与实例方法的区别在于,自动接收的第一个参数不同:
| 方法类型 | 装饰器 | 第一个参数 | 适合处理 |
|---|---|---|---|
| 实例方法 | 无 | self | 当前实例的数据 |
| 类方法 | @classmethod | cls | 整个类的数据或创建实例 |
| 静态方法 | @staticmethod | 无 | 与类主题相关、但不依赖实例和类状态的工具逻辑 |
01class User:02def __init__(self, name):03self.name = name0405def rename(self, name):06self.name = name0708@classmethod09def guest(cls):10return cls("游客")1112@staticmethod13def is_valid_name(name):14return bool(name.strip())1516user = User.guest()17user.rename("访客")18print(user.name) # 访客19print(User.is_valid_name("小明")) # True
guest() 是类方法,可以通过 cls 创建当前类的实例,因此常被用作「替代构造方法」。is_valid_name() 不需要读取实例或类的数据,只是一个和用户相关的检查函数,所以可以定义为静态方法。
除了 __str__(),还可以实现 __repr__() 和 __eq__()。前者主要用于调试时展示对象,后者用于定义两个实例如何比较:
01class User:02def __init__(self, name):03self.name = name0405def __str__(self):06return self.name0708def __repr__(self):09return f"User(name={self.name!r})"1011def __eq__(self, other):12return isinstance(other, User) and self.name == other.name1314first = User("小明")15second = User("小明")1617print(str(first)) # 小明18print(repr(first)) # User(name='小明')19print(first == second) # True
str() 更适合面向用户的内容,repr() 更适合调试。默认情况下,两个不同实例即使属性相同,使用 == 也不会被认为相等;实现 __eq__() 后,类可以定义自己的比较规则。is 判断的是不是同一个对象,不会调用 __eq__()。
4. 私有
Python 没有像某些语言那样强制禁止访问的私有方法。下划线主要是给开发者的访问约定:
| 写法 | 含义 | 是否可以从类外调用 |
|---|---|---|
method() | 公共方法,供外部正常使用 | 可以 |
_method() | 内部方法,表示不建议外部直接使用 | 可以,但不推荐 |
__method() | 触发名称改写,主要用于避免子类意外覆盖 | 仍然可以通过改写后的名称访问 |
__method__() | 特殊方法,由 Python 在特定时机调用 | 通常不手动调用 |
单个下划线最常见。它不会阻止访问,只是告诉调用方「这是类的内部实现,公开接口发生变化时不一定保持兼容」:
01class Account:02def __init__(self, owner):03self.owner = owner04self._balance = 00506def deposit(self, amount):07if not self._is_valid_amount(amount):08raise ValueError("存款金额必须大于 0")0910self._balance += amount1112def _is_valid_amount(self, amount):13return amount > 01415account = Account("小明")16account.deposit(100)17print(account._balance) # 可以访问,但业务代码不应该依赖这个内部属性
deposit() 是对外提供的操作,_is_valid_amount() 只是它内部使用的辅助方法。外部代码技术上可以调用 _is_valid_amount(),但应该优先使用类提供的公共方法。
两个连续的前置下划线会触发名称改写(name mangling)。例如,__cleanup() 会在类内部被改写为类似 _Session__cleanup,这样子类定义同名方法时,不容易意外覆盖父类的内部实现:
01class Session:02def __cleanup(self):03print("清理会话")0405def close(self):06self.__cleanup()0708session = Session()09session.close() # 清理会话10# session.__cleanup() # AttributeError:原始名称不能直接访问
名称改写不是安全机制,也不能真正阻止访问。它主要用于减少子类和父类之间的命名冲突。最后要特别区分:__init__()、__str__() 两端都有下划线,它们是特殊方法,不是私有方法。
5. 属性
实例属性属于某一个对象,类属性则属于整个类。类属性可以被所有实例读取:
01class Article:02category = "Python"0304def __init__(self, title):05self.title = title0607first = Article("变量")08second = Article("函数")0910print(first.category) # Python11print(second.category) # Python
如果实例没有自己的同名属性,Python 会继续到类上查找。因此,类属性适合保存所有实例共享的常量或默认配置:
1class Request:2default_timeout = 3034def __init__(self, path):5self.path = path67request = Request("/users")8print(request.default_timeout)9print(Request.default_timeout)
不要把可变对象(例如列表、字典)作为类属性来保存每个实例自己的状态,否则所有实例可能会共享同一个对象:
1class BadPlaylist:2songs = []34first = BadPlaylist()5second = BadPlaylist()6first.songs.append("晴天")78print(second.songs) # ['晴天'],两个实例共享同一个列表
正确做法是把可变属性放到 __init__() 中,让每个实例创建自己的列表:
01class Playlist:02def __init__(self):03self.songs = []0405first = Playlist()06second = Playlist()07first.songs.append("晴天")0809print(first.songs) # ['晴天']10print(second.songs) # []
如果直接公开属性,外部代码可以写入任意值。@property 可以让一个方法以属性的方式被读取,并在赋值时加入校验:
01class User:02def __init__(self, name):03self.name = name0405@property06def name(self):07return self._name0809@name.setter10def name(self, value):11value = value.strip()12if not value:13raise ValueError("用户名不能为空")1415self._name = value1617user = User(" 小明 ")18print(user.name) # 小明19user.name = "小红"
调用方仍然使用 user.name,不需要写成 user.name()。只有定义 @property 而没有定义 setter 时,这个属性才是只读的。@property 适合把校验、格式转换和兼容旧接口的逻辑封装在类内部。
6. 组合
类可以通过组合表达「一个对象使用另一个对象」。例如,一个聊天服务可以持有模型名称,并负责组装请求数据:
01class ChatService:02def __init__(self, model_name="deepseek-chat"):03self.model_name = model_name0405def build_request(self, question):06return {07"model": self.model_name,08"messages": [09{"role": "user", "content": question},10],11}1213service = ChatService()14request = service.build_request("请介绍一下 Python。")15print(request)
在真实的 FastAPI 项目中,接口函数可以接收请求,再调用 ChatService 的方法;在 LangChain 项目中,服务类也可以持有模型实例、提示词和工具。这样,路由负责 HTTP,服务类负责业务,模型调用不会全部堆在接口函数里。
如果一个类主要用于保存数据,可以使用标准库中的 dataclass。它会根据类型注解自动生成常用的 __init__()、__repr__() 和 __eq__():
01from dataclasses import dataclass0203@dataclass04class ChatRequest:05question: str06model: str = "deepseek-chat"0708request = ChatRequest("请介绍一下 Python。")09print(request)10print(request == ChatRequest("请介绍一下 Python。")) # True
dataclass 适合数据结构清晰、行为较少的对象,例如 FastAPI 的请求配置或 LangChain 的运行参数。如果一个类包含较多业务行为,仍然可以手写普通类和方法。
类之间还可以通过继承复用代码。子类会获得父类的方法和属性,也可以覆盖某个方法:
01class Message:02def __init__(self, content):03self.content = content0405def render(self):06return self.content0708class SystemMessage(Message):09def render(self):10return f"[系统] {self.content}"1112message = SystemMessage("你是一名温和的助手。")13print(message.render()) # [系统] 你是一名温和的助手。
子类需要复用父类的初始化逻辑时,可以调用 super():
01class User:02def __init__(self, name):03self.name = name0405class Admin(User):06def __init__(self, name, level):07super().__init__(name)08self.level = level0910admin = Admin("小明", 2)11print(admin.name) # 小明12print(admin.level) # 2
继承适合表达明确的「是一种」关系,例如 Admin 是一种 User。如果两个对象只是互相协作,优先考虑组合,避免为了复用几行代码创建过深的继承层级。
可以使用 isinstance() 检查一个对象是否属于某个类,使用 issubclass() 检查两个类之间是否存在继承关系:
01class User:02pass0304class Admin(User):05pass0607admin = Admin()0809print(isinstance(admin, Admin)) # True10print(isinstance(admin, User)) # True,子类实例也属于父类类型11print(issubclass(Admin, User)) # True
type(admin) is Admin 只判断对象的直接类型,而 isinstance(admin, User) 还会考虑继承关系。业务代码需要兼容子类时,通常优先使用 isinstance()。