在 Python 面向对象编程中,property 工厂函数是非常常用的工具,它允许我们把一个方法伪装成属性访问,从而在不改变调用方式的情况下加入取值、赋值和删除的逻辑控制。但当我们在大型项目里用 property 工厂创建类属性时,类型检查器和 IDE 常常无法得知这个属性到底是什么类型,这让代码提示和静态分析变得很弱。理解并补上这些类型提示,对提升代码可维护性很有帮助。

为什么 property 工厂会丢失类型信息
property 本身是一个描述符类,调用 property(getter) 时返回的是一个 property 实例,而不是原本函数的返回值类型。类型检查器在默认情况下只能看到这个实例,却看不到 getter 方法上的返回标注,除非我们显式地把类型信息暴露出来。从底层看,描述符协议只定义了 __get__、__set__ 等魔术方法,并没有携带关于“被描述对象业务类型”的元数据。
下面这段示例展示了最常见、但没有类型提示的写法。虽然运行没问题,但你在别处写 obj.name 时,工具链完全不知道它应该是 str。
class User:
def __init__(self, first, last):
self.first = first
self.last = last
def get_full_name(self):
return self.first + ' ' + self.last
full_name = property(get_full_name)
这种写法在动态语言里很灵活,但当你把代码交给 mypy 或交给团队协作时,类型不明确就会变成隐患。接下来我们看几种把类型提示加回去的办法。
方案一:在 getter 上标注返回类型并配合变量注解
最简单且兼容性最好的方式,是给 getter 方法加上 -> 返回类型,同时在类体中用变量注解声明这个 property 的名字。Python 的变量注解不会生成实际赋值,只是给类型检查器看,因此不会覆盖 property 实例。
示例如下,我们在 get_full_name 上写了 -> str,并在类里写 full_name: str,这样 mypy 就知道 full_name 是 str 类型的只读属性。
class User:
full_name: str
def __init__(self, first: str, last: str):
self.first = first
self.last = last
def get_full_name(self) -> str:
return self.first + ' ' + self.last
full_name = property(get_full_name)
这种方案的优点是几乎支持所有 Python 3.6+ 环境,不需要额外依赖。缺点是要写两遍名字,且如果 property 有 setter,还要保证注解和 setter 参数类型一致,否则检查器会报冲突。
方案二:使用 property 作为装饰器并直接注解方法
很多人习惯用 @property 装饰器而不是工厂函数调用,其实原理一样,但代码更紧凑。类型提示只需要写在被装饰的方法上,大多数新版本类型检查器会自动把方法返回类型当成属性类型。
下面代码在 Python 3.7+ 的 mypy 中可以直接识别 user.age 为 int,无需额外变量注解。若你仍用老版本,可像方案一那样补一行 age: int。
class User:
def __init__(self, birth_year: int, now: int):
self.birth_year = birth_year
self.now = now
@property
def age(self) -> int:
return self.now - self.birth_year
如果你坚持用 property 工厂而不是装饰器,也可以把工厂调用包在一个有注解的函数里返回,但可读性通常不如装饰器。此方案让代码更 Pythonic,也更容易被静态工具推断。
方案三:利用 typing 模块强化复杂类型
当 property 返回的是容器、联合类型或自定义泛型时,单纯写 -> list 不够精确。我们可以用 typing.List、typing.Optional 等,在 Python 3.9+ 还能直接用内置泛型。下面的例子里,property 返回一个可能为 None 的用户对象列表。
在继承场景中,子类重写 property 时也要保持返回类型协变,否则 mypy 会认为子类不完整。类型标注让重构更安全。
from typing import List, Optional
class Account:
def __init__(self):
self._logs: List[str] = []
@property
def logs(self) -> List[str]:
return self._logs
class AdminAccount(Account):
@property
def logs(self) -> List[str]:
return ['admin'] + self._logs
如果返回类型可能是 None,就把注解写成 -> Optional[List[str]],调用方就会被强制做空值判断,减少运行时错误。这种写法在业务系统里非常实用。
不同方案的对比与建议
为了直观比较,我们整理了一个简单表格,说明三种方式在可读性、工具支持和适用版本上的差异。
| 方案 | 写法风格 | 类型检查支持 | 推荐场景 |
|---|---|---|---|
| 变量注解 + 工厂 | 分开写注解和 property | Python 3.6+ 稳定 | 老项目改造 |
| 装饰器注解 | @property 直接标返回 | 3.7+ 体验最好 | 新代码默认选 |
| typing 强化 | 复杂泛型标注 | 全版本可用 | 领域模型复杂时 |
总体建议是:新项目直接用装饰器加返回类型;老项目用变量注解补类型;遇到联合、可选、泛型返回时引入 typing。这样既能保持 property 的运行时行为不变,又能让 IDE 和 CI 里的类型检查真正发挥作用,减少因为属性类型不清带来的低级 bug。