@property 到底是怎么把方法伪装成属性的?一文吃透 property、staticmethod、classmethod

「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. 三者的本质区别:绑定方式不同

flowchart TB A[普通实例方法] --> A1[自动绑定self<br/>调用者是实例本身] B[classmethod] --> B1[自动绑定cls<br/>调用者所属的类] C[staticmethod] --> C1[不自动绑定任何东西<br/>就是普通函数]
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 怎么用更少的代码实现同样的效果。

相关推荐
qq_316411031 小时前
AI 情感陪伴智能潮玩软硬件一体化开发案例
人工智能·python
废弃的小码农1 小时前
功能测试--Day07--Python编程基础
开发语言·python
zx1154501 小时前
大模型工具调用次数限制
人工智能·python
MC皮蛋侠客2 小时前
SQLAlchemy 系列(八):AsyncIO、并发与 Web 生命周期——让每个并发任务持有自己的 Session
数据库·python
用户8181870627462 小时前
第21章 JDBC 异常全集与连接池诊断
后端
Java内核笔记2 小时前
告别第三方库!Spring Boot 4 原生 API 版本控制全解析:4 种策略 + 实战案例
java·后端
神奇小汤圆2 小时前
把Spring Boot 4的Native Image玩明白了,启动3秒变50毫秒的踩坑全记录
后端
东方小月2 小时前
从零开发一个 Coding Agent(五):使用 TypeBox 校验工具参数
前端·人工智能·后端
Scene2163 小时前
Agent Harness、Loop 与 Graph:构建生产级 AI Agent 的三大架构支柱
后端