文章内容

2025/9/22 23:50:26,作 者: 黄兵

在 Python 中理解 @property / @<prop>.setter(详解 + 示例)

1) 基本概念(一句话版)

@property 把一个方法变成像属性一样访问的“访问器”。配合 @<prop>.setter 可以实现“读写受控的属性”。
它是 Python 的 描述符(descriptor) 的高阶用法。

2) 最简单的例子

class User:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @property
    def full_name(self):
        # 只读属性,像访问属性,但其实执行方法
        return f"{self.first} {self.last}"

    @full_name.setter
    def full_name(self, value):
        # 写入时会走这里:把传入字符串分解为 first/last
        first, last = value.split(" ", 1)
        self.first = first
        self.last = last

u = User("John", "Doe")
print(u.full_name)   # -> "John Doe"   (调用 getter)
u.full_name = "Jane Smith"  # -> 调用 setter
  • @property(getter)让 obj.full_name 调用函数结果而不是直接读字段。

  • @full_name.setter 定义写入逻辑:obj.full_name = "..." 会调用 setter。

  • 3) property 的实现原理(重要细节)

    • property 是描述符(descriptor)。它在类上是一个对象:property(fget,fset,fdel,doc)

    • 数据描述符(data descriptor):如果 propertyfset(setter)或 fdel,它就是数据描述符,优先于实例字典(__dict__)。也就是说,读取/写入都会走描述符的方法。

    • 非数据描述符(non-data descriptor):如果只有 fget(只有 getter,没有 setter),它就是非数据描述符,实例字典中的同名键会覆盖它(赋值给实例会创建实例属性,遮住 property)。
      这点解释了为什么要提供 setter 来阻止外部直接覆盖属性。

    4) 常见用法与模式

    a) 只读属性

    只定义 getter,不定义 setter — 外部无法用 obj.prop = ... 改它(但注意:如果没有 setter,赋值会在实例字典创建一个同名普通属性,遮住 property)。

    class C:
        @property
        def now(self):
            return datetime.utcnow()
    

    b) 有验证的写入

    在 setter 中做校验或转换:

    class Person:
        @property
        def age(self):
            return self._age
    
        @age.setter
        def age(self, value):
            if value < 0:
                raise ValueError("age must be >= 0")
            self._age = int(value)

    c) 延迟计算 & 缓存

    使用 functools.cached_property(Python 3.8+)或内部缓存 _cached_value

    d) 可删除属性

    @prop.deleter 定义 del obj.prop 的行为。

    5) 在 ORM / SQLAlchemy 中用 @property:要注意的几个坑

    1. 不要把同名的 db.Column@property 放在同一个名字上

      • 如果你在类上同时声明了 secret_key = db.Column(...) @property def secret_key(...),会造成冲突(SQLAlchemy 会把 Column 映射为类属性或 descriptor,与 property 冲突)。

      • 解决办法:数据库字段使用不同的名字(常见:secret_key_encrypted_secret_key_encrypted),对外暴露 secret_key 属性。

    2. @property 只在 Python 层生效,不会被 SQL 查询引擎理解

      • user.secret_key(property)是 Python 层的“计算属性”,不能用于 .filter(User.secret_key == 'x')。如果你需要在查询中过滤某种“属性”,考虑用 SQLAlchemy 的 hybrid_property

    3. 迁移 / Alembic

      • property 不会生成数据库列,迁移脚本只根据 db.Column 生成。确保数据库列定义完备用于迁移。

    分享到:

    发表评论

    评论列表