0Pricing
Python Academy · 课时

描述器:__get__、__set__、__delete__

实现描述器协议,控制属性访问。

描述器:__get__、__set__、__delete__ 是 CoddyKit 上的免费 Python Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Python Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Python Academy 课程共包含 4 节课。

什么是描述符?

描述符是定义了 __get__、__set__ 或 __delete__,并被赋值为类属性的对象。Python 会在属性访问时调用这些方法。

class Descriptor:
    def __get__(self, obj, objtype=None):
        print(f"__get__ called, obj={obj}")
        return 42

class MyClass:
    attr = Descriptor()

print(MyClass().attr)   # __get__ called ... 42

数据描述符与非数据描述符

数据描述符定义了 __set__ 或 __delete__,其优先级高于实例的 __dict__。非数据描述符(只有 __get__)则让位于 __dict__。

class DataDesc:
    def __get__(self, obj, t): return "data"
    def __set__(self, obj, val): pass

class NonDataDesc:
    def __get__(self, obj, t): return "nondata"

class C:
    d = DataDesc()
    n = NonDataDesc()

c = C()
c.__dict__["n"] = "instance"  # shadows the non-data desc
c.__dict__["d"] = "instance"  # does NOT shadow data desc
print(c.n)   # instance
print(c.d)   # data

__get__ 签名

__get__(self, obj, objtype):obj 是实例(在类上访问时为 None);objtype 是类。

class Verbose:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self   # accessed on class
        return f"value for {obj!r}"

class Widget:
    label = Verbose()

print(Widget.label)        # <Verbose object>
print(Widget().label)      # value for <Widget object>

__set__ 与验证

实现 __set__(self, obj, value),即可在存储值之前对其进行验证或转换。

class PositiveInt:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, t):
        return obj.__dict__.get(self.name)

    def __set__(self, obj, val):
        if not isinstance(val, int) or val <= 0:
            raise ValueError(f"{self.name} must be a positive int")
        obj.__dict__[self.name] = val

class Rect:
    width  = PositiveInt()
    height = PositiveInt()

r = Rect()
r.width = 10
# r.width = -1  # ValueError

__set_name__

当描述符被赋值给类时,Python 3.6+ 会在描述符上调用 __set_name__(owner, name),使其能够访问自身的属性名称。

class Typed:
    def __set_name__(self, owner, name):
        self.public  = name
        self.private = "_" + name

    def __get__(self, obj, t):
        return None if obj is None else getattr(obj, self.private, None)

    def __set__(self, obj, val):
        setattr(obj, self.private, val)

class User:
    name = Typed()

u = User()
u.name = "Alice"
print(u.name)     # Alice

用于移除属性的 __delete__

定义 __delete__(self, obj),即可拦截 del obj.attr。

class Protected:
    def __set_name__(self, owner, name): self.name = name
    def __get__(self, obj, t):
        return obj.__dict__.get(self.name)
    def __set__(self, obj, v): obj.__dict__[self.name] = v
    def __delete__(self, obj):
        raise AttributeError(f"Cannot delete {self.name}")

class Config:
    host = Protected()

c = Config(); c.host = "localhost"
# del c.host   # AttributeError

函数是非数据描述符

函数实现 __get__,在实例上访问时返回绑定方法。这就是 Python 实现方法绑定的方式。

def greet(self):
    return f"Hello from {self}"

class C: pass
C.greet = greet

c = C()
print(c.greet())    # Hello from <C object>

classmethod 和 staticmethod 是描述符

classmethod 和 staticmethod 是内置描述符类,它们包装函数并改变 __get__ 的返回值。

class MyClass:
    @classmethod
    def from_string(cls, s):
        return cls()

# classmethod.__get__ returns a bound method with cls
# staticmethod.__get__ returns the plain function

惰性属性描述符

构建一个描述符,使其只计算一次值,并按实例对值进行缓存。

class LazyAttr:
    def __init__(self, func): self.func = func
    def __set_name__(self, owner, name): self.name = name
    def __get__(self, obj, t):
        if obj is None: return self
        val = self.func(obj)
        obj.__dict__[self.name] = val   # shadows descriptor
        return val

class Report:
    @LazyAttr
    def summary(self):
        print("computing...")
        return "done"

r = Report()
print(r.summary)   # computing... done
print(r.summary)   # done (cached)

property 是描述符

property 本身就是用 C 实现的数据描述符。理解描述符有助于您理解 @property、@classmethod 和 @staticmethod 的工作原理。

class Circle:
    def __init__(self, r): self._r = r

    @property
    def radius(self): return self._r

    @radius.setter
    def radius(self, v):
        if v < 0: raise ValueError
        self._r = v

# property is: property.__get__ = fget, property.__set__ = fset

描述符查找顺序

Python 属性查找顺序:1)类型(MRO)中的数据描述符;2)实例 __dict__;3)非数据描述符和类变量。

# Pseudocode for obj.attr:
# 1. Check type(obj).__mro__ for a data descriptor
# 2. Check obj.__dict__
# 3. Check type(obj).__mro__ for non-data descriptor or class var
# 4. Raise AttributeError

快速检查

哪种类型的描述符优先于实例 __dict__?

回顾

描述符通过 __get__、__set__ 和 __delete__ 拦截属性访问。数据描述符会遮蔽实例的 __dict__;非数据描述符不会。使用 __set_name__ 可获取属性名称。property、classmethod 和 staticmethod 都是描述符。

常见问题解答

「描述器:__get__、__set__、__delete__」课时是免费的吗?

是的 — 「描述器:__get__、__set__、__delete__」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Python Academy 课程的其余内容,请升级到 CoddyKit PRO。 Python Academy 课程共包含 4 节课。

「描述器:__get__、__set__、__delete__」这节课中我会学到什么?

实现描述器协议,控制属性访问。 你通过在浏览器中直接运行的动手代码来练习 Python Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Python Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Python Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「描述器:__get__、__set__、__delete__」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Python Academy 课中编写并运行代码吗?

能。每节 Python Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. Python 类的创建方式
  2. 编写自定义元类
  3. 描述器:__get__、__set__、__delete__
  4. __slots__ 与内存优化
← 返回 Python Academy