ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python Mastery 实战:用特殊方法协议从零打造可变整数 MutInt 自定义原始类型

Python Mastery 实战:用特殊方法协议从零打造可变整数 MutInt 自定义原始类型 示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载本指南源自 Advanced Python Mastery 课程dabeaz 主讲的 Exercise 2.4属于「数据处理」章节的核心实战在不使用任何 C 扩展的前提下仅靠 Python 特殊方法dunder methods实现一个全新的原始类型——可变整数MutInt。读完本文你将系统掌握__str__/__repr__/__format__、__add__/__radd__/__iadd__、__eq__/__lt__/total_ordering、__int__/__float__/__index__等全部底层协议并能独立设计出可融入 Python 生态的自定义数据类型。为什么需要自己造一个原始类型在绝大多数程序中我们用int、float、str这类内建原始类型表示数据但 Python 并不限制你只能使用这些类型。标准库中的decimal十进制精确运算与fractions有理数模块本质上就是各自实现了一套新的原始类型。你完全可以做出自己的类型前提是理解支撑 Python 对象工作的底层协议protocol——即那些由解释器在特定语法场景下自动调用的特殊方法。本练习要造的类型叫MutIntMutable Integer可变整数。内建的int是不可变的a 3; a 1实际上创建了新对象。如果我们想要一个值可以原地修改的整数对象就必须自己实现。这个练习过程会暴露大量细节但它能让你对对象如何与 Python 其余部分交互建立整体认知。仓库中该练习的完整参考实现位于 Solutions/2_4/mutint.py标准解答文档见 Exercises/soln2_4.md建议先自行尝试卡住再对照。(a) 起步一个什么都不会的 MutIntPython 整数通常不可变。假设你想做一个可变整数对象先从这样一个类开始# mutint.py class MutInt: __slots__ [value] def __init__(self, value): self.value value__slots__声明了实例只允许拥有的属性避免为每个实例维护__dict__既省内存又限制属性集合。试着用一下 a MutInt(3) a __main__.MutInt object at 0x10e79d408 a.value 3 a.value 42 a.value 42 a 10 Traceback (most recent call last): File stdin, line 1, in module TypeError: unsupported operand type(s) for : MutInt and int 看起来挺兴奋但除了value 可变这一点这个新对象几乎什么都干不了打印输出是丑陋的内存地址数学运算符全部失效。接下来几个小节将逐层补齐它缺失的能力。(b) 修复输出str/repr/format通过给对象实现__str__()、__repr__()和__format__()方法可以修复输出行为# mutint.py class MutInt: __slots__ [value] def __init__(self, value): self.value value def __str__(self): return str(self.value) def __repr__(self): return fMutInt({self.value!r}) def __format__(self, fmt): return format(self.value, fmt)三个方法的职责分工__str__面向用户的可读字符串print(a)时调用输出3__repr__面向开发者的无歧义表示交互式终端直接输入a回车时调用输出MutInt(3)且应尽可能让eval(repr(a))能还原对象__format__让对象支持 f-string 与format()的格式说明符直接把格式描述符fmt透传给底层值。验证效果 a MutInt(3) print(a) 3 a MutInt(3) fThe value is {a:*^10d} The value is ****3***** a.value 42 a MutInt(42) 注意{a:*^10d}中的*^10d是完整格式说明符*为填充字符、^表示居中、10是宽度、d表示十进制整数最终渲染为****3*****——这正是__format__把fmt原样转交format(self.value, fmt)的结果。(c) 数学运算符add/radd/iadd要让对象支持各种数学运算符需要实现相应的方法但识别其他类型的数据并编写相应的转换代码是开发者自己的责任。给MutInt加上__add__()方法class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __add__(self, other): if isinstance(other, MutInt): return MutInt(self.value other.value) elif isinstance(other, int): return MutInt(self.value other) else: return NotImplemented关键设计点用isinstance分支处理不同操作数类型同类型相加、与int相加各自返回新的MutInt实例对其他类型返回NotImplemented注意不是抛异常告诉解释器我不支持这个组合由解释器尝试反向操作或最终抛出TypeError。这比直接抛异常更符合 Python 协议规范。有了这个改动整数与可变整数可以相加结果仍是MutInt实例与float相加则报错 a MutInt(3) b a 10 b MutInt(13) b.value 23 c a b c MutInt(26) a 3.5 Traceback (most recent call last): File stdin, line 1, in module TypeError: unsupported operand type(s) for : MutInt and float 反向操作radd的由来这段代码的一个问题是操作数顺序颠倒后失效。考虑 a 10 MutInt(13) 10 a Traceback (most recent call last): File stdin, line 1, in module TypeError: unsupported operand type(s) for : int and MutInt 原因在于int类型对MutInt一无所知它无法处理这个加法。解决办法是添加__radd__()方法——当第一次尝试调用左侧操作数的__add__()失败后解释器会转而调用右侧操作数的__radd__()class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __add__(self, other): if isinstance(other, MutInt): return MutInt(self.value other.value) elif isinstance(other, int): return MutInt(self.value other) else: return NotImplemented __radd__ __add__ # Reversed operands由于加法满足交换律直接把__add__赋值给__radd__即可。验证 a MutInt(3) a 10 MutInt(13) 10 a MutInt(13) 原地更新iadd与可变语义既然整数是可变的还可以实现原地加更新运算符需要__iadd__()方法class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __iadd__(self, other): if isinstance(other, MutInt): self.value other.value return self elif isinstance(other, int): self.value other return self else: return NotImplemented注意与__add__的本质区别__iadd__直接修改self.value并返回self而不是返回新对象。这允许一些有趣甚至令人惊讶的用法 a MutInt(3) b a a 10 a MutInt(13) b # Notice that b also changes MutInt(13) b也跟着变了这看起来有些奇怪但内建 Python 对象中就有类似微妙的特性。例如 list 与 tuple 在上的行为截然不同 a [1,2,3] b a a [4,5] a [1, 2, 3, 4, 5] b [1, 2, 3, 4, 5] c (1,2,3) d c c (4,5) c (1, 2, 3, 4, 5) d # Explain difference from lists (1, 2, 3) 差异的根源list 实现了__iadd__a [4,5]原地扩展列表b与a共享同一对象所以同步变化tuple 不可变、没有__iadd__c (4,5)退化为c c (4,5)创建了新对象d仍指向旧元组。这与MutInt的行为机制完全一致——理解__iadd__是否存在是预测别名aliasing行为的关键。(d) 比较运算eq/lt与 total_ordering目前比较运算仍然不工作 a MutInt(3) b MutInt(3) a b False a 3 False 原因很简单没有定义__eq__退化为对象身份比较内存地址两个独立实例自然不相等。通过添加__eq__()可以修复__lt__()、__le__()、__gt__()、__ge__()则可实现其他比较class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __eq__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplemented def __lt__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplemented测试一下 a MutInt(3) b MutInt(3) a b True c MutInt(4) a c True a c Traceback (most recent call last): File stdin, line 1, in module TypeError: not supported between instances of MutInt and MutInt 失败是因为没有提供__le__()方法。你可以单独编码它但更省事的方式是使用total_ordering装饰器来自functools标准库from functools import total_ordering total_ordering class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __eq__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplemented def __lt__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplementedtotal_ordering会在你**至少提供相等运算符__eq__和其余关系中的一个如__lt__**的前提下自动补全缺失的__le__、__gt__、__ge__等方法。(e) 类型转换int/float/index新原始类型已接近完整还差与常见转换的配合能力 a MutInt(3) int(a) Traceback (most recent call last): File stdin, line 1, in module TypeError: int() argument must be a string, a bytes-like object or a number, not MutInt float(a) Traceback (most recent call last): File stdin, line 1, in module TypeError: float() argument must be a string, a bytes-like object or a number, not MutInt 给类加上__int__()和__float__()方法即可修复from functools import total_ordering total_ordering class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __int__(self): return self.value def __float__(self): return float(self.value)现在可以正确转换了 a MutInt(3) int(a) 3 float(a) 3.0 为什么还不够Python 从不自动转换一般规则是Python 从不自动转换数据类型。因此即使给了类__int__()方法MutInt也不会在所有期望整数的场景下自动生效。例如索引 names [Dave, Guido, Paula, Thomas, Lewis] a MutInt(1) names[a] Traceback (most recent call last): File stdin, line 1, in module TypeError: list indices must be integers or slices, not MutInt 这可以通过给MutInt一个产生整数的__index__()方法来修复。__index__()是整数语义协议凡是需要真实整数索引的场合列表索引、切片、bin()/hex()/oct()、range()等解释器都要求对象提供__index__。修改类from functools import total_ordering total_ordering class MutInt: __slots__ [value] def __init__(self, value): self.value value ... def __int__(self): return self.value __index__ __int__ # Make indexing work此后names[a]这类索引操作即可正常执行。从实现上看__index__ __int__的别名赋值与__radd__ __add__一脉相承都是复用已有方法满足另一个协议入口的惯用技巧。完整实现一览将以上所有方法合并即得到仓库中 Solutions/2_4/mutint.py 的完整代码# mutint.py # # Mutable integers from functools import total_ordering total_ordering class MutInt: __slots__ [value] def __init__(self, value): self.value value def __str__(self): return str(self.value) def __repr__(self): return fMutInt({self.value!r}) def __format__(self, fmt): return format(self.value, fmt) # Implement the operator. Forward operands (MutInt other) def __add__(self, other): if isinstance(other, MutInt): return MutInt(self.value other.value) elif isinstance(other, int): return MutInt(self.value other) else: return NotImplemented # Support for reversed operands (other MutInt) __radd__ __add__ # Support for in-place update (MutInt other) def __iadd__(self, other): if isinstance(other, MutInt): self.value other.value return self elif isinstance(other, int): self.value other return self else: return NotImplemented # Support for equality testing def __eq__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplemented # One relation is needed for total_ordering decorator. It fills in others def __lt__(self, other): if isinstance(other, MutInt): return self.value other.value elif isinstance(other, int): return self.value other else: return NotImplemented # Conversions to int() and float() def __int__(self): return int(self.value) def __float__(self): return float(self.value) # Support for indexing s[MutInt] __index__ __int__讨论自定义原始类型为什么难制造一个新的原始数据类型实际上是 Python 编程中最复杂的任务之一。这其中有大量边界情况和底层问题需要操心尤其是你的类型如何与其他 Python 类型交互。本练习刻意省略了-、*、/等其余运算符、与float的互操作、hash 支持可变的MutInt本就不该被 hash、序列化等众多细节它们分别对应__sub__/__rsub__、__mul__、__truediv__、__hash__等协议。最关键的一点是只要你了解底层协议几乎可以自定义对象与 Python 其余部分交互的每一个方面。如果你打算这样做建议先查看与你目标相似的现有代码——就像decimal与fractions模块所展示的那样它们是学习新原始类型设计的最佳范本。延伸学习本练习属于「数据处理」单元前后衔接Exercise 2.3列式数据读取与 Exercise 2.5字典与集合解答文档Exercises/soln2_4.md完整实现Solutions/2_4/mutint.py课程全部练习索引Exercises/index.md对应课件见仓库根目录的PythonMastery.pdf后续第 3 章「Classes and Objects」会在此基础上深入类与对象机制可继续按顺序学习。练习文件默认保存在仓库根目录下你可以新建mutint.py并像文档示例那样在 Python 交互环境中逐段验证若想重置到可用状态直接参考Solutions/2_4/下的标准解答即可。赞分享示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载相关推荐Serilog自定义ScalarConversionPolicy特殊类型序列化Serilog自定义ScalarConversionPolicy特殊类型序列化 在使用Serilog进行.NET日志记录时我们经常需要处理各种数据类型的序列后端可观测性Larastan自定义类型定义model-property、view-string等特殊类型详解Larastan自定义类型定义model property、view string等特殊类型详解 Larastan作为Laravel生态中强大的静态分析工具静态分析代码质量开发工具后端10up Engineering Best Practices移动优先响应式设计实现方案终极指南10up Engineering Best Practices移动优先响应式设计实现方案终极指南 想要构建卓越的移动端体验10up Engineering创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表