跳到主要内容

类和实例

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()操作实例数据的函数
selfself.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(...) 时,可以简化理解为以下过程:

  1. __new__() 创建并返回实例;
  2. Python 调用实例的 __init__() 设置初始状态;
  3. 类调用表达式得到初始化完成的实例。

严格来说,__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)

返回副本会带来时间和内存成本,应结合接口需求选择,但不要无意中泄露内部状态。

设计类时的实用原则​

  1. 类是否代表一个清晰的概念?几行一次性逻辑可能更适合函数;
  2. 实例创建后是否立即有效?必要属性和校验应在初始化阶段完成;
  3. 哪些状态属于实例,哪些状态确实需要共享?
  4. 调用方需要执行哪些行为?优先提供有意义的方法;
  5. 关系是“是一种”还是“拥有一个”?前者考虑继承,后者考虑组合;
  6. 类是否承担太多职责?大类通常可以拆分成更容易测试的小类;
  7. 对象主要记录数据,还是维护复杂行为和约束?前者可以考虑数据类。

类不是越多越好。面向对象编程的价值,是把相关状态、行为和约束组织成容易理解的边界,而不是把每个函数都包进类。

练习​

练习一:购物车​

定义 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() 可以处理继承;
  • 继承表达“是一种”关系,组合表达“拥有一个”关系;
  • 主要保存数据时可以使用数据类,并注意可变默认值;
  • 设计类时要明确有效状态、共享范围和公开接口。