拓冰建站拓冰建站
首页 / 资讯中心 / 正文

python-mastery 实战:从验证器到描述符——用 Python 描述符协议重构 Stock 类

示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载本文以 python-mastery 课程练习 Exercise 4.3 及其标准解答为主线系统讲解 Python 描述符协议descriptor protocol的工作原理并演示如何把上一节Exercise 4.2基于classmethod的 Validator 验证器家族升级为真正的描述符从而用极少量代码为Stock类注入完整的类型与取值范围校验。读完本文你将掌握__get__/__set__/__set_name__三个核心钩子的语义、数据描述符与非数据描述符的区别以及如何通过__set_name__Python 3.6消除描述符声明中的冗余命名。一、背景属性、slots、property 背后的共同机制在 python-mastery 的前序练习中Stock类先后经历了多次演进从Exercise 3.1 的简单类到使用property为shares、price添加校验参考 Solutions/3_2/stock.py 中 property 与私有属性_shares/_price的组合再到练习 3.x 中的__slots__、类属性校验等特性。Exercise 4.3 开头点明了一个关键事实所有这些特性的底层实现都是描述符协议descriptor protocol。理解描述符就等于理解了 Python 对象模型中最底层、也最强大的机制之一——它允许你在点运算符obj.attr这一层注入自定义逻辑。描述符协议三件套一个类只要定义了下列特殊方法之一其实例就称为描述符特殊方法触发时机典型用途__get__(self, instance, cls)读取属性obj.attr时返回属性值或绑定方法/计算属性__set__(self, instance, value)赋值obj.attr value时校验、转换、存入实例字典__delete__(self, instance)del obj.attr时删除逻辑其中同时实现__get__与__set__的描述符称为数据描述符data descriptor只实现__get__的称为非数据描述符non-data descriptor——后者的查找优先级低于实例字典这也是方法绑定等行为的基础。二、(a) 描述符实战解剖 Stock.dict练习第一步要求观察一个现有Stock对象的属性访问然后检查类字典 s Stock(GOOG, 100, 490.10) s.name GOOG s.shares 100 Stock.__dict__.keys() [sell, __module__, __weakref__, price, _price, shares, _shares, __slots__, cost, __repr__, __doc__, __init__]注意shares、_shares、price、_price、cost、sell这些名字全部出现在类字典Stock.__dict__中而不是存储在实例上。这是因为 property、slots 这些特性在类定义时就已经以描述符的形式注册进了类字典。关键实验是手动调用描述符协议方法绕过常规的点运算符语法 q Stock.__dict__[shares] q.__get__(s) # 等价于 s.shares 100 q.__set__(s, 75) # 等价于 s.shares 75 s.shares 75 q.__set__(s, 75) # 触发校验抛 TypeError Traceback (most recent call last): File stdin, line 1, in module File stock.py, line 23, in shares raise TypeError(Expected an integer) TypeError: Expected an integer这里揭示的机制是每当你对实例执行属性访问或赋值Python 都会自动在类字典中查找对应的描述符并调用其__get__()/__set__()。q.__set__(s, 75)之所以报错正是因为底层 property 描述符内部执行了raise TypeError(Expected an integer)校验。三、(b) 亲手实现一个描述符捕获点运算符理解了协议之后练习要求从零实现一个最简单的描述符类对应练习中创建的descrip.py文件# descrip.py class Descriptor: def __init__(self, name): self.name name def __get__(self, instance, cls): print(%s:__get__ % self.name) def __set__(self, instance, value): print(%s:__set__ %s % (self.name, value)) def __delete__(self, instance): print(%s:__delete__ % self.name)把它作为类属性放进一个普通类并观察交互 class Foo: a Descriptor(a) b Descriptor(b) c Descriptor(c) f Foo() f.a a:__get__ f.b b:__get__ f.a 23 a:__set__ 23 del f.a a:__delete__输出清晰地表明f.a的读取、赋值、删除三个操作分别被__get__、__set__、__delete__拦截并打印。练习在这里特意让你ponder the fact that you have captured the dot-operator for a specific attribute——即你已经在单个属性层面接管了 Python 的语法糖这是后续一切高级特性的地基。注意__get__的签名是__get__(self, instance, cls)当通过实例访问时instance是对象本身、cls是它的类当通过类直接访问时如Foo.ainstance为None。四、(c) 从 Validators 到 Descriptors改造 validate.py回顾上一练习的验证器家族在 Exercise 4.2 中我们用协作式多重继承构建了一组基于classmethod的验证器完整实现见 Solutions/4_2/validate.pyclass Validator: classmethod def check(cls, value): return value class Typed(Validator): expected_type object classmethod def check(cls, value): if not isinstance(value, cls.expected_type): raise TypeError(fExpected {cls.expected_type}) return super().check(value) class Integer(Typed): expected_type int # Float / String 同理 class Positive(Validator): classmethod def check(cls, value): if value 0: raise ValueError(Must be 0) return super().check(value) class NonEmpty(Validator): classmethod def check(cls, value): if len(value) 0: raise ValueError(Must be non-empty) return super().check(value) class PositiveInteger(Integer, Positive): pass class PositiveFloat(Float, Positive): pass class NonEmptyString(String, NonEmpty): pass这套体系可以像函数一样直接调用例如PositiveInteger.check(10)返回10PositiveInteger.check(10)抛TypeError: expected class intPositiveInteger.check(-10)抛ValueError。但它目前只是游离于类之外的校验函数——想要应用到Stock的属性上一练习还得写大量重复的 property 样板代码。关键一步给 Validator 加上set练习c部分的精髓只有一处改动在Validator基类中新增__set__方法并补一个记录名字的__init__把验证器变成数据描述符# validate.py class Validator: def __init__(self, name): self.name name classmethod def check(cls, value): return value def __set__(self, instance, value): instance.__dict__[self.name] self.check(value)理解这段代码的三层含义__set__是通用入口无论哪种子类验证器类型检查、非负检查、非空检查……赋值时都统一走到instance.__dict__[self.name] self.check(value)真正的校验逻辑由多态的check()完成。结果写入实例字典self.check(value)校验通过后原始值而非校验器实例被存进实例的__dict__供后续正常读取。刻意不定义__get__练习特别提醒——描述符缺少__get__时Python 会退回默认的属性查找逻辑直接返回实例字典中self.name对应的值。这要求self.name必须与实例字典中的键名完全一致。子类完全无需改动多重继承的组合类PositiveInteger、PositiveFloat、NonEmptyString原样复用。接着重写Stock用描述符替换掉手写的 propertyclass Stock: name String(name) shares PositiveInteger(shares) price PositiveFloat(price) def __init__(self, name, shares, price): self.name name self.shares shares self.price price行为与之前完全一致但代码量大幅下降 s Stock(GOOG, 100, 490.10) s.name GOOG s.shares 100 s.shares 75 # 通过 __set__ 写入 s.shares 75 # TypeError ... TypeError ... s.shares -50 # ValueError ... ValueError ...这就是描述符的真正威力在点运算符层面获得低层控制权用声明式的类属性替代冗长的 property 样板。从 Solutions/4_2/validate.py 与 Solutions/4_3/validate.py 的对比可以直观看到仅Stock内部的 property 代码就从约 15 行缩减为 3 行类属性声明。五、(d) 用set_name修复冗余命名现在声明中仍有一处别扭的地方shares PositiveInteger(shares)必须重复写两遍属性名。练习d要求给Validator增加__set_name__钩子# validate.py class Validator: def __init__(self, nameNone): self.name name def __set_name__(self, cls, name): self.name name classmethod def check(cls, value): return value def __set__(self, instance, value): instance.__dict__[self.name] self.check(value)__set_name__(cls, name)是 Python 3.6 新增的类创建机制每当描述符被赋值为某个类的类属性时解释器会自动回调它把类名和属性名传进来。于是Stock可以写成最干净的形式class Stock: name String() shares PositiveInteger() price PositiveFloat() def __init__(self, name, shares, price): self.name name self.shares shares self.price price练习原文特别提醒这一特性仅适用于 Python 3.6 及以上版本旧版本无法使用旧版只能退回 (c) 部分显式传名的写法。这是本文所述方案在运行环境上的唯一硬性前提。六、完整参考实现soln4_3.md练习的标准解答Exercises/soln4_3.md给出了最终可运行的完整代码合并了描述符协议、__set_name__、协作式多重继承三大特性class Validator: def __init__(self, nameNone): self.name name def __set_name__(self, cls, name): self.name name classmethod def check(cls, value): return value def __set__(self, instance, value): instance.__dict__[self.name] self.check(value) class Typed(Validator): expected_type object classmethod def check(cls, value): if not isinstance(value, cls.expected_type): raise TypeError(fexpected {cls.expected_type}) return super().check(value) class Integer(Typed): expected_type int class Float(Typed): expected_type float class String(Typed): expected_type str class Positive(Validator): classmethod def check(cls, value): if value 0: raise ValueError(must be 0) return super().check(value) class NonEmpty(Validator): classmethod def check(cls, value): if len(value) 0: raise ValueError(must be non-empty) return super().check(value) class PositiveInteger(Integer, Positive): pass class PositiveFloat(Float, Positive): pass class NonEmptyString(String, NonEmpty): pass # Examples if __name__ __main__: def add(x, y): Integer.check(x) Integer.check(y) return x y class Stock: name NonEmptyString() shares PositiveInteger() price PositiveFloat() def __init__(self, name, shares, price): self.name name self.shares shares self.price price def __repr__(self): return fStock({self.name!r}, {self.shares!r}, {self.price!r}) property def cost(self): return self.shares * self.price def sell(self, nshares): self.shares - nshares值得注意的细节__init__中self.name name这类赋值会走name描述符的__set__即__init__同样被自动校验无需在构造函数里手写任何check()调用add()函数示例展示了描述符类同时保留了 (c) 之前的类方法用法Integer.check(x)两种使用方式并存互不冲突cost仍用property定义说明描述符与 property 可以共存——property 本身就是一种内建数据描述符最终效果s.shares 75抛TypeErrors.shares -50抛ValueErrors.name 抛ValueError三个维度类型、非负、非空的校验全部由 3 行声明式类属性完成。七、深入描述符协议的关键语义结合练习与源码补充几个容易踩坑的协议要点数据描述符优先于实例字典对数据描述符实现了__set__而言即使实例__dict__里已有同名键属性访问仍优先调用描述符的__get__/__set__。本练习刻意省略__get__正是为了利用非数据描述符让位于实例字典这一规则从而简化读取路径。__get__的第三个参数__get__(self, instance, cls)中当instance is None时表示通过类访问如Stock.shares此时通常应返回描述符自身return self这也是很多框架的标准写法。__set_name__的调用时机它在类创建完成时、__init_subclass__与元类处理之后回调所以描述符可以放心地把name用于后续所有__set__调用不需要任何惰性初始化。协作式 super() 链PositiveInteger.check会沿 MRO 依次执行Integer.check类型校验→Positive.check非负校验→Validator.check原样返回这正是 Exercise 4.2 铺垫的协作式多重继承在描述符场景下的自然延续。八、仓库中的后续演化这套 Validator 走向何方validate.py并没有止步于练习 4.3。在 python-mastery 仓库后续章节中这个描述符化 Validator 被反复复用并持续演进是理解课程主线的重要线索Solutions/6_5/validate.py在生成器与类章节中与__slots__风格的结构化类structure配合继续承担属性校验职责Solutions/7_1/validate.py、Solutions/7_2/validate.py进入装饰器、类装饰器与元类章节Validator 被进一步抽象Solutions/8_1/validate.py、Solutions/8_2/validate.py、Solutions/8_3/validate.py与协程、并发 ticker 应用集成Solutions/9_2/structly/validate.py、Solutions/9_3/structly/validate.py、Solutions/9_4/structly/validate.py最终被打包进structly库结构类 表格格式化 验证器的完整工具箱作为可导入的标准模块。可以推断练习 4.3 是整条验证器→描述符→元类/库化演进链的枢纽没有把 Validator 描述符化这一步后续structure类、structly包的声明式设计就无从谈起。如果你在仓库中继续完成第 59 章会反复看到本节代码的身影。九、小结通过 python-mastery 练习 4.3我们完成了三个层层递进的改造观察通过手动调用Stock.__dict__[shares].__get__/__set__理解 property、slots 等特性背后的描述符协议实现从零写出打印型Descriptor确认__get__/__set__/__delete__能完整接管点运算符工程化给上一章的 Validator 家族补上__set__与__set_name__让Stock的校验代码从约 15 行 property 样板缩到 3 行声明且保留函数式check()用法。这套模式描述符 协作式多重继承 __set_name__至今仍是 Python 数据校验、ORM 字段定义等库的常见实现手法。掌握它你就拥有了在 Python 对象模型底层自由定制属性行为的能力。延伸阅读完整练习说明见 Exercises/ex4_3.md标准解答见 Exercises/soln4_3.md上一节验证器家族的构建见 Exercises/ex4_2.md全部练习索引见 Exercises/index.md。赞分享示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载相关推荐Python描述符协议终极指南掌握property与数据描述符的完整教程Python描述符协议终极指南掌握property与数据描述符的完整教程 Python描述符协议是面向对象编程中的高级特性它让属性访问变得更加灵活和强大。在示例工程教程10个实战案例掌握Python装饰器与描述符从入门到精通的终极指南10个实战案例掌握Python装饰器与描述符从入门到精通的终极指南 Python装饰器与描述符是提升代码质量的关键技术但对新手而言常常充满挑战。本文将通过G示例工程教程CPython 描述符对象 C API 详解从 PyDescr_New 系列函数到描述符协议实现CPython 描述符对象 C API 详解从 PyDescr_New 系列函数到描述符协议实现 本文基于 CPython 官方 C API 文档 Doc/c编程语言语言运行时解释器标准库上一篇终极指南OpenTSDB多语言客户端开发与完整示例教程下一篇探索Gulf of Mexico完美编程语言的终极指南与快速入门教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门