Python 内置类方法装饰器:@classmethod、@staticmethod、@property 到底怎么用?

一篇帮你彻底搞懂 Python 类里三个最常用装饰器的文章

写 Python 类的时候,你是不是经常看到这几个装饰器,却总是分不清它们到底有什么区别、什么时候该用哪个?

  • @classmethod
  • @staticmethod
  • @property

它们看起来都是加在方法上面的「帽子」,但背后的行为却大不相同。今天这篇文章,就带你一次性把它们搞清楚。


一、先看一张对比表

装饰器 第一个参数 调用方式 核心用途
@classmethod cls(类本身) 类或实例 工厂方法,属于整个类型的行为
@staticmethod 类或实例 与状态无关的方法
@property self(实例) 像属性一样访问 把方法变成属性,控制读/写/删

下面我们一个一个拆开看。


二、@classmethod:属于「类」而不是「实例」的方法

普通方法的痛点

先看一个普通的鸭子类:

1
2
3
4
5
6
class Duck:
def __init__(self, color):
self.color = color

def quack(self):
print(f"Hi, I'm a {self.color} duck!")

创建一只鸭子,调用它的方法:

1
2
3
>>> d = Duck('yellow')
>>> d.quack()
Hi, I'm a yellow duck!

但如果你不创建实例,直接用类来调用 quack(),就会报错:

1
2
>>> Duck.quack()
TypeError: quack() missing 1 required positional argument: 'self'

原因很简单:普通方法需要 self,也就是一个具体的实例。

用 @classmethod 突破这个限制

如果你想要一个方法,不需要实例就能调用,那就用 @classmethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random

class Duck:
def __init__(self, color):
self.color = color

def quack(self):
print(f"Hi, I'm a {self.color} duck!")

@classmethod
def create_random(cls):
"""创建一只随机颜色的鸭子"""
color = random.choice(['yellow', 'white', 'gray'])
return cls(color=color)

注意两点:

  1. 第一个参数是 cls,代表类本身,而不是 self
  2. 通过 cls(color=color) 来创建实例。

调用效果:

1
2
3
>>> d = Duck.create_random()
>>> d.quack()
Hi, I'm a white duck!

甚至可以用实例来调用类方法,效果一样:

1
2
>>> d.create_random()
<__main__.Duck object at 0x10f8f2f40>

什么时候用 @classmethod?

当你发现某个行为不属于某个具体实例,而是属于整个类型时,就用类方法。

最典型的场景就是工厂方法——像上面的 create_random(),它负责「生产」实例,而不是「操作」某个已有实例。


三、@staticmethod:与状态无关的方法

一个尴尬的方法

假设我们写了一个猫类:

1
2
3
4
5
6
7
8
9
10
11
class Cat:
def __init__(self, name):
self.name = name

def say(self):
sound = self.get_sound()
print(f'{self.name}: {sound}...')

def get_sound(self):
repeats = random.randrange(1, 10)
return ' '.join(['Meow'] * repeats)

get_sound() 这个方法,压根没用到 self 的任何东西。它不依赖 self.name,也不依赖任何实例状态。既然如此,为什么还要把 self 传进去呢?

用 @staticmethod 去掉 self

1
2
3
4
5
6
7
8
9
10
11
12
class Cat:
def __init__(self, name):
self.name = name

def say(self):
sound = self.get_sound()
print(f'{self.name}: {sound}...')

@staticmethod
def get_sound():
repeats = random.randrange(1, 10)
return ' '.join(['Meow'] * repeats)

调用效果:

1
2
3
4
5
6
>>> c = Cat('Jack')
>>> c.say()
Jack: Meow Meow Meow...

>>> Cat.get_sound()
'Meow Meow Meow Meow Meow Meow'

静态方法不接收实例作为第一个位置参数,所以既可以通过实例调用,也可以通过类调用。

静态方法 vs 普通函数:怎么选?

既然静态方法不需要 self,那它跟写在类外面的普通函数有什么区别?答案是:几乎没区别。 那为什么要放在类里?

