Python 的类中有三种常用的内置装饰器,它们改变的是方法的调用方式,而不是方法本身的功能。很多初学者把这三者混为一谈,其实它们解决的问题截然不同:
@property 让你把方法当属性访问,不用加括号;
@classmethod 让方法绑定到类而不是实例,第一个参数是 cls;
@staticmethod 让方法既不绑定实例也不绑定类,纯粹是挂在类名下的普通函数。
理解它们的区别,才能写出接口干净、职责清晰的类。本章会逐个拆解,并给出贴近实战的代码。
假设你有一个 User 类,最初用 age 作为普通属性:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age后来需求变了:年龄不能是负数,而且需要支持 "虚岁" 展示。如果你直接改 age 为方法,所有调用 user.age 的地方都要改成 user.age(),改动量巨大。@property 就是解决这个矛盾的:对外保持 user.age 的写法,内部用方法控制逻辑。
class User:
def __init__(self, name: str, age: int):
self.name = name
self._age = age # 内部用私有属性存储
@property
def age(self) -> int:
"""只读属性:获取年龄"""
return self._age
@property
def virtual_age(self) -> int:
"""虚岁:周岁 + 1"""
return self._age + 1
# 使用
u = User("Alice", 25)
print(u.age) # 25 —— 不用括号,像属性一样访问
print(u.virtual_age) # 26
# u.age = 30 # AttributeError: can't set attribute@property 默认只读。如果需要允许修改,用同名的 @age.setter 装饰器:
class User:
def __init__(self, name: str, age: int):
self._name = name
self._age = None
self.age = age # 触发 setter 做校验
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, value: int):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError("年龄超出合理范围")
self._age = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
if not value or not isinstance(value, str):
raise ValueError("姓名不能为空")
self._name = value.strip()
@age.deleter
def age(self):
print("年龄属性被删除")
self._age = None
# 使用
u = User("Bob", 20)
print(u.age) # 20
u.age = 30 # 通过 setter 修改
print(u.age) # 30
try:
u.age = -5 # ValueError: 年龄超出合理范围
except ValueError as e:
print(f"拦截错误: {e}")
del u.age # 触发 deleter
print(u.age) # None运行代码,输出:
20
30
拦截错误: 年龄超出合理范围
年龄属性被删除
None@xxx.setter 和 @xxx.deleter 的命名有严格要求:必须是已经定义了 @property 的同名方法,装饰器名称必须是 属性名.setter 和 属性名.deleter。如果写错名字,Python 不会报错,但你会得到两个独立的方法而不是属性控制。
对于计算耗时的属性,如果结果不变,可以缓存起来避免重复计算。Python 3.8+ 提供了 functools.cached_property:
from functools import cached_property
class Circle:
def __init__(self, radius: float):
self.radius = radius
@cached_property
def area(self) -> float:
"""计算圆的面积"""
print("[计算面积] 只执行一次")
import math
return math.pi * self.radius ** 2
@property
def perimeter(self) -> float:
"""计算圆的周长"""
print("[计算周长] 每次访问都会执行")
import math
return 2 * math.pi * self.radius
c = Circle(5.0)
print(c.area) # [计算面积] 只执行一次 → 78.54...
print(c.area) # 78.54... —— 直接从缓存读取,不重新计算
print(c.perimeter) # [计算周长] 每次访问都会执行 → 31.41...
print(c.perimeter)
c.radius = 10.0
# cached_property 不会自动更新!需要手动清理缓存
del c.area # 删除缓存,下次访问重新计算
print(c.area) # [计算面积] 只执行一次 → 314.15...运行代码,输出:
[计算面积] 只执行一次
78.53981633974483
78.53981633974483
[计算周长] 每次访问都会执行
31.41592653589793
[计算周长] 每次访问都会执行
31.41592653589793
[计算面积] 只执行一次
314.1592653589793cached_property 和 property 的区别:
property 每次访问都执行方法;
cached_property 只执行一次,结果存入实例字典。但如果依赖的属性变了,缓存不会自动失效,需要手动 del 清理。
注意:对于需要自动失效的复杂场景,可以用 functools.lru_cache 配合自定义逻辑,或者第三方库如 methodtools。
@classmethod 把方法的第一个参数绑定为类本身(cls),而不是实例(self)。它可以在不创建实例的情况下调用,常用于工厂方法、替代构造函数。
例如:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name: str, year: int):
"""根据出生年份创建用户"""
from datetime import date
age = date.today().year - year
return cls(name, age)
@classmethod
def anonymous(cls):
"""创建匿名用户"""
return cls("Anonymous", 0)
def __repr__(self):
return f"User({self.name!r}, {self.age})"
# 使用
u1 = User("Alice", 25)
u2 = User.from_birth_year("Bob", 1995)
u3 = User.anonymous()
print(u1) # User('Alice', 25)
print(u2) # User('Bob', 30) —— 结果随当前年份变化
print(u3) # User('Anonymous', 0)classmethod 的 cls 参数在继承时非常有用。子类调用父类的类方法,返回的是子类实例,而不是父类实例。这是工厂模式能正确工作的关键。
例如:
class Animal:
"""动物基类"""
def __init__(self, name: str):
self.name = name
@classmethod
def from_dict(cls, data: dict):
"""从字典创建实例,子类自动获得此能力"""
return cls(data["name"])
def speak(self):
raise NotImplementedError
class Dog(Animal):
"""狗类,继承自 Animal"""
def __init__(self, name: str, breed: str = "Unknown"):
super().__init__(name)
self.breed = breed
@classmethod
def from_dict(cls, data: dict):
"""重写以支持额外字段"""
return cls(data["name"], data.get("breed", "Unknown"))
def speak(self):
return f"{self.name} says: Woof!"
class Cat(Animal):
"""猫类,继承自 Animal"""
def speak(self):
return f"{self.name} says: Meow!"
# 父类方法返回 Dog 实例,因为 cls 是 Dog
dog = Dog.from_dict({"name": "Buddy", "breed": "Golden"})
print(type(dog)) # <class '__main__.Dog'>
print(dog.speak()) # Buddy says: Woof!
# 如果 Cat 没重写 from_dict,它仍然继承父类版本,但 cls 是 Cat
cat = Cat.from_dict({"name": "Kitty"})
print(type(cat)) # <class '__main__.Cat'>
print(cat.speak()) # Kitty says: Meow!如果 from_dict 是普通方法或静态方法,你就无法优雅地返回子类实例。cls 的自动绑定让继承体系中的工厂方法天然正确。
类方法也常用于全局注册表模式,比如插件系统:
class Plugin:
# 全局插件注册表:key=插件类名,value=对应的插件类
_registry = {}
def __init_subclass__(cls, **kwargs):
"""
钩子方法:Python 在定义当前类的**子类**时会自动触发执行
无需手动调用,只要写 class XXX(Plugin): 就会自动进入该方法
:param cls: 刚刚定义完成的子类本身(例如 TextPlugin、HtmlPlugin)
:param kwargs: 定义子类时传入的关键字参数,原样透传给父类的 __init_subclass__
"""
# 调用 object 原生的 __init_subclass__,保证继承体系钩子逻辑完整
super().__init_subclass__(**kwargs)
# 把当前子类注册进注册表,键为子类的类名字符串
# 这里不要使用 self.register(cls.__name__)
cls.register(cls.__name__)
@classmethod
def register(cls, name: str):
"""
类方法:将插件类存入全局注册表
:param name: 用于索引插件的标识名称
"""
# 以插件名称为键,插件类本身为值存入静态注册表
cls._registry[name] = cls
@classmethod
def create(cls, name: str, *args, **kwargs):
"""
工厂类方法:根据插件名称从注册表取出对应类并实例化
:param name: 注册时使用的插件名称
:param args: 实例化插件时的位置参数
:param kwargs: 实例化插件时的关键字参数
:return: 目标插件类的实例对象
"""
# 从注册表查找指定名称的插件类
plugin_cls = cls._registry.get(name)
if not plugin_cls:
raise ValueError(f"Unknown plugin: {name}")
return plugin_cls(*args, **kwargs)
class TextPlugin(Plugin):
"""文本处理插件,继承Plugin后定义阶段自动注册"""
def process(self, text: str) -> str:
# 将输入文本全部转为大写后返回
return text.upper()
class HtmlPlugin(Plugin):
"""HTML处理插件,继承Plugin后定义阶段自动注册"""
def process(self, text: str) -> str:
# 把文本包裹在p标签中,简单封装成HTML段落
return f"<p>{text}</p>"
# 调用工厂方法,根据名称取出TextPlugin类并实例化
plugin = Plugin.create("TextPlugin")
# 调用插件业务处理方法
print(plugin.process("hello")) # HELLO注意,为什么不能将 cls.register(cls.__name__) 改为 self.register(cls.__name__),是因为此刻根本不存在实例 self:
self 指代类的实例对象,必须执行 TextPlugin() 创建出实例之后才会有 self;
__init_subclass__ 运行时机:只是把类定义好了,还没有创建任何插件实例,全程只有类本身,没有实例;
没有实例,自然拿不出 self,强行写 self.register() 会直接报错未定义。
@staticmethod 既不接收 self 也不接收 cls,它纯粹是把一个函数挂在类名下,起到命名空间的作用。如果某个功能与类有关,但不需要访问实例或类的任何状态,就用它。
例如:编写一个日期工具类,在 Java 中工具类中的方法使用 public static 修饰,这里类似。
class DateUtil:
"""日期工具类,提供与日期相关的静态方法"""
@staticmethod
def is_leap_year(year: int) -> bool:
"""判断闰年,与具体实例无关"""
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
@staticmethod
def days_in_month(year: int, month: int) -> int:
"""返回指定年份和月份的天数"""
if month == 2:
return 29 if DateUtil.is_leap_year(year) else 28
if month in (4, 6, 9, 11):
return 30
return 31
@staticmethod
def format_date(year: int, month: int, day: int) -> str:
"""格式化日期为字符串"""
return f"{year:04d}-{month:02d}-{day:02d}"
# 调用方式:类名.方法名,不需要实例
print(DateUtil.is_leap_year(2024)) # True
print(DateUtil.days_in_month(2024, 2)) # 29
print(DateUtil.format_date(2024, 3, 15)) # 2024-03-15DateUtil 里的方法完全可以写成模块级函数,但放在类里有几个好处:
逻辑归拢:所有日期相关工具都在 DateUtil 命名空间下;
可继承:子类可以重写或扩展这些静态方法;
可导入:从类导入比从模块导入一堆函数更清晰。
下面通过一个例子体验模块级函数和 staticmethod 的区别:
# 模块级函数
import os
def is_valid_filename(name: str) -> bool:
return os.path.sep not in name and name.strip() != ""
# 静态方法
class FileValidator:
@staticmethod
def is_valid_filename(name: str) -> bool:
import os
return os.path.sep not in name and name.strip() != ""
# 两者功能完全一样,但后者归类更明确
print(FileValidator.is_valid_filename("test.txt")) # True
print(FileValidator.is_valid_filename("test.txt")) # False如果工具函数与某个类强相关(比如只有 User 类才会用到年龄校验),放在类里作为 staticmethod 更合理。如果完全通用,模块级函数更简单。
通过实例压实在类方法、属性方法和普通方法中如何调用静态方法:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@staticmethod
def validate_dimension(value: float) -> float:
"""验证尺寸是否为正数"""
if value <= 0:
raise ValueError("尺寸必须为正数")
return float(value)
@classmethod
def create(cls, width: float, height: float):
# 类方法里可以调用静态方法
w = cls.validate_dimension(width)
h = cls.validate_dimension(height)
return cls(w, h)
@property
def area(self) -> float:
# 属性方法中调用静态方法
self.validate_dimension(self.width)
self.validate_dimension(self.height)
return self.width * self.height
def perimeter(self) -> float:
# 普通方法中调用静态方法
self.validate_dimension(self.width)
self.validate_dimension(self.height)
return 2 * (self.width + self.height)
r = Rectangle.create(3, 4)
print(r.area) # 12
print(r.perimeter()) # 14这个例子展示了三者的配合:
validate_dimension 是静态方法,纯校验逻辑,不依赖实例或类。
create 是类方法,用 cls 构造实例,内部调用静态方法做校验。
area 是 property,对外像属性,内部是计算。
perimeter 是普通方法,对外提供功能。
| 特性 | 实例方法 | @classmethod | @staticmethod | @property |
| 第一个参数 | self(实例) | cls(类) | 无 | self(实例) |
| 访问方式 | obj.method() | Class.method() 或 obj.method() | Class.method() 或 obj.method() | obj.attr(无括号) |
| 能否访问实例属性 | 是 | 可以(通过实例) | 否 | 是 |
| 能否访问类属性 | 是 | 是 | 可以(通过类名) | 是 |
| 典型用途 | 实例业务逻辑 | 工厂方法、替代构造 | 工具函数、命名空间 | 封装计算、校验、缓存 |
| 继承时行为 | 绑定到实例类型 | cls 指向调用时的类 | 不绑定,可被子类重写 | 绑定到实例类型 |
class BadUser:
def __init__(self, name):
# 由于存在 @property 修饰 name 属性
# 这里的 name 只是对外的访问入口,并不是真正存数据的变量
self.name = name
# 获取name值时,执行这里,从 _name 取值
@property
def name(self):
return self._name
# 设置name值时,执行这里
# 函数内部的 self.name 会再次触发,形成递归调用
@name.setter
def name(self, value):
self.name = value # 错误!递归调用 setter,导致 RecursionError
b = BadUser("Alice")
b.name = "Bob" # 这里将抛出 RecursionError 错误正确做法是内部存储用 _name(单下划线约定),setter 给 _name 赋值,而不是 self.name。
# 设置name值时,执行这里,存储到 _name 中
@name.setter
def name(self, value):
self._name = value.strip() # 正确总结 name、_name、__name 和 __name__ 几种形式的含义:
| 写法 | 约束级别 | 意义 |
| name | 无约束 | 公开属性,放心外部调用。访问方式:实例.name |
| _name | 人为约定 | 内部私有,语法允许访问,靠自觉不碰。访问方式:实例._name |
| __name | 名称改写 | 防止子类重名覆盖,尽量外部也别碰。访问方式:实例. _ 类名 __name注意:Python 会自动把名字改写“_类名__name”。 |
| __name__ | 系统内置 | Python 自带魔法属性 / 方法。 |
property 本身是一个类,不是魔法。它的构造函数是 property(fget=None, fset=None, fdel=None, doc=None)。你甚至可以不用 @ 语法,手动创建:
class ManualProp:
def __init__(self, value):
# 单下划线 _value:真实存储数据的底层实例变量,约定类内部使用,外部不建议直接操作
self._value = value
def _get_value(self):
"""取值回调函数:读取 m.value 时自动执行该方法"""
return self._value
def _set_value(self, v):
"""赋值回调函数:执行 m.value = xxx 时自动执行该方法"""
self._value = v
def _del_value(self):
"""删除回调函数:执行 del m.value 时自动执行该方法"""
del self._value
# 手动实例化 property 对象,绑定读写删回调 + 属性文档说明
# property(getter, setter, deleter, docstring) 参数顺序:
# 1. _get_value:获取属性时触发的方法
# 2. _set_value:给属性赋值时触发的方法
# 3. _del_value:del 删除该属性时触发的方法
# 4. "The value property":该属性的文档字符串,help(ManualProp.value) 可以查看
# 整体效果等价于装饰器写法 @property + @value.setter + @value.deleter
value = property(_get_value, _set_value, _del_value, "The value property")
# 实例化对象,初始化数据存入底层 self._value
m = ManualProp(10)
# 读取 m.value → 自动调用 _get_value(),返回 self._value
print(m.value) # 10
# 给 m.value 赋值 → 自动调用 _set_value(20),把20存入 self._value
m.value = 20
# 再次读取,依旧走取值方法
print(m.value) # 20这个知识在写高级元编程时有用,但日常代码中 @ 语法更直观。它揭示了装饰器的本质:@property 就是把方法包装成 property 类的实例,然后赋值给同名属性。
那么,下面这行代码有什么用?
value = property (_get_value, _set_value, _del_value, "The value property")把类属性 value 包装成托管属性,以后执行 obj.value、obj.value = x、del obj.value 时,不再直接读写实例字典里的字段,而是自动分别调用你指定的取值、赋值、删除函数,实现对属性读写的拦截与管控。
对比原生普通属性,看出它解决了什么问题?
普通属性写法(无管控)
class Test:
def __init__(self):
self.value = 10
t = Test()
t.value = "随便塞字符串" # 想赋值什么就赋值什么,无法拦截外部可以肆无忌惮读写、篡改,你没办法在赋值时加校验逻辑。
用 property 托管之后,底层真实数据存在 self._value,外部碰不到底层变量,所有操作必须走你写的函数:
def _set_value(self, v):
# 举例:强制只能赋值数字
if not isinstance(v, int):
raise TypeError("只能传入整数")
self._value = v时外部执行 m.value = "abc" 会直接报错,实现数据校验。
其实,下面两种写法本质完全等价:
写法 A:你现在这种手动绑定 property
value = property(_get_value, _set_value, _del_value, "The value property")写法 B:日常更常用的 @property 装饰器
@property
def value(self):
return self._value
@value.setter
def value(self, v):
self._value = v
@value.deleter
def value(self):
del self._value装饰器语法只是语法糖,底层最后依然是调用 property() 构建属性对象,二者运行机制一模一样。
装饰器是 Python 的语法糖,但糖吃多了会腻。判断一个方法该用 @property 还是普通方法,看它是否需要参数;判断该用 @classmethod 还是 @staticmethod,看它内部是否需要拿到类本身 cls。
保持简单,是最高级的工程技巧。