「Python 进阶之路」系列 Day05
写在前面
假设你写了一个 Person 类,age 是个普通属性,调用方到处都在写 p.age = 30 这样的赋值。有一天产品说年龄不能是负数,你想加一个校验------但如果把 age 改成 set_age() 方法,之前所有 p.age = xxx 的调用代码都得跟着改,这个成本谁都不想承担。
@property 就是为了解决这个矛盾而生的:让方法在调用形式上完全长得像属性,内部却能悄悄执行任意逻辑 。今天这篇把 @property 连同它的两个"兄弟"------@staticmethod、@classmethod------一次性讲透,这三个是 Python 类里最常见、也最容易被面试官追问细节的内置装饰器。
一、是什么:先复习装饰器本质,再认识这三兄弟
@decorator 只是个语法糖,等价于把函数重新赋值成 decorator(原函数)(Day03 详细讲过 *args/**kwargs 如何在自定义装饰器里转发参数):
python
def upper_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@upper_decorator
def greet(name):
return f"hello {name}"
print(greet("tom")) # HELLO TOM
# 完全等价:
def greet2(name):
return f"hello {name}"
greet2 = upper_decorator(greet2)
print(greet2("jerry")) # HELLO JERRY
而 @property、@staticmethod、@classmethod 是 Python 内置 的三个特殊装饰器,跟"包一层逻辑"的自定义装饰器不是一回事------它们专门用来改变类里定义的方法的绑定方式和调用形态。
二、为什么:三个装饰器分别解决什么问题
@property:解决"想给属性加校验/计算逻辑,又不想破坏调用方obj.attr这种写法"的矛盾@staticmethod:某个函数逻辑上属于这个类(放在类里语义更清晰、调用更方便),但函数体内完全不需要访问实例或类的任何状态@classmethod:需要提供额外的构造方式(工厂方法),并且希望这个构造方式在被子类继承时依然能正确返回子类的实例,而不是写死父类
三、怎么用
1. @property:把方法伪装成属性
python
class Person:
def __init__(self, name, age):
self.name = name
self._age = age # 下划线前缀表示这是内部实现细节
@property
def age(self): # getter:读取 p.age 时调用
return self._age
@age.setter
def age(self, value): # setter:给 p.age 赋值时调用
if value < 0:
raise ValueError("年龄不能为负数")
self._age = value
@age.deleter
def age(self): # deleter:del p.age 时调用
print("age 被删除了")
del self._age
p = Person("Tom", 25)
print(p.age) # 25 ------ 看起来是读属性,实际调用了 getter
p.age = 30 # 看起来是赋值属性,实际调用了 setter
print(p.age) # 30
p.age = -1
# ValueError: 年龄不能为负数
del p.age # 调用 deleter,打印"age 被删除了"
三个装饰器怎么串起来的 :@property 先把 age 方法变成一个 property 对象(此时只有 getter);@age.setter 是这个 property 对象自带的方法,用来注册 setter 函数------注意它用的名字必须和属性名一致,都叫 age,因为它返回的是一个新的、同时包含 getter 和 setter 的 property 对象,重新绑定回类的 age 名字。
2. 只读属性:只定义 getter
python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(2)
print(c.area) # 12.56636 ------ 面积根据半径实时算出来,不需要单独存一份
c.area = 100
# AttributeError: property 'area' of 'Circle' object has no setter
不定义对应的 @x.setter,这个"属性"就是只读的------很适合用来包装"根据其他属性实时计算出来的值",调用方完全不需要知道这背后是函数调用还是简单属性读取。
3. @staticmethod:纯粹的命名空间归类
python
class DateUtil:
@staticmethod
def is_valid_date(year, month, day):
return 1 <= month <= 12 and 1 <= day <= 31
print(DateUtil.is_valid_date(2024, 13, 1)) # False ------ 用类直接调用
print(DateUtil().is_valid_date(2024, 5, 20)) # True ------ 用实例调用效果一样,不会自动传 self
@staticmethod 修饰的方法,本质就是存放在类命名空间里的普通函数------不管用类调用还是实例调用,都不会有任何参数被自动传进去。
4. @classmethod:备用构造函数,感知子类
python
class Person2:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_string(cls, s):
name, age = s.split(",")
return cls(name, int(age)) # 用 cls(...),不写死 Person2(...)
p2 = Person2.from_string("Tom,25")
print(p2.name, p2.age) # Tom 25
关键点:一定要用 cls(...) 而不是硬编码类名 ,因为 cls 在子类调用这个方法时会自动变成子类本身,子类不用重写这个工厂方法也能正确工作:
python
class Base:
@classmethod
def create(cls):
return cls() # 用 cls(),不写死 Base()
def __repr__(self):
return f"<{self.__class__.__name__} instance>"
class Sub(Base):
pass
print(Base.create()) # <Base instance>
print(Sub.create()) # <Sub instance> ------ 如果硬编码成 Base(),这里就会错误地返回 Base 实例
5. 三者的本质区别:绑定方式不同
python
class Demo:
def instance_method(self):
pass
@staticmethod
def static_method():
pass
@classmethod
def class_method(cls):
pass
d = Demo()
print(d.instance_method) # <bound method ...>,绑定了实例 d
print(d.static_method) # <function ...>,没有绑定任何东西,纯粹是个普通函数
print(d.class_method) # <bound method ...>,绑定了类 Demo(不是实例!)
print(Demo.class_method) # 从类直接访问效果一样,同样绑定了 Demo
@property 的实现方式又不一样:它是一个数据描述符 (同时实现了 __get__/__set__),定义在类的命名空间里,不会出现在实例的 __dict__ 里:
python
print(type(Person.__dict__["age"])) # <class 'property'>
print("age" in vars(p)) # False ------ age 不是实例属性,是类级别的描述符
四、面试追问
Q1:@property 的作用是什么?
把一个方法伪装成属性访问的形式:读取这个"属性"时实际调用的是 getter 方法,赋值时调用的是 setter 方法。这样可以在完全不改变调用方 obj.attr/obj.attr = xxx 写法的前提下,给属性访问加入校验逻辑或者实时计算逻辑,是 Python 里实现封装的常见手段。
Q2:怎么用 @property 实现只读属性?
只定义 getter(用 @property 装饰),不定义对应的 @属性名.setter 方法。这样调用方尝试给这个"属性"赋值时,会直接抛出 AttributeError,提示这个 property 没有 setter。
Q3:@staticmethod 和 @classmethod 的区别是什么?
@staticmethod 修饰的方法不会自动绑定任何参数,本质上就是被存放在类命名空间里的一个普通函数,用类调用还是用实例调用都一样,没有隐式传参;@classmethod 修饰的方法调用时会自动把"调用者所属的类"作为第一个参数(约定叫 cls)传进去,不管是用类还是用实例去调用它,第一个参数拿到的永远是类对象本身。
Q4:为什么工厂方法通常用 @classmethod 而不是 @staticmethod?
因为 classmethod 里的 cls 具有继承感知能力------子类调用这个方法时,cls 会自动变成子类本身,用 cls(...) 创建实例,子类不需要重写这个工厂方法也能正确返回子类的实例;如果用 @staticmethod 并且在函数体里硬编码类名去创建实例,子类调用时永远只能拿到父类的实例,这是明显的设计缺陷。
Q5:@property 底层是怎么实现的?
@property 本质是一个数据描述符,实现了 __get__/__set__/__delete__ 这几个描述符协议方法,定义在类的命名空间里,不会出现在实例的 __dict__ 里。当你访问 obj.attr 时,如果类上存在同名的数据描述符,Python 的属性查找机制会优先调用描述符的 __get__ 方法,转发给对应的 getter;赋值、删除同理会转发给 setter、deleter。
下一篇预告
Day06 讲上下文管理器------with 语句背后到底发生了什么,__enter__/__exit__ 这对魔法方法如何配合,以及用 contextlib 怎么用更少的代码实现同样的效果。