这里有三个判断依据:

  1. 如果静态方法特别通用、与类关系不大 → 改成普通函数更好;
  2. 如果静态方法与类关系密切 → 用静态方法更好;
  3. 相比普通函数,静态方法有先天优势:它能被子类继承和重写。

所以,把 get_sound() 放在 Cat 类里,是因为它跟「猫叫」这件事强相关;如果某个函数跟类八竿子打不着,那就别硬塞进类里。


四、@property:让方法「伪装」成属性

属性 vs 方法:两种接口

在一个类里,属性和方法的职责不同:

  • 属性代表状态,通过 inst.attr 直接访问;
  • 方法代表行为,通过 inst.method() 调用。

但有时候,我们想要一个「计算出来的属性」——它看起来像属性,实际上背后是一段逻辑。这时候就用 @property

基础用法

1
2
3
4
5
6
7
8
9
10
import os

class FilePath:
def __init__(self, path):
self.path = path

@property
def basename(self):
"""获取文件名"""
return self.path.rsplit(os.sep, 1)[-1]

调用时,就像用普通属性一样:

1
2
3
>>> p = FilePath('/tmp/foo.py')
>>> p.basename
'foo.py'

注意:调用时没有括号basename 看起来是个属性,实际上是个方法。

进阶:控制写入和删除

@property 不只是读取,还支持自定义赋值删除逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FilePath:
def __init__(self, path):
self.path = path

@property
def basename(self):
"""获取文件名"""
return self.path.rsplit(os.sep, 1)[-1]

@basename.setter # 赋值时触发
def basename(self, name):
"""修改当前路径里的文件名部分"""
new_path = self.path.rsplit(os.sep, 1)[:-1] + [name]
self.path = os.sep.join(new_path)

@basename.deleter # 删除时触发
def basename(self):
raise RuntimeError('Can not delete basename!')

效果:

1
2
3
4
5
6
7
>>> p = FilePath('/tmp/foo.py')
>>> p.basename = 'bar.txt' # 触发 setter
>>> p.path
'/tmp/bar.txt'

>>> del p.basename # 触发 deleter
RuntimeError: Can not delete basename!

通过 @property,你可以基于方法定义类属性,精确控制属性的读取、赋值和删除行为,灵活地实现动态属性。

一个重要的提醒

把方法改成属性后,它的使用接口会发生很大的变化。你需要学会判断,方法和属性分别适合什么样的场景

书里举了一个很典型的反例:

假设你的类有个方法叫 get_latest_items(),调用它会请求外部服务的数十个接口,耗费 5~10 秒钟。那么这时,盲目把这个方法改成 .latest_items 属性就不太恰当。

为什么?因为人们在读取属性时,总是期望能迅速拿到结果;而调用方法则不一样——快点儿慢点儿都无所谓。

让自己设计的接口符合他人的使用预期,也是写代码时很重要的一环。


五、一张图总结

1
2
3
4
5
6
7
8
9
10
11
12
类的接口设计

├── 普通方法 ──→ self ──→ 操作实例状态

├── @classmethod ──→ cls ──→ 工厂方法 / 类型行为

├── @staticmethod ──→ 无 ──→ 与状态无关的工具方法

└── @property ──→ self ──→ 像属性一样访问

├── @xxx.setter ──→ 控制赋值
└── @xxx.deleter ──→ 控制删除

六、记忆口诀

最后,送你一句口诀,帮你快速回忆:

@classmethod 管「类型」,@staticmethod 管「无关状态」,@property 管「接口」。

  • 想写工厂方法、操作整个类 → @classmethod
  • 方法不需要 self、纯粹是个工具 → @staticmethod
  • 想让方法伪装成属性、控制读写 → @property

搞懂这三个装饰器,你的 Python 类设计能力就上了一个台阶。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。也欢迎在评论区聊聊:你平时最常用哪个装饰器?有没有踩过 @property 的坑?