引言:一个“错误”的直觉

从初学者的视角切入:我们通常认为 self.name = name 就是把数据存进字典,self.name 就是把数据取出来。但在处理复杂对象(如Django模型、Pydantic、@property)时,这种理解是完全错误的。在Python的高级世界里,.= 只是表象,真正的幕后黑手是 Getter 和 Setter。

第一层洋葱:@property 的伪装

展示一段标准的 @property 代码:

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

@property
def name(self):
return self._name

@name.setter
def name(self, value):
self._name = value

# 看起来像是在访问属性,但实际上是在调用方法
s = Student("Alice")
print(s.name) # 调用 getter
s.name = "Bob" # 调用 setter

当我们访问 s.name 时,看起来像是在访问变量,但实际上是在执行函数。@property 让我们误以为在操作属性,其实是在调用方法。

第二层洋葱:揭开 property 的面纱(描述符协议)

@property 只是一个实现了 get 和 set 方法的类(描述符)。让我们手写一个简单的 MyProperty 类,实现 get 和 set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyProperty:
def __init__(self, fget=None, fset=None):
self.fget = fget
self.fset = fset

def __get__(self, instance, owner):
if instance is None:
return self
if self.fget is None:
raise AttributeError("can't get attribute")
return self.fget(instance)

def __set__(self, instance, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(instance, value)

def setter(self, fset):
return MyProperty(self.fget, fset)

# 使用自定义的 MyProperty
class Student:
def __init__(self, name):
self.name = name

def get_name(self):
return self._name

def set_name(self, value):
self._name = value

name = MyProperty(get_name, set_name)

当我们在类中定义 name = MyProperty() 时,外部的 obj.name 是如何自动触发内部的 get 的?

终极奥义:点号的底层翻译

用伪代码或底层视角展示 Python 解释器是如何“翻译”代码的:

  • 读取时:obj.name -> 查找类字典 -> 发现是描述符 -> 调用 __get__ (即 Getter)。
  • 赋值时:obj.name = 1 -> 查找类字典 -> 发现是描述符 -> 调用 __set__ (即 Setter)。

强调:这就是为什么你不能在 setter 里直接写 self.name = ...,因为那会再次触发 __set__,导致无限递归(死循环)。你必须操作真正的存储介质(如 _name__dict__)。

为什么要这么设计?(升华主题)

这种机制带来的巨大优势:接口与实现的解耦。你可以把“存储逻辑”(存数据库、写文件、计算)隐藏在“访问语法”(点号)之下,调用者完全无感知。这也是 ORM(如SQLAlchemy)和 数据验证库(如Pydantic)能够存在的基石。

总结

从简单的赋值到复杂的描述符,Python的属性访问机制远比表面看起来复杂。下次写 self.x = y 时,想一想,这背后是否藏着一个 __set__ 在等着你?

Python的这种设计,让代码既简洁直观,又能灵活应对复杂场景,体现了其“优雅”的设计哲学。