├── img ├── header.jpg └── magicmethods.jpg ├── 10-前言.md ├── README.md ├── 50-属性访问.md ├── 30-构造和析构.md ├── 80-可调用和槽.md ├── 20-运算符.md ├── 40-类的表示.md ├── 60-容器.md └── 70-描述符.md /img/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/python-magic-method-cookbook/HEAD/img/header.jpg -------------------------------------------------------------------------------- /img/magicmethods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/python-magic-method-cookbook/HEAD/img/magicmethods.jpg -------------------------------------------------------------------------------- /10-前言.md: -------------------------------------------------------------------------------- 1 | **这个教程是个啥?** 2 | 3 | - 手把手级别的系列文章,介绍Python魔法方法 4 | - 所有文章开源免费,任何人都可以下载、阅读和传播 5 | 6 | 如果你还不知道怎么使用Python中的魔法方法,阅读本系列可以帮助你提高代码效率和生活品质。 7 | 8 | ## 魔法方法简介 9 | 10 | Python 中的魔法方法是指以双下划线开头和结尾的特殊方法,比如 `__init__` 、 `__abs__` 等。 11 | 12 | Python 中的内置类定义了非常多的魔法方法。比如 `int` 类,你可以用 `dir()` 函数查看: 13 | 14 | ```python 15 | >>> dir(int) 16 | ['__abs__', '__add__', '__and__', '__bool__', ...] 17 | ``` 18 | 19 | 魔法方法可以直接被调用,但更多的时候,它会在特定情况下被自动调用。 20 | 21 | 就比如整数相加的计算,它实际上就相当于: 22 | 23 | ```python 24 | >>> num = 1 25 | >>> num + 2 26 | 3 27 | >>> num.__add__(2) 28 | 3 29 | ``` 30 | 31 | 你可以让一个自定义的类实现 `__add__` 魔法方法,从而使它也可以进行加法计算。 32 | 33 | 比如定义一个矢量: 34 | 35 | ```python 36 | class Vector: 37 | def __init__(self, x, y): 38 | self.x = x 39 | self.y = y 40 | 41 | def __add__(self, other): 42 | new_x = self.x + other.x 43 | new_y = self.y + other.y 44 | return Vector(new_x, new_y) 45 | ``` 46 | 47 | 由于实现了 `__add__` 方法,这个矢量类就可以非常自然的相加: 48 | 49 | ```python 50 | >>> v1 = Vector(1, 2) 51 | >>> v2 = Vector(3, 4) 52 | 53 | >>> v3 = v1 + v2 54 | 55 | >>> v3.x 56 | 4 57 | >>> v3.y 58 | 6 59 | ``` 60 | 61 | 总之,魔法方法在 Python 中占有重要的地位,并且涵盖了你想得到的几乎全部基础功能,灵活运用可以让你的代码更加简洁高效。 62 | 63 | 所有的魔法方法在官方文档里都可以找到,但是它非常的枯燥并且缺少示例,不太容易理解。 64 | 65 | 因此我将**常用和重要的魔法方法**归纳总结好(注意不是全部!),面向新手从基础讲起,写成这个系列文章。 66 | 67 | 希望你漫游愉快! 68 | 69 | > 如果本系列教程对你有帮助,可通过点赞、评论、转发来支持我。 70 | 71 | ## 加入社区 72 | 73 | 一个人的学习是孤单的。欢迎扫码 Python 交流QQ群、公众号、TG群组,和大家一起进步。 74 | 75 | ![](https://blog.dusaiphoto.com/QR-0608.jpg) 76 | 77 | ## 许可协议 78 | 79 | 本教程(包括且不限于文章、代码、图片等内容)遵守 **署名-非商业性使用 4.0 国际 (CC BY-NC 4.0) 协议**。协议内容如下。 80 | 81 | **您可以自由地:** 82 | 83 | - **共享** — 在任何媒介以任何形式复制、发行本作品。 84 | - **演绎** — 修改、转换或以本作品为基础进行创作。 85 | 86 | 只要你遵守许可协议条款,许可人就无法收回你的这些权利。 87 | 88 | **惟须遵守下列条件:** 89 | 90 | - **署名** — 您必须给出**适当的署名**,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。 91 | - **非商业性使用** — 您不得将本作品用于**商业目的**。 92 | - **没有附加限制** — 您不得适用法律术语或者技术措施从而限制其他人做许可协议允许的事情。 93 | 94 | > 适当的署名:您必须提供创作者和署名者的姓名或名称、版权标识、许可协议标识、免责标识和作品链接。 95 | > 96 | > 商业目的:主要目的为获得商业优势或金钱回报。 97 | 98 | --- 99 | 100 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 101 | 102 | 看完文章想吐槽?欢迎留言告诉我! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](img/header.jpg) 2 | 3 | [![](https://img.shields.io/badge/python-3.8-orange.svg)](https://www.python.org/downloads/release/python-370/) 4 | [![](https://img.shields.io/badge/license-CC_BY_NC_4.0-000000.svg)](https://creativecommons.org/licenses/by-nc/4.0/) 5 | 6 | ## Python魔法方法漫游指南 7 | 8 | **这个库是个啥?** 9 | 10 | - 手把手级别的系列文章,介绍Python魔法方法 11 | - 所有文章开源免费,任何人都可以下载、阅读和传播 12 | 13 | 如果你还不知道怎么使用Python中的魔法方法,阅读本系列可以帮助你提高代码效率和生活品质。 14 | 15 | ## 魔法方法简介 16 | 17 | Python 中的魔法方法是指以双下划线开头和结尾的特殊方法,比如 `__init__` 、 `__abs__` 等。 18 | 19 | Python 中的内置类定义了非常多的魔法方法。比如 `int` 类,你可以用 `dir()` 函数查看: 20 | 21 | ```python 22 | >>> dir(int) 23 | ['__abs__', '__add__', '__and__', '__bool__', ...] 24 | ``` 25 | 26 | 魔法方法可以直接被调用,但更多的时候,它会在特定情况下被自动调用。 27 | 28 | 就比如整数相加的计算,它实际上就相当于: 29 | 30 | ```python 31 | >>> num = 1 32 | >>> num + 2 33 | 3 34 | >>> num.__add__(2) 35 | 3 36 | ``` 37 | 38 | 你可以让一个自定义的类实现 `__add__` 魔法方法,从而使它也可以进行加法计算。 39 | 40 | 比如定义一个矢量: 41 | 42 | ```python 43 | class Vector: 44 | def __init__(self, x, y): 45 | self.x = x 46 | self.y = y 47 | 48 | def __add__(self, other): 49 | new_x = self.x + other.x 50 | new_y = self.y + other.y 51 | return Vector(new_x, new_y) 52 | ``` 53 | 54 | 由于实现了 `__add__` 方法,这个矢量类就可以非常自然的相加: 55 | 56 | ```python 57 | >>> v1 = Vector(1, 2) 58 | >>> v2 = Vector(3, 4) 59 | 60 | >>> v3 = v1 + v2 61 | 62 | >>> v3.x 63 | 4 64 | >>> v3.y 65 | 6 66 | ``` 67 | 68 | 总之,魔法方法在 Python 中占有重要的地位,并且涵盖了你想得到的几乎全部基础功能,灵活运用可以让你的代码更加简洁高效。 69 | 70 | 所有的魔法方法在官方文档里都可以找到,但是它非常的枯燥并且缺少示例,不太容易理解。 71 | 72 | 因此我将**常用和重要的魔法方法**归纳总结好(注意不是全部!),面向新手从基础讲起,写成这个系列文章。 73 | 74 | 希望你漫游愉快! 75 | 76 | > 如果本系列教程对你有帮助,可通过点赞、评论、转发来支持我。 77 | 78 | ## 加入社区 79 | 80 | 一个人的学习是孤单的。欢迎扫码 Python 交流QQ群、公众号、TG群组,和大家一起进步。 81 | 82 | ![](https://blog.dusaiphoto.com/QR-0608.jpg) 83 | 84 | 此外,本系列文章也会同步发布在[我的博客](https://www.dusaiphoto.com/topic/)。博客主要发布 Python 或 Django 的教程,欢迎围观。 85 | 86 | ## 许可协议 87 | 88 | 本教程(包括且不限于文章、代码、图片等内容)遵守 **署名-非商业性使用 4.0 国际 (CC BY-NC 4.0) 协议**。协议内容如下。 89 | 90 | **您可以自由地:** 91 | 92 | - **共享** — 在任何媒介以任何形式复制、发行本作品。 93 | - **演绎** — 修改、转换或以本作品为基础进行创作。 94 | 95 | 只要你遵守许可协议条款,许可人就无法收回你的这些权利。 96 | 97 | **惟须遵守下列条件:** 98 | 99 | - **署名** — 您必须给出**适当的署名**,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。 100 | - **非商业性使用** — 您不得将本作品用于**商业目的**。 101 | - **没有附加限制** — 您不得适用法律术语或者技术措施从而限制其他人做许可协议允许的事情。 102 | 103 | > 适当的署名:您必须提供创作者和署名者的姓名或名称、版权标识、许可协议标识、免责标识和作品链接。 104 | > 105 | > 商业目的:主要目的为获得商业优势或金钱回报。 106 | -------------------------------------------------------------------------------- /50-属性访问.md: -------------------------------------------------------------------------------- 1 | Python 提供了几种常见的**访问控制**的魔法方法,其作用有点类似其他语言中对属性的封装(定义私有属性,通过公有的 getter/setter 访问)。 2 | 3 | 更通俗一点,这类魔法方法可以接管“点号”运算。每当你访问类的某个属性时,就将调用这类方法。 4 | 5 | 此类方法有如下几个: 6 | 7 | - `__getattr__`:在实例中找不到属性时调用。 8 | - `__getattribute__`:无论是否找到属性均调用。 9 | - `__setattr__`:对属性赋值时调用。 10 | - `__delattr__`:删除属性时调用。 11 | 12 | 来看个例子: 13 | 14 | ```python 15 | class Foo: 16 | def __init__(self, a=10): 17 | self.a = a 18 | 19 | def __getattr__(self, name): 20 | return f'Invoke getattr. name: {name}' 21 | 22 | f = Foo() 23 | 24 | a = f.a 25 | print(a) 26 | # 输出: 27 | # 10 28 | b = f.b 29 | print(b) 30 | # 输出: 31 | # Invoke getattr. name: b 32 | ``` 33 | 34 | 当调用一个不存在的属性时,此方法可以给出警告,或者灵活处理 `AttributeError` 和返回值。 35 | 36 | 再看看 `__getattribute__` 的行为: 37 | 38 | ```python 39 | class Foo: 40 | def __init__(self, a=10): 41 | self.a = a 42 | 43 | def __getattribute__(self, name): 44 | return f'Invoke getattr. name: {name}' 45 | 46 | f = Foo() 47 | 48 | a = f.a 49 | print(a) 50 | # 输出: 51 | # Invoke getattr. name: a 52 | b = f.b 53 | print(b) 54 | # 输出: 55 | # Invoke getattr. name: b 56 | ``` 57 | 58 | 不管调用的属性存在与否,`__getattribute__` 均被调用。运用此方法你可以对属性的返回值做某种定制,比如单位换算、动态计算之类的骚操作。 59 | 60 | 类似的还有 `__setattr__` : 61 | 62 | ```python 63 | class Foo: 64 | def __init__(self, a=10): 65 | self.a = a 66 | 67 | def __setattr__(self, name, value): 68 | # 注意千万不能 self.name = value + 1 赋值 69 | # 引发无限递归错误 70 | self.__dict__[name] = value + 1 71 | 72 | f = Foo() 73 | f.a = 20 74 | f.b = 30 75 | print(f.a, f.b) 76 | # 输出: 77 | # 21 31 78 | ``` 79 | 80 | 实现了 `__setattr__` 方法后,相当于所有的 `self.name = value` 都会变成 `self.__setattr__('name', value)` 。 81 | 82 | 注意方法中不能用 `self.name = value` 进行赋值,会引发无限递归错误,而是要用 `self.__dict__[name]` 直接修改命名空间中的属性。 83 | 84 | > 文章末尾有对 `__dict__` 的解释。 85 | 86 | 最后就是删除属性的 `__delattr__` 了: 87 | 88 | ```python 89 | class Foo: 90 | def __init__(self, a=10): 91 | self.a = a 92 | 93 | def __delattr__(self, name): 94 | print(f'Invoke delattr. name: {name}') 95 | self.__dict__.pop(name) 96 | 97 | f = Foo() 98 | print(f.a) 99 | # 输出: 100 | # 10 101 | 102 | del f.a 103 | print(f.a) 104 | # 输出: 105 | # Invoke delattr. name: a 106 | # AttributeError: 'Foo' object has no attribute 'a' 107 | ``` 108 | 109 | 你可以想象将这些方法组合运用的强大了,强大到可以写出反直觉的花里胡哨的玩意儿出来。 Python 将灵活性交给你,而你的任务是谨慎使用这些能力,同时达到代码简洁之道。 110 | 111 | ## 命名空间 112 | 113 | Python 的类具有一个特殊的字典叫 `__dict__` ,它被称作**命名空间**,说白了就是一个存放对象所有属性的字典。 114 | 115 | 对属性的引用被解释器转换为对该字典的查找,比如 `a.x` 相当于 `a.__dict__['x']` 。看下面的例子: 116 | 117 | ```python 118 | class Foo: 119 | def __init__(self): 120 | self.a = 10 121 | self.b = 20 122 | 123 | foo = Foo() 124 | 125 | print(foo.__dict__) 126 | # {'a': 10, 'b': 20} 127 | 128 | foo.__dict__['c'] = 30 129 | 130 | print(foo.__dict__) 131 | # {'a': 10, 'b': 20, 'c': 30} 132 | 133 | print(foo.c) 134 | # 30 135 | ``` 136 | 137 | 可以看到在程序运行期间,你可以动态的向 `__dict__` 中插入新的值,使得对象具有新的属性。 138 | 139 | `__setattr__` 中之所以不能用 `self.name = xxx` 是因为对属性的赋值会嵌套调用 `__setattr__` ,从而导致无限递归。 140 | 141 | 而直接操作 `__dict__` 则不会产生这个问题,其他几个属性访问的魔法方法也是类似的道理。 142 | 143 | --- 144 | 145 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 146 | 147 | 看完文章想吐槽?欢迎留言告诉我! -------------------------------------------------------------------------------- /30-构造和析构.md: -------------------------------------------------------------------------------- 1 | 在类 C++ 的语言中,类的**构造函数**和**析构函数**是非常重要的概念,负责对象的创建和销毁时的相关工作。比如在创建对象时用 `new` 开辟内存空间,销毁时 `delete` 释放内存。 2 | 3 | 而 Python 中似乎没有专门的构造和析构函数,不过也有对应的魔法方法替代构造和析构函数的功能。 4 | 5 | ## 构造对象 6 | 7 | Python 学习者最熟悉的魔法方法可能就是 `__init__` 了,它的作用是初始化对象。 8 | 9 | 比如说给实例对象的属性进行初始化的工作: 10 | 11 | ```python 12 | class Foo: 13 | def __init__(self, x): 14 | self.x = x 15 | 16 | f = Foo(10) 17 | print(f.x) 18 | # 输出: 19 | # 10 20 | ``` 21 | 22 | 如果父类也实现了 `__init__` 方法,那么子类中需要显式调用 `super()` 以确保正确初始化了父类的属性: 23 | 24 | ```python 25 | class Foo: 26 | def __init__(self, x=10): 27 | self.x = x 28 | 29 | class Bar(Foo): 30 | def __init__(self, y): 31 | super().__init__() 32 | self.y = y 33 | 34 | f = Bar(20) 35 | print(f.x, f.y) 36 | # 输出: 37 | # 10 20 38 | ``` 39 | 40 | 可能你觉得 `__init__` 就是构造函数了,但实际上它只负责初始化对象,并不负责创建对象。真正创建对象的是执行得更早的 `__new__` 方法: 41 | 42 | ```python 43 | class Foo: 44 | def __new__(cls, *args, **kwargs): 45 | print('new...', args, kwargs) 46 | obj = super().__new__(cls) 47 | print(obj) 48 | return obj 49 | 50 | def __init__(self, *args, **kwargs): 51 | print('init...', args, kwargs) 52 | print(self) 53 | 54 | 55 | Foo('a', x=10) 56 | # 输出: 57 | # new... ('a',) {'x': 10} 58 | # <__main__.Foo object at 0x00000176B9F34AF0> 59 | # init... ('a',) {'x': 10} 60 | # <__main__.Foo object at 0x00000176B9F34AF0> 61 | ``` 62 | 63 | 这里面信息量很大: 64 | 65 | - `__new__` 创建了对象,并且执行要早于 `__init__`。 66 | - `__new__` 的第一个参数 `cls` ,就是需要创建实例的类 `Foo`。 67 | - `__new__` 必须返回一个对象,这个对象其实就是创建出来的类实例,也就是 `self` 了。 68 | - `__new__` 和 `__init__` 的参数是相同的。 69 | 70 | 总之,`__new__` 和 `__init__` 共同完成了对象的构造工作。 71 | 72 | 实际上 `__new__` 方法通常不太会用到。其主要的应用场景归纳如下。 73 | 74 | ### 不可变对象 75 | 76 | 假设现在要定义一个叫“英寸”的类,并且它是 `float` 的子类,你会怎么做呢? 77 | 78 | 在 `__init__` 里实现单位转化是无效的: 79 | 80 | ```python 81 | class inch(float): 82 | # 错误的方式 83 | def __init__(self, arg): 84 | float.__init__(arg*0.0254) 85 | 86 | print(inch(12.0)) 87 | # 输出: 88 | # 12.0 89 | ``` 90 | 91 | 因为此时对象已经创建好了,再初始化也没求用。 92 | 93 | 正确的方式: 94 | 95 | ```python 96 | class inch(float): 97 | # 正确的方式 98 | def __new__(cls, arg): 99 | return float.__new__(cls, arg*0.0254) 100 | 101 | print(inch(12.0)) 102 | # 输出: 103 | # 0.3048 104 | ``` 105 | 106 | 其他自定义不可变对象,比如整型、元组等用法也都类似。 107 | 108 | ### 单例模式 109 | 110 | 单例模式是指全局至多只有一个实例的类。 111 | 112 | 可以这样实现单例模式: 113 | 114 | ```python 115 | class Singleton: 116 | def __new__(cls, *args, **kwds): 117 | it = cls.__dict__.get("__it__") 118 | if it is not None: 119 | return it 120 | cls.__it__ = it = super().__new__(cls) 121 | it.init(*args, **kwds) 122 | return it 123 | 124 | def init(self, *args, **kwds): 125 | pass 126 | 127 | 128 | class Bar(Singleton): 129 | pass 130 | 131 | first = Bar() 132 | second = Bar() 133 | print(first) 134 | print(second) 135 | # 输出: 136 | # <__main__.Bar object at 0x00000176B9F349A0> 137 | # <__main__.Bar object at 0x00000176B9F349A0> 138 | ``` 139 | 140 | 不过单例模式在 Python 中不那么常见就是了。 141 | 142 | > 用装饰器也可以实现单例模式,在我以前的文章[装饰器入门](https://www.dusaiphoto.com/article/139/)中有探讨。 143 | 144 | ### 元编程 145 | 146 | 元编程是指代码动态修改和创建类的编程方式。在元编程的黑魔法里,`__new__` 方法也占有一席之地。 147 | 148 | 元编程又是另外一个晦涩的概念了,这里也不展开了,有兴趣的同学看我另一篇文章[元类与元编程](https://www.dusaiphoto.com/article/142/)。 149 | 150 | ## 析构对象 151 | 152 | `__new__` 和 `__init__` 是对象的构造器,那么 `__del__` 就是析构器,或者叫销毁器。但需要注意的是,Python 有一套自己的垃圾回收机制,`__del__` 并不等同于 C 系语言中的析构器,而是更类似于生命周期钩子,它提供了一个对象被销毁时执行自定义逻辑的位置。 153 | 154 | 比如说你想确保对象被销毁时,文件能够正常关闭: 155 | 156 | ```python 157 | class FileObject: 158 | # 确保对象被销毁时关闭文件 159 | def __del__(self): 160 | self.file.close() 161 | del self.file 162 | ``` 163 | 164 | 需要小心的是,如果 Python 解释器(或程序)被强行退出时,`__del__` 并不会被执行。 165 | 166 | 由于 `__del__` 可以在执行任意代码时调用,包括从任意线程调用。如果它执行时需要获取锁或者调用任何其他阻塞资源,可能会导致死锁。 167 | 168 | 所以不要把重要操作完全押宝在上面了,养成定期手动清理资源的好习惯。 169 | 170 | --- 171 | 172 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 173 | 174 | 看完文章想吐槽?欢迎留言告诉我! -------------------------------------------------------------------------------- /80-可调用和槽.md: -------------------------------------------------------------------------------- 1 | > 看到最后,如何用一行代码优化内存占用。 2 | 3 | 鉴于前面几个章节难度较高,本章我们聊两个轻松点的魔法方法。 4 | 5 | 虽然轻松,但是一样很有用哦。 6 | 7 | ## 可调用对象 8 | 9 | 通常情况下,类的实例是不可调用的。 10 | 11 | 举个栗子: 12 | 13 | ```python 14 | class Foo: 15 | pass 16 | 17 | foo = Foo() 18 | foo() 19 | # 输出: 20 | # TypeError: 'Foo' object is not callable 21 | ``` 22 | 23 | 但是如果类定义里实现了 `__call__` 协议,那么这个类就变成**可调用对象**(callable)了。 24 | 25 | 比如: 26 | 27 | ```python 28 | class Bar: 29 | def __call__(self): 30 | print('Hi there') 31 | 32 | bar = Bar() 33 | bar() 34 | # 输出: 35 | # Hi there 36 | ``` 37 | 38 | 单纯从 `bar()` 你都无法得知它是函数还是类实例。所以在 Python 中一切皆对象,连函数也是对象,它和类的区分不是那么显著。 39 | 40 | 那把类变成可调用对象有什么用呢? 41 | 42 | 有的时候你的代码需要同时支持调用函数和类,那么就可以这样: 43 | 44 | ```python 45 | def foo(value=70): 46 | """待调用函数""" 47 | print(f'Score is: {value}') 48 | 49 | 50 | class Bar: 51 | """待调用类""" 52 | def __init__(self, value=80): 53 | self.value = value 54 | 55 | def __call__(self): 56 | print(f'Score is: {self.value}') 57 | 58 | 59 | def print_score(obj): 60 | """实际运行函数""" 61 | obj() 62 | 63 | 64 | print_score(foo) 65 | # Score is: 70 66 | print_score(Bar()) 67 | # Score is: 80 68 | ``` 69 | 70 | 另一种应用场景是**类装饰器**。因为装饰器要求其对象必须可调用: 71 | 72 | ```python 73 | import functools 74 | 75 | class Logit(): 76 | def __init__(self, name): 77 | self.name = name 78 | 79 | def __call__(self, func): 80 | @functools.wraps(func) 81 | def wrapper(*args, **kwargs): 82 | value = func(*args, **kwargs) 83 | print(f'{self.name} is calling: ' + func.__name__) 84 | return value 85 | return wrapper 86 | 87 | @Logit(name='Dusai') 88 | def a_func(): 89 | pass 90 | 91 | a_func() 92 | 93 | # 输出: 94 | # Dusai is calling: a_func 95 | ``` 96 | 97 | > 想深入了解装饰器原理的同学,可以看我旧文[装饰器入门](https://www.dusaiphoto.com/article/139/)。 98 | 99 | ## 槽 100 | 101 | Python 是一门动态语言。当我们从定义好的类创建了实例后,可以在程序运行过程中,继续给实例绑定任何属性和方法。突出一个灵活。 102 | 103 | 举个例子: 104 | 105 | ```python 106 | class Foo: 107 | def __init__(self): 108 | self.a = 10 109 | 110 | foo = Foo() 111 | 112 | foo.b = 20 113 | foo.c = 30 114 | 115 | print(foo.b, foo.c) 116 | # 20 30 117 | ``` 118 | 119 | 这也太灵活了。如果我不想要这种动态特性,或者要限制属性的范围呢? 120 | 121 | 这时候就可以用到 `__slots__` 属性了: 122 | 123 | ```python 124 | class Foo: 125 | __slots__ = ('a', 'b') 126 | 127 | def __init__(self): 128 | self.a = 10 129 | 130 | foo = Foo() 131 | foo.b = 20 132 | 133 | foo.c = 30 134 | # 输出报错: 135 | # AttributeError: 'Foo' object has no attribute 'c' 136 | ``` 137 | 138 | 这在多人协作开发的场景可能派上用场,你可以用代码明确告诉同伴,这个类只允许有这几个属性,不要再添加了。 139 | 140 | `__slots__` 的另一个应用场景是**优化内存**、**提高查询效率**。 141 | 142 | 为了测试,定义两个类,并分别进行一万次实例化放到列表里: 143 | 144 | ```python 145 | class Foo(): 146 | def __init__(self): 147 | self.a = 'xyz' 148 | self.b = 100 149 | self.c = True 150 | 151 | 152 | class Bar(): 153 | __slots__ = ('a', 'b', 'c') 154 | 155 | def __init__(self): 156 | self.a = 'xyz' 157 | self.b = 100 158 | self.c = True 159 | 160 | 161 | foos = [Foo() for _ in range(10000)] 162 | bars = [Bar() for _ in range(10000)] 163 | ``` 164 | 165 | 接着使用某些手段,查看内存占用情况: 166 | 167 | ```python 168 | Partition of a set of 30026 objects. Total size = 2259528 bytes. 169 | Index Count % Size % Cumulative % Kind (class / dict of class) 170 | 0 10000 33 1040000 46 1040000 46 dict of __main__.Foo 171 | 1 10000 33 560000 25 1600000 71 __main__.Bar 172 | 2 10000 33 480000 21 2080000 92 __main__.Foo 173 | ... 174 | ``` 175 | 176 | 序号 0 和 2 均为 `Foo` 实例占用的内存,序号 1 为 `Bar` 实例占用。可以看到,使用了 `__slots__` 的 `Bar` 类,占用内存压缩到只剩惊人的 36.8%,而你付出的劳动仅仅只有一行代码。 177 | 178 | 原因就在于 Python 原本的属性命名空间 `__dict__` 占用内存较多,而 `__slots__` 显然在幕后禁止了 `__dict__` 的创建并优化了其性能。 179 | 180 | > 查看内存可用 `guppy3` 库,使用方法见[heapy](https://smira.ru/wp-content/uploads/2011/08/heapy.html)。 181 | 182 | 除了**内存**得到优化,**查询效率**也有提升。 183 | 184 | 用下面的代码测试:(适当增加了实例数量) 185 | 186 | ```python 187 | import time 188 | 189 | # Foo 和 Bar 的定义略过... 190 | 191 | foos = [Foo() for _ in range(1000000)] 192 | bars = [Bar() for _ in range(1000000)] 193 | 194 | t1 = time.time() 195 | 196 | for item in foos: 197 | item.a 198 | item.b = 50 199 | del item.c 200 | 201 | t2 = time.time() 202 | 203 | for item in bars: 204 | item.a 205 | item.b = 50 206 | del item.c 207 | 208 | t3 = time.time() 209 | 210 | print(t2 - t1) 211 | # 0.20045256614685059 212 | print(t3 - t2) 213 | # 0.11068224906921387 214 | ``` 215 | 216 | 查询速度提升了 44.8%,相当不错。 217 | 218 | > 需要指出的是,实际情况下很少有如此大量的实例化对象。是否真的需要用 `__slots__` 牺牲灵活性以优化效率,请谨慎考虑。 219 | 220 | 最后要注意的是,`__slots__` 对继承的子类是不起作用的。除非在子类中也定义 `__slots__` ,此时子类允许定义的属性就是自身的 `__slots__` 加上父类的 `__slots__` 。 221 | 222 | --- 223 | 224 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 225 | 226 | 看完文章想吐槽?欢迎留言告诉我! -------------------------------------------------------------------------------- /20-运算符.md: -------------------------------------------------------------------------------- 1 | **运算符**魔法方法通常用于实现对象的运算、赋值等操作。 2 | 3 | 前言中介绍的 `__add__` 就属于运算符魔法方法。让我们继续探讨。 4 | 5 | ## 比较运算符 6 | 7 | 使用 Python 魔法方法的一个巨大的好处,就是可以构建与内置类型相似行为的对象。 8 | 9 | 什么意思?比方说在某些语言中,要验证两个自定义对象是否相等,代码可能是这样: 10 | 11 | ```python 12 | if obj.isEqual(other): 13 | # ... 14 | ``` 15 | 16 | Python 中同样可以这样实现,但缺点是它是非标准的,你可能需要稍微花一点时间才知道它要做什么。另外,不同的程序员可能对函数的名称理解不同,从而使得同一个功能具有 `equals()` 、 `isEqual()` 、 `equalTo()` 等不同的名称。 17 | 18 | 让我们看看如果运用了魔法方法: 19 | 20 | ```python 21 | if obj == other: 22 | # ... 23 | ``` 24 | 25 | 是不是一目了然? 26 | 27 | 要实现 `==` 运算符,只需要你实现 `__eq__` 方法: 28 | 29 | ```python 30 | class Foo: 31 | def __init__(self, id): 32 | self.id = id 33 | 34 | def __eq__(self, other): 35 | return self.id == other.id 36 | 37 | 38 | f1 = Foo(1) 39 | f2 = Foo(1) 40 | f3 = Foo(2) 41 | 42 | print(f1 == f2, f2 == f3) 43 | # 输出: 44 | # True False 45 | ``` 46 | 47 | 这就是魔法的力量。 48 | 49 | 类似的比较运算符还有: 50 | 51 | - `__lt__` 52 | - `__le__` 53 | - `__ne__` 54 | - `__gt__` 55 | - `__ge__` 56 | 57 | 分别实现 `<` 、 `<=` 、 `!=` 、 `>` 、 `>=` 运算符。 58 | 59 | ## 算数运算符 60 | 61 | 算数运算符用于常见的数学运算,如 `+ - * /` 加减乘除等。 62 | 63 | 比如 `__add__` 魔法方法可以实现加法操作。 64 | 65 | 整数相加的计算就相当于: 66 | 67 | ```python 68 | >>> num = 1 69 | >>> num + 2 70 | 3 71 | >>> num.__add__(2) 72 | 3 73 | ``` 74 | 75 | 你可以让一个自定义的类实现 `__add__` 魔法方法,从而使它也可以进行加法计算。 76 | 77 | 比如定义一个矢量: 78 | 79 | ```python 80 | class Vector: 81 | def __init__(self, x, y): 82 | self.x = x 83 | self.y = y 84 | 85 | def __add__(self, other): 86 | new_x = self.x + other.x 87 | new_y = self.y + other.y 88 | return Vector(new_x, new_y) 89 | ``` 90 | 91 | 由于实现了 `__add__` 方法,这个矢量类就可以非常自然的相加: 92 | 93 | ```python 94 | >>> v1 = Vector(1, 2) 95 | >>> v2 = Vector(3, 4) 96 | 97 | >>> v3 = v1 + v2 98 | 99 | >>> v3.x 100 | 4 101 | >>> v3.y 102 | 6 103 | ``` 104 | 105 | 类似的,我们就可以实现 `Vector` 类的整套加减乘除运算了: 106 | 107 | ```python 108 | class Vector: 109 | def __init__(self, x, y): 110 | self.x = x 111 | self.y = y 112 | 113 | # 加 114 | def __add__(self, other): 115 | new_x = self.x + other.x 116 | new_y = self.y + other.y 117 | return Vector(new_x, new_y) 118 | 119 | # 减 120 | def __sub__(self, other): 121 | new_x = self.x - other.x 122 | new_y = self.y - other.y 123 | return Vector(new_x, new_y) 124 | 125 | # 乘 126 | def __mul__(self, other): 127 | new_x = self.x * other.x 128 | new_y = self.y * other.y 129 | return Vector(new_x, new_y) 130 | 131 | # 除 132 | def __truediv__(self, other): 133 | new_x = self.x / other.x 134 | new_y = self.y / other.y 135 | return Vector(new_x, new_y) 136 | ``` 137 | 138 | 类似的算数运算符还有: 139 | 140 | - `__matmul__` 141 | - `__floordiv__` 142 | - `__mod__` 143 | - `__pow__` 144 | - `__lshift__` 145 | - `__rshift__` 146 | - `__and__` 147 | - `__xor__` 148 | - `__or__` 149 | 150 | 以上魔法方法分别实现 `@` ,`//` ,`%` ,`**` ,`<<` ,`>>` , `&` , `^` , `|` 操作。 151 | 152 | > 了解这些方法的功能即可。需要用到时稍加搜索,就能知道其需要的参数。 153 | 154 | 除此之外还有个 `__divmod__` 方法,它把除数和余数运算结果结合起来,返回一个包含商和余数的元组,也就是把 `__floordiv__` 和 `__mod__` 的作用融合在一起了。 155 | 156 | ## 反算数运算符 157 | 158 | 上面探讨的算数运算符要求位于运算符前面的对象实现,比如: 159 | 160 | ```python 161 | first + second 162 | ``` 163 | 164 | 这个式子中,要求 `first` 必须实现 `__add__` 方法。 165 | 166 | 但是如果你没办法保证 `first` 的具体实现,那么也可以在 `second` 实现**反算数运算符** `__radd__`,达到同样的效果。 167 | 168 | > 应用场景举例:`second` 是库提供的,而 `first` 是用户自行编写,没办法保证其能够实现 `__add__`。 169 | 170 | 反算数运算的名称就是在正常算数运算符前面加字母 `r`,比如 `__radd__` 、 `__rsub__`,就不展开讲了。 171 | 172 | ## 增量赋值符 173 | 174 | 与反算数运算符类似的还有**增量赋值符**,比如最常用的 `+=`: 175 | 176 | ```python 177 | class Vector: 178 | def __init__(self, x, y): 179 | self.x = x 180 | self.y = y 181 | 182 | def __iadd__(self, other): 183 | return Vector(self.x + other, self.y + other) 184 | 185 | 186 | v1 = Vector(-1, 2) 187 | v1 += 1 188 | print(v1.x, v1.y) 189 | # 输出: 190 | # 0 3 191 | ``` 192 | 193 | 其他的增量赋值符的还有 `__isub__` 、 `__imul__` 等,也不展开列举了。 194 | 195 | ## 一元运算符 196 | 197 | 一元运算符只有一个操作符。 198 | 199 | 还是拿上面的 `Vector` 类举例,比方说我要实现**取负**的操作: 200 | 201 | ```python 202 | class Vector: 203 | def __neg__(self): 204 | return Vector(-self.x, -self.y) 205 | 206 | # 其他方法 207 | # ... 208 | ``` 209 | 210 | 那么就可以这样取负了: 211 | 212 | ```python 213 | >>> v1 = Vector(-1, 2) 214 | >>> v2 = -v1 215 | 216 | >>> print(v2.x, v2.y) 217 | 1 -2 218 | ``` 219 | 220 | 类似的一元运算符还有: 221 | 222 | - `__pos__`:取正,比如 `+v1` 。 223 | - `__abs__`:取绝对值,如 `abs(v1)`。 224 | - `__invert__`:实现取反操作符 `~`,二进制逐位取反。 225 | - `__complex__`:实现内建函数 `complex()`,取复数。 226 | - `__int__`:实现内建函数 `int()`,整型转换。 227 | - `__float__`:实现内建函数 `float()`,浮点数转换。 228 | - `__round__`:实现内建函数 `round()`,四舍五入。 229 | - `__ceil__`:实现内建函数 `math.ceil()`,大于原始值的最小整数。 230 | - `__floor__`:实现内建函数 `math.floor()`,小于原始值的最大整数 231 | - `__trunc__`:实现内建函数 `math.trunc()`,朝零取整 232 | - `__index__`:作为列表索引的数字。 233 | 234 | 稍微难理解的只有最后这个 `__index__`,用一个例子说明: 235 | 236 | ```python 237 | class Index: 238 | def __index__(self): 239 | return 1 240 | 241 | my_list = [0, 1, 2] 242 | index = Index() 243 | 244 | print(my_list[index]) 245 | # 输出: 246 | # 1 247 | ``` 248 | 249 | ## 总结 250 | 251 | 以上就是常见运算符魔法方法的用法了。 252 | 253 | 可能你觉得实现个加减乘除没多大用处。别急,这只是魔法方法中非常小的一部分。让我们下一章继续深入。 254 | 255 | --- 256 | 257 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 258 | 259 | 看完文章想吐槽?欢迎留言告诉我! 260 | -------------------------------------------------------------------------------- /40-类的表示.md: -------------------------------------------------------------------------------- 1 | 使用**字符串**等信息来表示类是一个相当实用的特性。比方说你在调试代码时,会频繁使用 `print()` 等函数来获取对象信息,其背后就是隐式调用了将类转化为字符串的魔法方法。相对应的,还有另一部分魔法方法用于自定义在使用内建函数时类的行为。 2 | 3 | ## 基础方法 4 | 5 | Python 中将对象转换为字符串有两个类似的魔法方法,即 `__str__` 和 `__repr__` 。 6 | 7 | 它两有什么区别呢?让我们先看结论: 8 | 9 | - `__str__` 注重**可读性**,比如展示给用户。 10 | - `__repr__` 注重**明确性**,比如展示给开发中的程序员。 11 | 12 | 举个栗子。假设有一个表示当前时间的类: 13 | 14 | ```python 15 | from datetime import datetime 16 | 17 | class MyDate: 18 | def __init__(self): 19 | self.date = datetime.now() 20 | 21 | f = MyDate() 22 | 23 | print(f) 24 | print(f.__repr__()) 25 | print(f.__str__()) 26 | 27 | # 输出: 28 | # <__main__.MyDate object at 0x000002510231AFA0> 29 | # <__main__.MyDate object at 0x000002510231AFA0> 30 | # <__main__.MyDate object at 0x000002510231AFA0> 31 | ``` 32 | 33 | 打印的结果不明确,得不到我想要的跟时间有关的信息。 34 | 35 | 增加 `__str__` 方法后: 36 | 37 | ```python 38 | from datetime import datetime 39 | 40 | class MyDate: 41 | def __init__(self): 42 | self.date = datetime.now() 43 | 44 | def __str__(self): 45 | return self.date.__str__() 46 | 47 | f = MyDate() 48 | print(f) 49 | print(f.__repr__()) 50 | print(f.__str__()) 51 | 52 | # 输出: 53 | # 2021-06-30 19:49:56.620427 54 | # <__main__.MyDate object at 0x00000251026C3CA0> 55 | # 2021-06-30 19:49:56.620427 56 | ``` 57 | 58 | 打印 `f` 或者 `f.__str__()` 均能够显示格式化后的时间信息,但是无法得知具体的类型。 59 | 60 | 如果只实现 `__repr__` ,则有: 61 | 62 | ```python 63 | from datetime import datetime 64 | 65 | class MyDate: 66 | def __init__(self): 67 | self.date = datetime.now() 68 | 69 | def __repr__(self): 70 | return self.date.__repr__() 71 | 72 | f = MyDate() 73 | print(f) 74 | print(f.__repr__()) 75 | print(f.__str__()) 76 | 77 | # 输出: 78 | # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960) 79 | # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960) 80 | # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960) 81 | ``` 82 | 83 | 三种打印方式均被 `__repr__` 覆盖,不仅显示了时间信息,也可得知具体的类型。 84 | 85 | 如果两种魔法方法同时实现: 86 | 87 | ```python 88 | from datetime import datetime 89 | 90 | class MyDate: 91 | def __init__(self): 92 | self.date = datetime.now() 93 | 94 | def __str__(self): 95 | return self.date.__str__() 96 | 97 | def __repr__(self): 98 | return self.date.__repr__() 99 | 100 | f = MyDate() 101 | print(f) 102 | print(f.__repr__()) 103 | print(f.__str__()) 104 | 105 | # 输出: 106 | # 2021-06-30 19:54:57.350076 107 | # datetime.datetime(2021, 6, 30, 19, 54, 57, 350076) 108 | # 2021-06-30 19:54:57.350076 109 | ``` 110 | 111 | 总的来说两者中可优先实现 `__repr__` ,有需要再实现 `__str__`。 112 | 113 | 此外,还有两个常用的方法 `__dir__` 和 `__dict__` 。 114 | 115 | `__dir__` 定义了调用 `dir()` 时的行为,返回对象的属性、方法的列表: 116 | 117 | ```python 118 | >>> a = 1 119 | >>> a.__dir__() 120 | ['__repr__', 121 | '__hash__', 122 | '__getattribute__', 123 | '__lt__', 124 | '__le__', 125 | '__eq__', 126 | '__ne__', 127 | '__gt__', 128 | '__ge__', 129 | '__add__', 130 | '...'] 131 | ``` 132 | 133 | 而 `__dict__` 则会输出所有实例属性组成的字典: 134 | 135 | ```python 136 | class Bar: 137 | def __init__(self, a, b): 138 | self.a = a 139 | self.b = b 140 | 141 | b = Bar(1, 2) 142 | print(b.__dict__) 143 | # 输出: 144 | # {'a': 1, 'b': 2} 145 | ``` 146 | 147 | ## bytes 和 format 和 bool 148 | 149 | 理解了前面的内容,再来说类似的方法就简单了。 150 | 151 | `__bytes__` 方法实现了通过 `bytes()` 获取对象字节序列的表示形式。而 `__format__` 方法被内置的 `format()` 或 `str.format()` 调用,获取对象的格式化后的字符串表示形式。 152 | 153 | 看例子: 154 | 155 | ```python 156 | from datetime import datetime 157 | 158 | class MyDate: 159 | def __init__(self): 160 | self.date = datetime.now() 161 | 162 | def __bytes__(self): 163 | return b'This is a bytes result' 164 | 165 | def __format__(self, format_spec): 166 | return 'The time is: ' + format(self.date, format_spec) 167 | 168 | f = MyDate() 169 | 170 | print(bytes(f)) 171 | print(format(f, '%H:%M:%S')) 172 | 173 | # 输出: 174 | # b'This is a bytes result' 175 | # The time is: 10:15:36 176 | ``` 177 | 178 | 而 `__bool__` 就更简单了,它负责实现内置的 `bool()` 方法: 179 | 180 | ```python 181 | class Foo: 182 | def __bool__(self): 183 | return False 184 | 185 | f = Foo() 186 | print(bool(f)) 187 | # 输出: 188 | # False 189 | ``` 190 | 191 | 如果类没有实现 `__bool__` ,那么调用 `bool()` 会检查类的 `__len__` ,非零则返回 `True` 。 192 | 193 | 如果连 `__len__` 也没实现,则会直接返回 `True` 。 194 | 195 | ## hash可哈希 196 | 197 | `__hash__` 这个稍微复杂点,放到最后来说。 198 | 199 | `Hash` ,一般称作**散列**或**哈希**。 200 | 201 | **哈希算法**是用来解决数据与数据之间对应关系的一种算法。它可以将任意长度的输入变换为固定长度的输出,该输出被称为哈希值。简单来说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。而实现了哈希算法的对象,就被称为**可哈希的**或者**可散列的**。 202 | 203 | Python 中的**不可变类型**通常都是可哈希的,比如数字、字母、字符串、元组等。 204 | 205 | **可变类型**通常是不可哈希的,比如列表、字典、集合等。 206 | 207 | Python 的三种数据结构:`set` 、 `frozenset` 和 `dict` 都是要求其键值是可哈希的,因为要保证键的唯一性。 208 | 209 | 如果自定义对象需要实现可哈希,那么就必须实现 `__hash__` 方法。 210 | 211 | 我们自定义一个矢量类作为例子: 212 | 213 | ```python 214 | class Vector: 215 | # 用于哈希算法的属性就像一个id 216 | # 改变id会导致对象的身份混乱 217 | # 因此将其标识为只能读取的私有变量 218 | def __init__(self, x, y): 219 | self.__x = x 220 | self.__y = y 221 | 222 | @property 223 | def x(self): 224 | return self.__x 225 | 226 | @property 227 | def y(self): 228 | return self.__y 229 | 230 | # 根据官方文档建议 231 | # 哈希算法最好作用于输入值的元组上 232 | # 以使得哈希值更加随机 233 | def __hash__(self): 234 | return hash((self.__x, self.__y)) 235 | 236 | # 实现__hash__ 必须同时实现 __eq__ 237 | def __eq__(self, other): 238 | return self.__x == other.__x and self.__y == other.__y 239 | 240 | # 格式化打印输出 241 | def __repr__(self): 242 | return f'(x: {self.__x}, y: {self.__y})' 243 | 244 | 245 | # 注意 v1 和 v2 的矢量值相同 246 | # 因此哈希函数计算结果也相同 247 | # 那么在集合中则会被归为同一个元素 248 | v1 = Vector(1,2) 249 | v2 = Vector(1,2) 250 | v3 = Vector(2,3) 251 | 252 | s = set([v1, v2, v3]) 253 | print(s) 254 | # 输出: 255 | # {(x: 2, y: 3), (x: 1, y: 2)} 256 | ``` 257 | 258 | 可以看到这个自定义的类实现了可哈希化,并且顺利的放到了集合 `set` 中。 259 | 260 | 需要注意的是,如果类实现了 `__hash__` ,那么它也必须同时实现 `__eq__` ,因为键的唯一性是由它两一起参与验证的。并且你还需要保证 `x==y` 和 `hash(x) == hash(y)` 是等效的。 261 | 262 | --- 263 | 264 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 265 | 266 | 看完文章想吐槽?欢迎留言告诉我! 267 | -------------------------------------------------------------------------------- /60-容器.md: -------------------------------------------------------------------------------- 1 | 容器是 Python 中的一个抽象概念,可以简单理解为**包含其他对象的对象**。常见的四种内置容器类型为列表、元组、字典、集合。 2 | 3 | 除了内置容器类型外,Python 也允许你通过实现对应的魔法方法,来自定义容器。 4 | 5 | 让我们展开探讨。 6 | 7 | ## 不可变序列 8 | 9 | **序列**是指一种**包含有序对象的容器**,比如列表。 10 | 11 | 要实现一个最简单的序列,只需要实现 `__len__` 和 `__getitem__` 方法即可: 12 | 13 | ```python 14 | class Seq: 15 | def __init__(self, values=[]): 16 | self.values = values 17 | 18 | def __len__(self): 19 | return len(self.values) 20 | 21 | def __getitem__(self, key): 22 | return self.values[key] 23 | ``` 24 | 25 | 因为没有规定如何去修改容器中的对象,因此这是一个**不可变序列**。其中的 `__len__` 方法定义序列的大小,`__getitem__` 方法定义如何对容器进行取值。 26 | 27 | 测试下: 28 | 29 | ```python 30 | >>> seq = Seq(values=[1, 2, 3]) 31 | >>> len(seq) 32 | 3 33 | >>> seq[1] 34 | 2 35 | >>> seq[0:2] + seq[1:3] 36 | [1, 2, 2, 3] 37 | ``` 38 | 39 | 这已经表现得有点像普通的列表了,对吧? 40 | 41 | 除此之外,对序列进行**迭代**是非常基本的需求,上面这个自定义序列似乎没有哪里实现了迭代的功能,它能够正常迭代吗? 42 | 43 | 再试试: 44 | 45 | ```python 46 | >>> for item in seq: 47 | ... print(item) 48 | 49 | # 输出: 50 | 1 51 | 2 52 | 3 53 | ``` 54 | 55 | 居然很顺利的迭代出了容器中的值。原因在于 Python 处理迭代时的流程: 56 | 57 | - 首先检查对象是否实现了 `__iter__` 方法。 58 | - 若实现了 `__iter__`,则通过此方法返回的迭代器进行迭代。 59 | - 若未实现 `__iter__` ,则遍历 `__getitem__` 中所有的值进行迭代。 60 | 61 | 也就是说,虽然没实现 `__iter__` 方法,但是 Python 的“保底机制”让迭代得以成功。 62 | 63 | 类似的还有判断容器是否包含某元素的 `in` 语句: 64 | 65 | ```python 66 | >>> 1 in seq 67 | True 68 | >>> 4 in seq 69 | False 70 | ``` 71 | 72 | `in` 语句的执行流程: 73 | 74 | - 如果容器定义了 `__contains__` ,则根据此方法判断是否包含当前元素。 75 | - 如果未定义 `__contains__` 但定义了 `__iter__` ,则遍历迭代器的值进行判断。 76 | - 如果都没定义,那就遍历 `__getitem__` 中的值进行判断。 77 | 78 | 除此之外,用于颠倒元素顺序的 `reversed()` 也可以用: 79 | 80 | ```python 81 | >>> re = reversed(seq) 82 | >>> for item in re: 83 | ... print(item) 84 | 85 | # 输出: 86 | 3 87 | 2 88 | 1 89 | ``` 90 | 91 | 它也是同样的道理,由 `__len__` 和 `__getitem__` 配合,隐式实现了 `__reversed__` 方法。 92 | 93 | 虽然上述方法可以隐式实现,但从效率的角度考虑,还是建议手动实现(Python 隐式机制使用穷举,但是手动实现可以运用优化取值的方法)。把它们都补充完整,则一个简单的不可变序列差不多是这样: 94 | 95 | ```python 96 | class Seq: 97 | def __init__(self, values=[]): 98 | self.values = values 99 | 100 | def __len__(self): 101 | return len(self.values) 102 | 103 | def __getitem__(self, key): 104 | return self.values[key] 105 | 106 | def __iter__(self): 107 | return iter(self.values) 108 | 109 | def __reversed__(self): 110 | return reversed(self.values) 111 | 112 | def __contains__(self, item): 113 | return item in self.values 114 | ``` 115 | 116 | ## 可变序列 117 | 118 | **可变序列**只需要在不可变序列的基础上,增加 `__setitem__` 和 `__delitem__` 以定义如何修改和删除容器中的元素。 119 | 120 | 此外,也推荐实现 `append()` 、 `insert()` 、 `pop()` 等方法,和 Python 内置的序列保持一致。 121 | 122 | 比如下面这个自定义的可变序列: 123 | 124 | ```python 125 | class Seq: 126 | def __init__(self, values=[]): 127 | self.values = values 128 | 129 | def __len__(self): 130 | return len(self.values) 131 | 132 | def __getitem__(self, key): 133 | return self.values[key] 134 | 135 | def __setitem__(self, key, value): 136 | self.values[key] = value 137 | 138 | def __delitem__(self, key): 139 | del self.values[key] 140 | 141 | def append(self, value): 142 | self.values.append(value) 143 | ``` 144 | 145 | ## 自带电池 146 | 147 | 虽然我们可以通过实现魔法方法的形式来自定义几乎所有类型的容器,但那实在是没有必要,因为 Python 有丰富的标准库,开箱即用非常强大。 148 | 149 | 比方说要实现**可变字典**,不需要自己造轮子实现底层细节,直接继承 `collections.abc.MutableMapping` 并实现上面那几个基础的魔法方法即可: 150 | 151 | ```python 152 | from collections.abc import MutableMapping 153 | 154 | class MyDict(MutableMapping): 155 | def __init__(self, **kwargs): 156 | self.data = kwargs 157 | 158 | def __getitem__(self, key): 159 | return self.data[key] 160 | 161 | def __delitem__(self, key): 162 | del self.data[key] 163 | 164 | def __setitem__(self, key, value): 165 | self.data[key] = value 166 | 167 | def __iter__(self): 168 | return iter(self.data) 169 | 170 | def __len__(self): 171 | return len(self.data) 172 | 173 | def __repr__(self): 174 | return repr(self.data) 175 | 176 | my_dict = MyDict(a=1, b=2) 177 | my_dict.update(c=3) 178 | my_dict['d'] = 4 179 | my_dict.pop('b') 180 | 181 | print(my_dict) 182 | # 输出: 183 | # {'a': 1, 'c': 3, 'd': 4} 184 | ``` 185 | 186 | 这个字典类自动从父类 `MutableMapping` 里继承了 `.update()` 、 `.pop()` 、 `.get()` 等基础的方法。 187 | 188 | 除了 `MutableMapping` 外,标准库还提供了更高层级的封装,即 `UserDict` : 189 | 190 | ```python 191 | from collections import UserDict 192 | 193 | class MyDict(UserDict): 194 | def __setitem__(self, key, value): 195 | super().__setitem__(key, value * 10) 196 | 197 | 198 | my_dict = MyDict(a=1, b=2) 199 | print(my_dict) 200 | # 输出: 201 | # {'a': 10, 'b': 20} 202 | ``` 203 | 204 | `UserDict` 将整个数据结构及方法都默认实现了,要改哪个行为,直接覆写对应的方法就好了,很方便。 205 | 206 | 因此,如果你要自定义容器,非常推荐先在 `collections` 模块里找找现成的轮子,比如 `UserDict` 、 `UserList` 、 `OrderedDict` 等,顺便学习下源码。 207 | 208 | ## 再看容器 209 | 210 | Python 是一门鸭子类型的语言:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 211 | 212 | 翻译成人话就是:只要一个**对象**满足了该**类型**的特定**协议**,那么此**对象**就可以被当成该**类型**使用。 213 | 214 | 这又引出另外一个问题,什么叫**协议**?协议即某种特定的规范。举个栗子,我们定义“会游泳的动物是鸭子”,这句话里面的“动物”是对象,“鸭子”是类型,“会游泳的”是协议。 215 | 216 | 理解了这个,那再来看看[Python文档](https://docs.python.org/dev/library/collections.abc.html#collections-abstract-base-classes)中,在**协议**层面上是如何定义容器的:只要对象满足 `__contains__` 协议,那就认为是容器*[注释1]*。也就是说下面这就是个简单的容器: 217 | 218 | ```python 219 | class Container: 220 | def __contains__(self, value): 221 | if value == 1: 222 | return True 223 | return False 224 | 225 | container = Container() 226 | 227 | print(1 in container) 228 | print(2 in container) 229 | # 输出: 230 | # True 231 | # False 232 | ``` 233 | 234 | 你可能会问:那前面实现的自定义序列也没实现 `__contains__` 协议啊?原因是 Python 的幕后机制帮你实现了。 235 | 236 | > 想了解具体机制请看官方文档[成员测试机制](https://docs.python.org/3/reference/expressions.html#membership-test-details)。 237 | 238 | 再看看几种内置的容器需要满足的协议: 239 | 240 | - **列表**:满足 `MutableSequence` 、 `Sequence` 、`Reversible` 、 `Collection` 等协议。 241 | - **元组**:满足 `Sequence` 、`Reversible` 、 `Collection` 等协议。 242 | - **字典**:满足 `MutableMapping` 、 `Mapping` 、`Collection` 等协议。 243 | - **集合**:满足 `MutableSet` 、 `Set` 、 `Collection` 等协议。 244 | 245 | 建议阅读官方的[collections.abc](https://docs.python.org/dev/library/collections.abc.html#collections-abstract-base-classes)这个文档,里面把各种容器所需要满足的协议规定得明明白白,还有很多提供给你做基类的宝藏,比如上面用到过的 `MutableMapping` 。 246 | 247 | > 包括前面章节里出现过的魔法方法,都可以理解为某种协议的一部分。 248 | 249 | 明白了协议的概念后,我们就接触到面向对象编程的最重要的法则之一:**面向协议编程,而非面向具体的实现**。 250 | 251 | 举个栗子。某日你要写一个函数,此函数接收一个仅有两个元素的列表,并将所有元素计算后返回。你的代码可能是这样: 252 | 253 | ```python 254 | def foo(container): 255 | container[0] = container[0] * 10 256 | container[1] = container[1] * 10 257 | return container 258 | 259 | a = [1, 2] 260 | print(foo(a)) 261 | # 输出: 262 | # [10, 20] 263 | ``` 264 | 265 | 这样写的问题是此函数严重依赖的**列表**这个容器的具体实现:`foo()` 接收的容器首先要**有序**,其次还要**可变**。万一客户提出个新需求,要求输入参数也可能是个**集合**,这函数就抓瞎了。 266 | 267 | 那应该怎么改? 268 | 269 | 首先让我们观察[collections.abc](https://docs.python.org/dev/library/collections.abc.html#collections-abstract-base-classes)里对容器的定义。你会发现几种容器都满足 `Collection` 协议,而此协议中又包含了 `Iterable` 协议。也就是说,几种内置的容器都是可迭代的。 270 | 271 | 因此,函数就应该在 `Iterable` 协议上做文章,输入的容器仅依赖此协议就OK了: 272 | 273 | ```python 274 | def foo(container): 275 | data = [] 276 | for item in container: 277 | data.append(item * 10) 278 | same_cls = type(container) 279 | return same_cls(data) 280 | 281 | 282 | a = [1, 2] 283 | b = (3, 4) 284 | c = set([5, 6]) 285 | 286 | for item in [a, b, c]: 287 | print(foo(item)) 288 | 289 | # 输出: 290 | # [10, 20] 列表 291 | # (30, 40) 元组 292 | # {50, 60} 集合 293 | ``` 294 | 295 | 新函数不再需要列表这个具体的对象,而是仅依赖可迭代协议,并且通过 `type()` 动态创建不同类型的容器,函数的复用性变得更好了。新函数能够同时提供对列表和集合的支持,甚至元组也没有问题。 296 | 297 | 希望通过这个拙劣的例子,可以帮你理解“面向协议编程”的优势。 298 | 299 | > 关于 `type()` 动态生成类的探讨,请看我以前的文章[Python元类入门](https://www.dusaiphoto.com/article/142/)。 300 | 301 | ## 总结 302 | 303 | 本文通过对自定义容器中需要实现的魔法方法的探讨,介绍了关于面向协议编程的概念。总结如下: 304 | 305 | - 定义对应的魔法方法,可以定义自定义容器。 306 | - `collections.abc` 和 `collections` 模块中包含丰富的用于自定义容器的基类。 307 | - Python 是鸭子类型的语言。 308 | - 面向协议而非面向具体实现进行编程。 309 | 310 | ## 注释 311 | 312 | [1] 用 `in` 运算符来判断对象是否为容器并不严谨,比如**生成器**或**文件**对象也是支持 `in` 的,但通常并不认为它们也是容器。更多讨论见[SO](https://stackoverflow.com/questions/11575925/what-exactly-are-containers-in-python-and-what-are-all-the-python-container)。 313 | 314 | ## 参考 315 | 316 | - [Custom Dict](https://frostming.com/2021/05-19/custom-dict/) 317 | - [mastering-container-types](https://www.zlovezl.cn/articles/mastering-container-types/) 318 | - [magic methods](https://github.com/RafeKettler/magicmethods) 319 | 320 | --- 321 | 322 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 323 | 324 | 看完文章想吐槽?欢迎留言告诉我! 325 | 326 | -------------------------------------------------------------------------------- /70-描述符.md: -------------------------------------------------------------------------------- 1 | **描述符**是 Python 语言中一个强大的特性,它隐藏在编程语言的底层,为许多神奇的魔法提供了动力。 2 | 3 | 如果你认为它只是个花里胡哨、且不太能用到的高级主题,那么本文将帮助你了解为什么描述符是一个非常有意思、并且让代码变简洁的优雅工具。 4 | 5 | ## 一个例子 6 | 7 | 在探讨枯燥的理论前,让我们从一个简单的例子来了解描述符。 8 | 9 | 某日,假设你需要一个类,来记录数学考试的分数。 10 | 11 | 这个需求非常简单,你10秒钟就写好了代码: 12 | 13 | ```python 14 | class Score: 15 | def __init__(self, math): 16 | self.math = math 17 | ``` 18 | 19 | 但是稍后你就发现了问题:分数为负值是没有意义的。 20 | 21 | 但显然上面的代码对输入参数没有任何检查: 22 | 23 | ```python 24 | >>> score = Score(-90) 25 | >>> score.math 26 | -90 27 | ``` 28 | 29 | 因此你修改代码,使得初始化时检查输入值: 30 | 31 | ```python 32 | class Score: 33 | def __init__(self, math): 34 | if math < 0: 35 | raise ValueError('math score must >= 0') 36 | self.math = math 37 | ``` 38 | 39 | 但这样也没解决问题,因为分数虽然在初始化时不能为负,但后续修改时还是可以输入非法值: 40 | 41 | ```python 42 | >>> score = Score(90) 43 | >>> score.math 44 | 90 45 | 46 | >>> score.math = -100 47 | >>> score.math 48 | -100 49 | ``` 50 | 51 | 幸运的是,有内置装饰器 `@property` 可以解决此问题。 52 | 53 | 如果你以前没用过 `@property` ,下面就是个例子: 54 | 55 | ```python 56 | class Score: 57 | def __init__(self, math): 58 | self.math = math 59 | 60 | @property 61 | def math(self): 62 | # self.math 取值 63 | return self._math 64 | 65 | @math.setter 66 | def math(self, value): 67 | # self.math 赋值 68 | if value < 0: 69 | raise ValueError('math score must >= 0') 70 | self._math = value 71 | ``` 72 | 73 | 试验下: 74 | 75 | ```python 76 | >>> score = Score(90) 77 | >>> score.math 78 | 90 79 | 80 | >>> score.math = 10 81 | >>> score.math 82 | 10 83 | 84 | >>> score.math = -10 85 | Traceback (most recent call last): 86 | File "...", line 20, in math 87 | raise ValueError('math score must >= 0') 88 | 89 | ValueError: math score must >= 0 90 | ``` 91 | 92 | 简单来说就是 `@property` 接管了对 `math` 属性的直接访问,而是将对应的取值赋值转交给 `@property` 封装的方法。 93 | 94 | 虽然 `@property` 已经表现得比较完美了,但是它最大的问题是不能重用。 95 | 96 | 如果要同时保存数学、英语、生物三门课程的成绩,这个类就会变成这样: 97 | 98 | ```python 99 | class Score: 100 | def __init__(self, math, english, bio): 101 | self.math = math 102 | self.english = english 103 | self.bio = bio 104 | 105 | @property 106 | def math(self): 107 | return self._math 108 | 109 | @math.setter 110 | def math(self, value): 111 | if value < 0: 112 | raise ValueError('math score must >= 0') 113 | self._math = value 114 | 115 | @property 116 | def english(self): 117 | return self._english 118 | 119 | @english.setter 120 | def english(self, value): 121 | if value < 0: 122 | raise ValueError('english score must >= 0') 123 | self._english = value 124 | 125 | @property 126 | def bio(self): 127 | return self._bio 128 | 129 | @bio.setter 130 | def bio(self, value): 131 | if value < 0: 132 | raise ValueError('bio score must >= 0') 133 | self._bio = value 134 | ``` 135 | 136 | 虽然外部调用时依然简洁,但掩盖不了类内部的臃肿。 137 | 138 | **描述符**就可以很好的解决上面的代码重用问题。 139 | 140 | 描述符这个词听起来很玄乎,其实就是实现了魔法方法 `__get__` 、 `__set__` 、 `__delete__` 的类(根据需求,可以只实现其中一部分方法,不一定三个都实现)。一但实现了描述符协议,那么这个类就具有非常强大的特性了。 141 | 142 | 比如上面这个检查非负的需求,写成**描述符类**就是这样: 143 | 144 | ```python 145 | class NonNegative: 146 | """检查输入值不能为负""" 147 | def __init__(self, name): 148 | self.name = name 149 | 150 | def __get__(self, instance, owner=None): 151 | return instance.__dict__.get(self.name) 152 | 153 | def __set__(self, instance, value): 154 | if value < 0: 155 | raise ValueError(f'{self.name} score must >= 0') 156 | instance.__dict__[self.name] = value 157 | ``` 158 | 159 | 里面的细节后面会讲到,现在你只需要注意以下几点: 160 | 161 | - 它实现了 `__get__` 用于取值,也实现了 `__set__` 用于赋值。因此它是一个描述符类。 162 | - 在 `__set__` 中对输入值 `value` 进行了检查,确保非负。 163 | 164 | 像这样来使用描述符: 165 | 166 | ```python 167 | class Score: 168 | math = NonNegative('math') 169 | english = NonNegative('english') 170 | bio = NonNegative('bio') 171 | 172 | def __init__(self, math, english, bio): 173 | self.math = math 174 | self.english = english 175 | self.bio = bio 176 | ``` 177 | 178 | 现在,`math` 、`english` 、 `bio` 三个属性均被描述符接管。也就是说,对它们进行点符的访问实际上会执行描述符类中对应的 `__get__` 、 `__set__` 方法。 179 | 180 | 试试其功能,与 `@property` 是类似的: 181 | 182 | ```python 183 | >>> score.math = 10 184 | >>> score.math 185 | 10 186 | 187 | >>> score.math = -10 188 | Traceback (most recent call last): 189 | File "...", line 1, in 190 | score.math = -10 191 | 192 | ValueError: math score must >= 0 193 | ``` 194 | 195 | 功能虽然相同,但是 `Score` 类的定义明显清爽了不少。 196 | 197 | ## 描述符类型 198 | 199 | 在开始讨论本节之前,让我们先回顾一点基础知识。 200 | 201 | Python 的类具有一个特殊的字典叫 `__dict__` ,它被称作**命名空间**,说白了就是一个存放对象所有属性的字典。 202 | 203 | 对属性的引用被解释器转换为对该字典的查找,比如 `a.x` 相当于 `a.__dict__['x']` 。看下面的例子: 204 | 205 | ```python 206 | class Foo: 207 | def __init__(self): 208 | self.a = 10 209 | self.b = 20 210 | 211 | foo = Foo() 212 | 213 | print(foo.__dict__) 214 | # {'a': 10, 'b': 20} 215 | 216 | foo.__dict__['c'] = 30 217 | 218 | print(foo.__dict__) 219 | # {'a': 10, 'b': 20, 'c': 30} 220 | 221 | print(foo.c) 222 | # 30 223 | ``` 224 | 225 | 可以看到在程序运行期间,你可以动态的向 `__dict__` 中插入新的值,使得对象具有新的属性。 226 | 227 | > 了解完这个,我们再回到描述符的话题。 228 | 229 | 描述符可以用一句话概括:**描述符是可重用的属性,它把函数调用伪装成对属性的访问**。 230 | 231 | 描述符可以只实现 `__get__` 方法: 232 | 233 | ```python 234 | class Ten: 235 | """非数据描述符""" 236 | def __get__(self, instance, owner=None): 237 | print(self) 238 | print(instance) 239 | print(owner) 240 | return 10 241 | 242 | class Foo: 243 | """应用了描述符的类""" 244 | ten = Ten() 245 | 246 | 247 | foo = Foo() 248 | print(foo.ten) 249 | # 输出: 250 | # <__main__.Ten object at 0x0000023B4B074EB0> 251 | # <__main__.Foo object at 0x0000023B4B074940> 252 | # 253 | # 10 254 | ``` 255 | 256 | `__get__` 方法中有三个参数: 257 | 258 | - `self` :描述符实例 259 | - `instance` :描述符所附加的对象的实例 260 | - `owner` :描述符所附加的对象的类型 261 | 262 | 这种只实现 `__get__` 方法的叫做**非数据描述符**。 263 | 264 | 如果描述符定义了 `__set__` 或者 `__delete__` ,则被叫做**数据描述符**。比如: 265 | 266 | ```python 267 | class Five: 268 | """数据描述符""" 269 | def __get__(self, instance, owner=None): 270 | return 5 271 | 272 | def __set__(self, instance, value): 273 | raise AttributeError('Cannot change this value') 274 | ``` 275 | 276 | `__set__` 方法中也有三个参数: 277 | 278 | - `self` :描述符实例 279 | - `instance` :描述符所附加的对象的实例 280 | - `value` :当前准备赋的值 281 | 282 | **数据描述符**和**非数据描述符**不仅仅是名字上的区别,更重要的是在**查找链**上的位置不同。 283 | 284 | 当访问对象的某个属性时,其查找链简单来说就是: 285 | 286 | - 首先在对应的**数据描述符**中查找此属性。 287 | - 如果失败,则在对象的 `__dict__` 中查找此属性。 288 | - 如果失败,则在**非数据描述符**中查找此属性。 289 | - 如果失败,再去别的地方查找。(本文就不展开了) 290 | 291 | 问题来了:根据以上查找规则,上面定义的两个描述符 `Ten` 和 `Five` ,哪个能作为**只读**属性? 292 | 293 | 答案是 `Five` 。 294 | 295 | 由于 `Ten` 没有设置 `__set__` 方法,因此对属性的赋值和取值会被对象的 `__dict__` 的属性所覆盖: 296 | 297 | ```python 298 | class Ten: 299 | def __get__(self, instance, owner=None): 300 | print('calling __get__') 301 | return 10 302 | 303 | class Foo: 304 | ten = Ten() 305 | 306 | foo = Foo() 307 | 308 | print(foo.ten) 309 | # calling __get__ 310 | # 10 311 | 312 | foo.ten = 20 313 | print(foo.ten) 314 | # 20 315 | ``` 316 | 317 | 但是由于数据描述符的查找要早于对象的 `__dict__` ,因此拦截了对属性的访问: 318 | 319 | ```python 320 | class Five: 321 | def __get__(self, instance, owner=None): 322 | print('calling __get__') 323 | return 5 324 | 325 | def __set__(self, instance, value): 326 | raise AttributeError('Cannot change this value') 327 | 328 | class Bar: 329 | five = Five() 330 | 331 | bar = Bar() 332 | 333 | print(bar.five) 334 | # calling __get__ 335 | # 5 336 | 337 | bar.five = 20 338 | # Traceback (most recent call last): 339 | # File "...", line 23, in __set__ 340 | # raise AttributeError('Cannot change this value') 341 | # AttributeError: Cannot change this value 342 | ``` 343 | 344 | ## 共享陷阱 345 | 346 | 描述符有一个非常迷惑人的特性:在同一个类中每个描述符**仅实例化一次**,也就是说所有实例**共享**该描述符实例。 347 | 348 | 看下面这个例子就明白了: 349 | 350 | ```python 351 | class NonNegative: 352 | """检查输入值不能为负""" 353 | def __get__(self, instance, owner=None): 354 | return self.value 355 | 356 | def __set__(self, instance, value): 357 | if value < 0: 358 | raise ValueError(f'{self.name} score must >= 0') 359 | # 数据被绑定在描述符实例上 360 | # 由于描述符实例是共享的 361 | # 因此数据也只有一份被共享 362 | self.value = value 363 | 364 | 365 | class Score: 366 | math = NonNegative() 367 | 368 | def __init__(self, math): 369 | self.math = math 370 | 371 | 372 | score_1 = Score(10) 373 | score_2 = Score(20) 374 | 375 | # 所有对象共享同一个描述符实例 376 | print(score_1.math, score_2.math) 377 | # 输出: 20 20 378 | 379 | score_1.math = 30 380 | print(score_1.math, score_2.math) 381 | # 输出: 30 30 382 | ``` 383 | 384 | 修改某个实例的值后,所有实例跟着一起改变了。这**通常**不是你想要的结果。 385 | 386 | 要破除这种共享状态,比较好的解决方式是将数据绑定到**使用描述符的对象实例**上,就像本文开头的例子所做的那样: 387 | 388 | ```python 389 | class NonNegative: 390 | """检查输入值不能为负""" 391 | def __init__(self, name): 392 | self.name = name 393 | 394 | def __get__(self, instance, owner=None): 395 | return instance.__dict__.get(self.name) 396 | 397 | def __set__(self, instance, value): 398 | if value < 0: 399 | raise ValueError(f'{self.name} score must >= 0') 400 | # 数据被绑定在描述符附加的对象上 401 | # 因此保持了对象之间的数据隔离 402 | instance.__dict__[self.name] = value 403 | 404 | 405 | class Score: 406 | math = NonNegative('math') 407 | 408 | def __init__(self, math): 409 | self.math = math 410 | ``` 411 | 412 | 唯一有些不爽的是,为了给数据属性规定一个名字,在定义描述符的时候 `NonNegative('math')` 还得传递 `math` 这个名字进去,有点多此一举。 413 | 414 | 幸好 Python 3.6 为描述符引入了 `__set_name__` 方法,现在你可以这样: 415 | 416 | ```python 417 | class NonNegative: 418 | # 注意这里 419 | # __init__ 也没有了 420 | def __set_name__(self, owner, name): 421 | self.name = name 422 | 423 | def __get__(self, instance, owner=None): 424 | return instance.__dict__.get(self.name) 425 | 426 | def __set__(self, instance, value): 427 | if value < 0: 428 | raise ValueError(f'{self.name} score must >= 0') 429 | instance.__dict__[self.name] = value 430 | 431 | 432 | class Score: 433 | # NonNegative() 不需要带参数以规定属性名了 434 | math = NonNegative() 435 | 436 | def __init__(self, math): 437 | self.math = math 438 | ``` 439 | 440 | ## 应用场景 441 | 442 | 上面关于赋值检查的 `NonNegative` 已经展示描述符的其中一种用途了:**托管属性并复用代码,保持简洁**。 443 | 444 | 接下来看看另外一些描述符的典型应用场景。 445 | 446 | ## 缓存 447 | 448 | 假设你有一个耗时很长的操作,需要缓存其计算结果以便后续直接使用(而不是每次都傻乎乎的重新计算)。 449 | 450 | 描述符就可以实现这个缓存功能: 451 | 452 | ```python 453 | class Cache: 454 | """缓存描述符""" 455 | def __init__(self, func): 456 | self.func = func 457 | self.name = func.__name__ 458 | 459 | def __get__(self, instance, owner=None): 460 | instance.__dict__[self.name] = self.func(instance) 461 | return instance.__dict__[self.name] 462 | 463 | 464 | from time import sleep 465 | 466 | class Foo: 467 | @Cache 468 | def bar(self): 469 | sleep(5) 470 | return 'Just sleep 5 sec...' 471 | 472 | 473 | foo = Foo() 474 | # 第一次执行耗时约5秒 475 | print(foo.bar) 476 | # 第二次执行瞬间返回 477 | print(foo.bar) 478 | ``` 479 | 480 | 让我们花点时间看看到底发生了什么。 481 | 482 | 这个缓存功能得以实现的原因,还是在于 `Cache` 是个**非数据描述符**,还记得吗?非数据描述符的查找顺序要晚于 `__dict__` ,因此使得附加描述符的对象有机会在 `__dict__` 中写入数据,从而覆盖掉描述符中的耗时运算。 483 | 484 | > 如果给 `Cache` 增加 `__set__` 方法,还能实现缓存能力吗?欢迎自行尝试,并在评论区告诉我。 485 | 486 | 其次,这里以装饰器的形式应用了描述符。读过我的旧文[装饰器入门](https://www.dusaiphoto.com/article/139/)的读者都知道,装饰器就是语法糖。 487 | 488 | 上面这个装饰器: 489 | 490 | ```python 491 | @Cache 492 | def bar(self): 493 | ... 494 | ``` 495 | 496 | 等效于下面这句: 497 | 498 | ```python 499 | bar = Cache(bar) 500 | ``` 501 | 502 | 因此完成了描述符的定义(同时将**方法**转化成了**属性**),并且将原函数 `bar` 传递给了描述符的参数 `func` 。 503 | 504 | ## 验证器 505 | 506 | 让我们看看[官方文档](https://docs.python.org/zh-cn/3/howto/descriptor.html#complete-practical-example)给出的例子,如何用描述符实现一个规范的验证器。 507 | 508 | 首先定义一个仅具有基础功能的验证器抽象基类: 509 | 510 | ```python 511 | from abc import ABC, abstractmethod 512 | 513 | class Validator(ABC): 514 | """验证器抽象基类""" 515 | def __set_name__(self, owner, name): 516 | self.private_name = '_' + name 517 | 518 | def __get__(self, instance, owner=None): 519 | return getattr(instance, self.private_name) 520 | 521 | def __set__(self, instance, value): 522 | self.validate(value) 523 | setattr(instance, self.private_name, value) 524 | 525 | @abstractmethod 526 | def validate(self, value): 527 | pass 528 | ``` 529 | 530 | `Validator` 描述符类定义了 `validate` 方法,用于子类覆写以执行具体的验证逻辑。`__get__` 和 `__set__` 表明这是类是数据描述符。 531 | 532 | 写好这个基类,接下来就可以写实际用到的验证器子类了。 533 | 534 | 比如写两个子类: 535 | 536 | ```python 537 | class OneOf(Validator): 538 | """字符串单选验证器""" 539 | def __init__(self, *options): 540 | self.options = set(options) 541 | 542 | def validate(self, value): 543 | if value not in self.options: 544 | raise ValueError(f'Expected {value!r} to be one of {self.options!r}') 545 | 546 | class Number(Validator): 547 | """数值类型验证器""" 548 | def validate(self, value): 549 | if not isinstance(value, (int, float)): 550 | raise TypeError(f'Expected {value!r} to be an int or float') 551 | ``` 552 | 553 | `OneOf` 用于确保输入值为固定的某种类型。`Number` 用于确保输入值必须为数值型。它们均以 `Validator` 为父类,并实现了 `validate` 方法。 554 | 555 | 像这样使用它们: 556 | 557 | ```python 558 | class Component: 559 | kind = OneOf('wood', 'metal', 'plastic') 560 | quantity = Number() 561 | 562 | def __init__(self, kind, quantity): 563 | self.kind = kind 564 | self.quantity = quantity 565 | ``` 566 | 567 | 实际操作试试效果: 568 | 569 | ```python 570 | >>> Component('abc', 100) 571 | # 失败,'abc' 不在选择范围中 572 | ValueError: Expected 'abc' to be one of {'metal', 'plastic', 'wood'} 573 | 574 | >>> Component('wood', 'notNum') 575 | # 失败,'notNum' 不是数值型 576 | TypeError: Expected 'notNum' to be an int or float 577 | 578 | >>> Component('wood', 100) 579 | # 成功,参数均合法 580 | Out[25]: <__main__.Component at 0x13df8059640> 581 | ``` 582 | 583 | 再试试赋值: 584 | 585 | ```python 586 | >>> c = Component('wood', 100) 587 | 588 | >>> c.kind = 'abc' 589 | ValueError: Expected 'abc' to be one of {'metal', 'plastic', 'wood'} 590 | 591 | >>> c.kind 592 | 'wood' 593 | 594 | >>> c.kind = 'metal' 595 | >>> c.kind 596 | 'metal' 597 | 598 | >>> c.quantity = 'haha' 599 | TypeError: Expected 'haha' to be an int or float 600 | 601 | >>> c.quantity = 20 602 | >>> c.quantity 603 | 20 604 | ``` 605 | 606 | 很顺利的实现了验证器的功能。 607 | 608 | 学过 Django 的同学看着眼熟不,是不是有点 Django 中的验证器和字段的意思了?除此之外,很多底层的功能都可以用描述符进行纯 Python 的实现,比如属性、方法、静态方法、类方法等等。 609 | 610 | > 完整例子见文档[描述符指南](https://docs.python.org/3/howto/descriptor.html#complete-practical-example)。 611 | 612 | ## 总结 613 | 614 | 通过本文,你应该已经感受到描述符的强大功能,并且大致明白应该在哪些场合运用它了: 615 | 616 | - 描述符就是可复用的属性,它将函数调用伪装成对属性的访问。 617 | - 数据描述符和非数据描述符,在查找链中位于不同的优先级。 618 | - 描述符在属性托管、缓存和验证器等场景下应用较为常见。 619 | 620 | 没骗你吧,描述符绝对是个很有意思的特性,也不是炫技用的花拳绣腿。合理运用,可以让你的代码简洁而优雅。 621 | 622 | ## 参考 623 | 624 | - [implementing-descriptors](https://docs.python.org/3/reference/datamodel.html#implementing-descriptors) 625 | - [descriptor](https://docs.python.org/3/howto/descriptor.html#complete-practical-example) 626 | - [why-use-python-descriptors](https://realpython.com/python-descriptors/#why-use-python-descriptors) 627 | 628 | --- 629 | 630 | 本系列文章开源发布于 Github,传送门:[Python魔法方法漫游指南](https://github.com/stacklens/python-magic-method-cookbook) 631 | 632 | 看完文章想吐槽?欢迎留言告诉我! 633 | 634 | --------------------------------------------------------------------------------