类和实例
Python 中的每个值都是对象。数字、字符串、列表是对象,自己定义的用户、订单和商品也可以是对象。**类(class)**描述一类对象共同的结构和行为,**实例(instance)**则是按照这个描述创建出来的具体对象。
可以把类想成“图纸”,把实例想成“根据图纸建成的房子”:同一张图纸可以建造很多栋房子,每栋房子又有自己的颜色、地址等状态。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 定义类并创建实例;
- 使用
__init__()初始化实例属性; - 区分实例属性、类属性和三种方法;
- 使用属性封装对象状态;
- 理解对象身份、相等性、继承和常用特殊方法;
- 判断什么时候适合普通类、数据类、继承或组合。
从一个完整的小例子开始
下面的 User 类表示用户。每个用户有自己的姓名和年龄,也都能执行相同的 introduce() 行为:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def introduce(self) -> str:
return f"我叫{self.name},今年{self.age}岁。"
first_user = User("小林", 18)
second_user = User("小周", 20)
print(first_user.name) # 小林
print(second_user.introduce()) # 我叫小周,今年20岁。
| 概念 | 例子 | 含义 |
|---|---|---|
| 类 | User | 描述用户具有哪些数据和行为 |
| 实例 | first_user、second_user | 两个具体的用户对象 |
| 实例属性 | first_user.name | 某个实例自己的数据 |
| 实例方法 | first_user.introduce() | 操作实例数据的函数 |
self | self.name | 当前正在调用方法的实例 |
类和实例的关系
类把数据和行为组织在一起
不用类时,可以用字典保存用户,用单独的函数处理用户:
user = {"name": "小林", "age": 18}
def introduce(user: dict[str, object]) -> str:
return f"我叫{user['name']},今年{user['age']}岁。"
这段代码可以工作,但数据与行为是分开的,调用方还必须记住字典中的键名。类可以把相关数据、行为和校验规则放在一起,为程序建立清晰的边界。
类本身也是对象。调用类会创建实例:
first_user = User("小林", 18)
second_user = User("小周", 20)
print(type(User)) # 通常是 <class 'type'>
print(type(first_user)) # <class '__main__.User'>
print(isinstance(first_user, User)) # True
两个实例可以有不同的状态:
print(first_user is second_user) # False
first_user.age = 19
print(first_user.age) # 19
print(second_user.age) # 20
最小的类
类体暂时没有内容时,可以用 pass 占位:
class Empty:
pass
value = Empty()
print(isinstance(value, Empty)) # True
类定义也是可执行语句。Python 执行类体后创建类对象,并把它绑定到类名。因此,类体中的语句是在定义类时执行,不是在每次创建实例时重新执行。
创建和初始化实例
使用 __init__() 设置初始状态
特殊方法 __init__() 通常负责初始化实例:
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
book = Product("Python 入门", 49.9)
print(book.name) # Python 入门
print(book.price) # 49.9
调用 Product(...) 时,可以简化理解为以下过程:
__new__()创建并返回实例;- Python 调用实例的
__init__()设置初始状态; - 类调用表达式得到初始化完成的实例。
严格来说,__init__() 是初始化方法,不负责创建实例。普通类通常只需要实现 __init__();控制不可变对象创建、实例缓存等高级场景才可能覆盖 __new__()。
__init__() 必须返回 None,不能返回其他值:
class Wrong:
def __init__(self):
return "错误的返回值"
# Wrong() # TypeError: __init__() should return None, not 'str'
self 表示当前实例
实例方法的第一个参数通常命名为 self。它不是关键字,但这是 Python 的通用约定:
class User:
def __init__(self, name: str):
self.name = name
def introduce(self) -> str:
return f"我叫{self.name}。"
调用 user.introduce() 时,Python 会自动把 user 传给 self。下面两种调用效果相同:
user = User("小林")
user.introduce()
User.introduce(user)
如果忘记声明 self,实例仍会被自动传入,从而产生参数数量错误:
class Broken:
def show():
return "缺少 self"
# Broken().show() # TypeError
实例属性:每个对象自己的状态
在 self 上赋值,会为当前实例设置属性:
class Counter:
def __init__(self, start: int = 0):
self.value = start
def increment(self) -> None:
self.value += 1
first = Counter()
second = Counter(10)
first.increment()
print(first.value) # 1
print(second.value) # 10
普通实例通常使用 __dict__ 保存属性:
print(first.__dict__) # {'value': 1}
__dict__ 有助于理解和调试,但业务代码通常应该通过类提供的公开接口使用对象。
在初始化时建立有效状态
应尽量在 __init__() 中设置所有必要属性,使实例一创建就处于有效状态:
class Task:
def __init__(self, title: str):
if not title.strip():
raise ValueError("标题不能为空")
self.title = title
self.completed = False
def finish(self) -> None:
self.completed = True
普通类默认允许在创建后动态增加属性:
task = Task("阅读文档")
task.priority = "high"
这种能力很灵活,但容易产生拼写错误或不一致的对象结构。稳定的数据最好在类中明确声明和初始化。
属性保存的是对象引用
把可变对象传给实例时,实例属性默认引用同一个对象:
class Playlist:
def __init__(self, songs: list[str]):
self.songs = songs
songs = ["A", "B"]
playlist = Playlist(songs)
songs.append("C")
print(playlist.songs) # ['A', 'B', 'C']
如果实例应该拥有独立数据,可以创建浅副本:
class Playlist:
def __init__(self, songs: list[str]):
self.songs = list(songs)
是否复制取决于接口约定:需要共享修改就保留引用,需要独立管理就复制。
实例方法:让对象执行行为
实例方法可以读取或修改当前实例的状态:
class BankAccount:
def __init__(self, owner: str, balance: float = 0):
if balance < 0:
raise ValueError("初始余额不能为负数")
self.owner = owner
self.balance = balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("存款金额必须大于 0")
self.balance += amount
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError("取款金额必须大于 0")
if amount > self.balance:
raise ValueError("余额不足")
self.balance -= amount
account = BankAccount("小林", 100)
account.deposit(50)
account.withdraw(30)
print(account.balance) # 120
把规则放进方法后,调用方只需表达“存款”和“取款”,不必在各处重复实现余额校验。好的类会提供有意义的操作,并负责维护自身状态的有效性。
方法可以返回结果,也可以修改状态。名称应清楚表达两者的差别:
class Circle:
def __init__(self, radius: float):
if radius < 0:
raise ValueError("半径不能为负数")
self.radius = radius
def area(self) -> float:
return 3.141592653589793 * self.radius**2
def enlarge(self, amount: float) -> None:
if amount < 0:
raise ValueError("增加量不能为负数")
self.radius += amount
area() 返回计算结果;enlarge() 修改半径并返回 None。需要复用结果时,应使用 return,不要用 print() 代替返回值。
类属性和实例属性
类属性由类及其实例共享
写在类体中的属性是类属性:
class Student:
school = "清风学校"
def __init__(self, name: str):
self.name = name
first = Student("小林")
second = Student("小周")
print(first.school) # 清风学校
print(second.school) # 清风学校
print(Student.school) # 清风学校
对于普通属性,可以先用以下简化顺序理解属性查找:实例 → 所属类 → 基类。property 等描述器有特殊的优先级,后面会看到一个例子。
同名实例属性会遮蔽类属性
通过实例给属性赋值,通常是在实例上创建同名属性,不会修改类属性:
first.school = "远山学校"
print(first.school) # 远山学校,读取实例属性
print(second.school) # 清风学校,读取类属性
print(Student.school) # 清风学校
del first.school
print(first.school) # 清风学校,再次读取类属性
修改所有实例使用的默认值时,应写 Student.school;只修改某个实例时,才写 first.school。
警惕可变的类属性
下面的列表会被所有实例共享:
class BadCart:
items = []
def add(self, item: str) -> None:
self.items.append(item)
first = BadCart()
second = BadCart()
first.add("书")
print(second.items) # ['书'],通常不是期望结果
append() 直接修改了类属性引用的列表。需要每个实例各有一份列表时,应在初始化方法中创建:
class Cart:
def __init__(self):
self.items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
共享可变对象并非永远错误,但必须明确它表达的是“所有实例共用一份状态”。
类型标注中可以用 ClassVar 表明类属性的设计意图:
from typing import ClassVar
class Student:
school: ClassVar[str] = "清风学校"
total: ClassVar[int] = 0
ClassVar 用于帮助类型检查工具理解代码,不会在运行时禁止实例给同名属性赋值。
实例方法、类方法和静态方法
类方法
@classmethod 定义的方法会自动接收类对象,参数通常命名为 cls。它常用于提供替代构造方式:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@classmethod
def from_text(cls, text: str) -> "User":
name, age = text.split(",")
return cls(name, int(age))
user = User.from_text("小林,18")
print(user.name, user.age) # 小林 18
使用 cls(...) 而不是写死 User(...),可以让子类继承该方法后创建子类实例:
class Admin(User):
pass
admin = Admin.from_text("管理员,30")
print(type(admin) is Admin) # True
静态方法
@staticmethod 定义的方法不会自动接收 self 或 cls:
class User:
@staticmethod
def is_valid_name(name: str) -> bool:
return bool(name.strip()) and len(name) <= 20
print(User.is_valid_name("小林")) # True
静态方法适合与类的概念密切相关、但不依赖实例或类状态的逻辑。如果函数与类没有明显关系,定义为模块级函数通常更直接。
| 方法类型 | 定义方式 | 自动传入的参数 | 适用场景 |
|---|---|---|---|
| 实例方法 | 普通方法 | 当前实例 self | 读取或修改实例状态 |
| 类方法 | @classmethod | 当前类 cls | 替代构造、操作类级状态 |
| 静态方法 | @staticmethod | 无 | 与类相关但不依赖状态的工具逻辑 |
封装和属性访问
Python 的可见性主要依靠约定
Python 没有强制的 private 关键字,常见命名约定是:
name:公开属性或方法;_name:内部实现细节,类外代码不应依赖;__name:触发名称改写,主要用于避免子类意外覆盖。
单下划线只是约定,外部代码仍可访问。双下划线也不是安全边界:
class Base:
def __init__(self):
self.__token = "base"
def token(self) -> str:
return self.__token
value = Base()
print(value.token()) # base
print(value._Base__token) # base,可以访问,但不推荐
__token 在类中被改写成类似 _Base__token 的名称,降低子类无意覆盖它的可能性,但没有真正阻止访问。
使用 @property 控制读写
当属性读写需要校验或计算时,可以使用 @property:
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
temperature = Temperature(20)
temperature.celsius = 25
print(temperature.celsius) # 25
# temperature.celsius = -300 # ValueError
调用方仍使用简洁的属性语法,但 setter 能保护对象的不变量。只定义 getter、不定义 setter 时,该属性对调用方是只读的。
不要为每个属性机械地编写 getter 和 setter。没有额外逻辑时,公开属性通常已经足够;真正需要校验、计算或保持接口兼容时再使用 property。
身份、相等性和类型
is 与 ==
a is b判断两个名称是否指向同一个对象;a == b判断两个对象的内容是否相等,可能调用__eq__()。
class User:
pass
first = User()
alias = first
second = User()
print(first is alias) # True
print(first is second) # False
print(first == second) # False,普通自定义类默认按身份比较
判断空值时使用 is None:
result = None
if result is None:
print("没有结果")
不要用 is 比较字符串、数字或业务对象的内容。某些 Python 实现可能复用小整数或字符串对象,但这不是可靠的内容比较规则。
type() 与 isinstance()
print(type(first) is User) # True,精确类型是 User
print(isinstance(first, User)) # True,是 User 或其子类实例
需要支持继承关系时,通常使用 isinstance()。只有明确要求精确类型时,才比较 type(value) is SomeClass。
特殊方法:接入 Python 的通用协议
特殊方法通常以双下划线开头和结尾。Python 会在特定语法或内置函数中自动调用它们。
__repr__() 和 __str__()
__repr__()面向开发和调试,应提供明确、有用的信息;__str__()面向普通用户,用于友好展示;- 没有定义
__str__()时,str(obj)通常退回使用__repr__()。
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Point(x={self.x!r}, y={self.y!r})"
def __str__(self) -> str:
return f"({self.x}, {self.y})"
point = Point(1, 2)
print(repr(point)) # Point(x=1, y=2)
print(str(point)) # (1, 2)
print(point) # (1, 2)
__eq__() 定义值相等
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
first = Point(1, 2)
second = Point(1, 2)
print(first is second) # False
print(first == second) # True
NotImplemented 表示当前类型不知道如何与对方比较,让 Python 尝试其他比较方式。它不是布尔值 False。
实现 __eq__() 后,如果没有适配的 __hash__(),实例通常不可哈希,不能作为集合元素或字典键。只有当对象的相等性和哈希值都能保持稳定时,才应实现 __hash__()。
其他常见特殊方法
| 语法或函数 | 特殊方法 | 作用 |
|---|---|---|
len(value) | __len__() | 提供长度 |
value[key] | __getitem__() | 支持索引或键访问 |
for item in value | __iter__() | 支持迭代 |
value + other | __add__() | 定义加法 |
with value | __enter__()、__exit__() | 支持上下文管理 |
特殊方法应符合 Python 用户的直觉。例如 __len__() 应返回非负整数,迭代器耗尽时应抛出 StopIteration。
继承和方法覆盖
继承让子类在父类基础上扩展。Admin 是一种特殊的 User:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def introduce(self) -> str:
return f"我叫{self.name},今年{self.age}岁。"
class Admin(User):
def __init__(self, name: str, age: int, level: int = 1):
super().__init__(name, age)
self.level = level
def introduce(self) -> str:
return f"{super().introduce()} 我的管理员等级是 {self.level}。"
admin = Admin("管理员", 30, 2)
print(admin.introduce())
print(isinstance(admin, Admin)) # True
print(isinstance(admin, User)) # True
print(issubclass(Admin, User)) # True
Admin(User)表示Admin继承User;super().__init__(...)复用父类初始化逻辑;- 子类重新定义
introduce(),称为方法覆盖; super().introduce()调用方法解析顺序中的下一个实现。
在简单单继承中,super() 通常会找到父类;在多继承中,它遵循方法解析顺序(MRO),不是写死某个父类的快捷语法。
继承适合稳定的“是一种(is-a)”关系。如果一个对象只是“拥有另一个对象(has-a)”,组合通常更清晰:
class Order:
def __init__(self, customer: User):
self.customer = customer
Order 拥有一个 User,但不是一种 User,因此不应该为了复用少量代码而强行继承。
数据类:以保存数据为主的类
主要保存数据时,可以用标准库的 dataclass 自动生成 __init__()、__repr__() 和 __eq__() 等方法:
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float
tags: list[str] = field(default_factory=list)
first = Product("Python 入门", 49.9)
second = Product("Python 入门", 49.9)
print(first) # Product(name='Python 入门', price=49.9, tags=[])
print(first == second) # True,默认按字段比较
first.tags.append("编程")
print(second.tags) # [],每个实例有独立列表
field(default_factory=list) 会在每次创建实例时生成新列表,避免意外共享可变数据。数据类仍然是普通类,可以添加方法和属性:
@dataclass
class Rectangle:
width: float
height: float
def area(self) -> float:
return self.width * self.height
类型标注主要表达意图,Python 默认不会在运行时自动检查传入值。需要严格校验时,可以在 __post_init__() 中主动检查。
引用、复制和生命周期
赋值不会复制实例
class Profile:
def __init__(self, name: str):
self.name = name
profile = Profile("小林")
alias = profile
alias.name = "小周"
print(profile.name) # 小周,两个名称指向同一个实例
需要复制时,可以使用 copy 模块,但要区分浅复制和深复制:
import copy
class Box:
def __init__(self, items: list[str]):
self.items = items
original = Box(["a"])
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print(shallow is original) # False
print(shallow.items is original.items) # True,共享内部列表
print(deep.items is original.items) # False,递归复制
深复制成本可能较高,也不适用于所有资源对象。类需要支持复制时,应明确说明内部数据是否共享。
删除名称不等于立即销毁对象
profile = Profile("小林")
alias = profile
del profile
print(alias.name) # 小林,对象仍被 alias 引用
对象不再被引用后,会由垃圾回收机制处理。文件、锁和网络连接等资源不应依赖 __del__() 及时释放,应使用 with 上下文管理器或显式的 close()。
__slots__ 限制属性(进阶)
class Pixel:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int):
self.x = x
self.y = y
pixel = Pixel(10, 20)
# pixel.color = "red" # AttributeError
__slots__ 可以限制普通动态属性,并可能减少大量实例的内存开销,但会影响继承、弱引用和序列化等行为。没有明确需求时,不必为了“更严格”而使用它;它也不是安全机制。
常见错误
忘记 self
实例方法需要用第一个参数接收当前实例,并通过 self.attribute 访问其状态。
把可变类属性当成实例属性
类体中的列表和字典默认被所有实例共享。需要每个实例独立拥有时,应在 __init__() 中创建。
在 __init__() 中返回值
初始化方法只设置状态。需要不同创建方式时,可以使用 @classmethod 编写替代构造方法。
忘记父类初始化
子类覆盖 __init__() 后,父类初始化不会自动执行。需要父类状态时,应调用 super().__init__(...)。
用 is 比较内容
is 判断身份,== 比较内容。判断空值使用 is None。
暴露内部可变数据
直接返回内部列表会让调用方绕过类的校验。必要时可以返回副本:
class Playlist:
def __init__(self, songs: list[str]):
self._songs = list(songs)
@property
def songs(self) -> list[str]:
return list(self._songs)
返回副本会带来时间和内存成本,应结合接口需求选择,但不要无意中泄露内部状态。
设计类时的实用原则
- 类是否代表一个清晰的概念?几行一次性逻辑可能更适合函数;
- 实例创建后是否立即有效?必要属性和校验应在初始化阶段完成;
- 哪些状态属于实例,哪些状态确实需要共享?
- 调用方需要执行哪些行为?优先提供有意义的方法;
- 关系是“是一种”还是“拥有一个”?前者考虑继承,后者考虑组合;
- 类是否承担太多职责?大类通常可以拆分成更容易测试的小类;
- 对象主要记录数据,还是维护复杂行为和约束?前者可以考虑数据类。
类不是越多越好。面向对象编程的价值,是把相关状态、行为和约束组织成容易理解的边界,而不是把每个函数都包进类。
练习
练习一:购物车
定义 ShoppingCart 类:
__init__()创建空的商品列表;add(name, price, quantity=1)添加商品,并拒绝非正价格或数量;total()返回总价;remove(name)删除指定商品;- 两个购物车实例不能共享商品列表。
练习二:圆和矩形
分别定义 Circle 和 Rectangle 类:
- 初始化时校验尺寸不能为负数;
- 提供
area()方法; - 实现清晰的
__repr__(); - 尝试用
@property保护尺寸修改。
练习三:从文本创建用户
为 User 增加 from_text() 类方法,解析 "小林|18",并让 Admin 子类继承后仍能创建 Admin 实例。思考为什么应该返回 cls(...),而不是写死 User(...)。
小结
- 类描述一类对象的结构和行为,实例是类创建出的具体对象;
__init__()初始化实例,实例方法通过self访问实例状态;- 实例属性属于单个对象,类属性由类及其实例共享;
@classmethod接收cls,@staticmethod不接收隐式对象参数;- Python 的封装主要依靠命名约定和
@property,双下划线改名不是安全机制; - 特殊方法让自定义对象接入打印、比较、迭代等 Python 协议;
is比较身份,==比较内容,isinstance()可以处理继承;- 继承表达“是一种”关系,组合表达“拥有一个”关系;
- 主要保存数据时可以使用数据类,并注意可变默认值;
- 设计类时要明确有效状态、共享范围和公开接口。