前言
在python cookbook
中学到的很多python
技巧,基本都是工作中做python
开发遇到的实际问题,实用性很强,故记录于此。
数据结构
序列分解
1 | 4,5) p = ( |
解压可迭代对象赋值给多个变量
1 | 'elssm','test@qq.com','13888888888','15888888888') record = ( |
保留有限历史记录
1 | from collections import deque |
deque
增删操作
1 | q = deque() |
查找最大或最小的N个元素
1 | import heapq |
处理更复杂的的数据结构
1 | portfolio = [ |
字典运算
1 | prices = { |
查找两字典的相同点
1 | a = { |
删除序列相同元素并保持顺序
1 | def dedupe(items): |
1 | def dedupe(items, key=None): |
序列中出现次数最多的元素
1 | words = [ |
通过某个关键字排序字典列表
1 | rows = [ |
通过某个字段将记录分组
1 | rows = [ |
过滤列表元素
1 | values = ['1', '2', '-3', '-', '4', 'N/A', '5'] |
从字典中提取子集
1 | prices = { |
合并多个字典或映射
假设有如下两个字典
1 | a = {'x': 1, 'z': 3 } |
假设必须在两个字典中执行查找操作(比如先从a中找,如果找不到再在b中找)
1 | from collections import ChainMap |
字符串和文本
使用多个界定符分割字符串
1 | 'asdf fjdk; afed, fjek,asdf, foo' line = |
函数re.split()
是非常实用的,因为它允许你为分隔符指定多个正则模式。 比如,在上面的例子中,分隔符可以是逗号,分号或者是空格,并且后面紧跟着任意个的空格。 只要这个模式被找到,那么匹配的分隔符两边的实体都会被当成是结果中的元素返回。 返回结果为一个字段列表,这个跟str.split()
返回值类型是一样的。
用shell通配符匹配字符串
1 | from fnmatch import fnmatch,fnmatchcase |
字符串匹配和搜索
1 | import re |
字符串搜索和替换
对于简单的搜索,直接使用str.replace()
方法。
1 | 'this is a test.' text = |
对于复杂的模式,可以使用re
模块中的sub()
函数。示例如下:
1 | 'today is 2023/10/7.tomorrow is 2023/10/8.' text = |
最短匹配模式
1 | r'"(.*)"') str_pat = re.compile( |
在这个例子中,模式r'\"(.*)\"'
的意图是匹配被双引号包含的文本。 但是在正则表达式中操作符是贪婪的,因此匹配操作会查找最长的可能匹配。 于是在第二个例子中搜索text2
的时候返回结果并不是正确的。
为了修正这个问题,可以在模式中的操作符后面加上?修饰符
1 | r'"(.*?)"') str_pat = re.compile( |
字符串对齐
对于基本的字符串对齐操作,可以使用字符串的ljust()
,rjust()
和center()
方法。示例如下:
1 | 'Hello World' text = |
所有这些方法都能接受一个可选的填充字符。示例如下:
1 | 20,'=') text.rjust( |
函数format()
同样可以用来很容易的对齐字符串。 你要做的就是使用<,>
或者^
字符后面紧跟一个指定的宽度。比如:
1 | '>20') format(text, |
如果你想指定一个非空格的填充字符,将它写到对齐字符的前面即可:
1 | '=>20s') format(text, |
日期和时间
数字的四舍五入
对于简单的舍入运算,使用内置的round(value, ndigits)
函数即可。示例如下:
1 | 1.23, 1) round( |
注意:当一个值刚好在两个边界的中间的时候,round
函数返回离它最近的偶数。 也就是说,对1.5或者2.5的舍入运算都会得到2。
传给 round() 函数的 ndigits 参数可以是负数,这种情况下, 舍入运算会作用在十位、百位、千位等上面。示例如下:
1 | 1627731 a = |
不要将舍入和格式化输出搞混淆了。 如果你的目的只是简单的输出一定宽度的数,你不需要使用round()
函数。 而仅仅只需要在格式化的时候指定精度即可。示例如下:
1 | 1.23456 x = |
执行精确的浮点数运算
浮点数的一个普遍问题是它们并不能精确的表示十进制数。 并且,即使是最简单的数学运算也会产生小的误差,比如:
1 | 4.2 a = |
这些错误是由底层CPU和IEEE 754标准通过自己的浮点单位去执行算术时的特征。 由于Python的浮点数据类型使用底层表示存储数据,因此你没办法去避免这样的误差。如果想更加精确可以使用decimal
模块:
1 | from decimal import Decimal |
输出进制整数
为了将整数转换为二进制、八进制或十六进制的文本串, 可以分别使用bin()
,oct()
或hex()
函数。
1 | 1234 x = |
另外,如果你不想输出0b,0o或者0x的前缀的话,可以使用format()
函数。示例如下:
1 | 'b') format(x, |
无穷大和NaN
Python并没有特殊的语法来表示这些特殊的浮点值,但是可以使用float()
来创建它们。示例如下:
1 | 'inf') a = float( |
为了测试这些值的存在,使用math.isinf()
和math.isnan()
函数。示例如下:
1 | math.isinf(a) |
基本的日期与时间转换
为了执行不同时间单位的转换和计算,可以使用datetime
模块,为了表示一个时间段,可以创建一个timedelta
实例,示例如下:
1 | from datetime import timedelta |
如果想表示指定的日期和时间,先创建一个datetime
实例然后使用标准的数学运算来操作,实例如下:
1 | from datetime import datetime |
计算当前月份的日期范围
1 | from datetime import datetime, date, timedelta |
上述代码首先计算出当月的第一天的日期,通过date
对象的replace()
方法将days
属性设置为1即可。然后,使用calendar.monthrange()
函数来找出该月的总天数。monthrange()
函数会返回包含星期和该月天数的元组。一旦该月的天数已知了,那么结束日期就可以通过在开始日期上面加上这个天数获得。 有个需要注意的是结束日期并不包含在这个日期范围内。 这个和Python
的slice
与range
操作行为保持一致,同样也不包含结尾。为了在日期范围上循环,要使用到标准的数学和比较操作。 比如,可以利用timedelta
实例来递增日期,小于号<
用来检查一个日期是否在结束日期之前。
字符串转换为日期
1 | from datetime import datetime |
datetime.strptime()
方法支持很多的格式化代码, 比如%Y
代表4位数年份,%m
代表两位数月份。 还有一点值得注意的是这些格式化占位符也可以反过来使用,将日期输出为指定的格式字符串形式。
1 | datetime.datetime(2023, 10, 9, 9, 57, 13, 378621) |
有一点需要注意的是,strptime()
的性能较差,因为它是使用纯python
实现,并且必须处理所有的系统本地设置,因此如果在代码中需要解析大量的日期并且已知日期字符串的确切格式,可以自定义实现日期解析函数,示例如下:
1 | from datetime import datetime |
迭代器和生成器
手动遍历迭代器
为了手动的遍历可迭代对象,使用next()
函数并在代码中捕获StopIteration
异常。
1 | def manual_iter(): |
1 | a = [1, 2, 3, 4, 5] |
使用生成器创建新的迭代模式
如果想实现一种新的迭代模式,使用一个生成器函数来定义它,如下是一个生产某个范围内浮点数的生成器:
1 | def frange(start, stop, increment): |
一个生成器函数主要特征是它只会回应在迭代中使用到的next
操作。 一旦生成器函数返回退出,迭代终止。我们在迭代中通常使用的for
语句会自动处理这些细节
反向迭代
使用内置的reversed()
函数,如下所示:
1 | 1,2,3,4] a = [ |
迭代器切片
如果想得到一个由迭代器生成的切片对象,但是标准切片操作并不能做到,可以使用itertools.islice()
做切片操作,如下所示:
1 | import itertools |
迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道(并且也没有实现索引)。 函数islice()
返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。 然后才开始一个个的返回元素,并直到切片结束索引位置。这里要着重强调的一点是islice()
会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
排列组合的迭代
如果想迭代遍历一个集合中元素的所有可能的排列组合,itertools
提供了三个函数来解决这类问题,其中一个是itertools.permutations()
,它接受一个集合并产生一个元组序列,每个元组由集合中所有元素的一个可能排列组成。 也就是说通过打乱集合中元素排列顺序生成一个元组,示例如下:
1 | from itertools import permutations |
如果想得到指定长度的所有排列,可以传递一个可选的长度参数,示例如下:
1 | from itertools import permutations |
使用itertools.combinations()
可得到输入集合中元素的所有组合,示例如下:
1 | from itertools import combinations |
对于combinations()
来讲,元素顺序已经不重要了,也就是说,('a','b')
和('b','a')
是一样的。
在计算组合的时候,一旦元素被选取就会从后选中剔除掉,而函数itertools.combinations_with_replacement()
允许同一个元素被选择多次,示例如下:
1 | from itertools import combinations_with_replacement |
序列上索引值迭代
一般内置的enumerate()
函数可以很好的处理该问题,示例如下:
1 | 'a','b','c'] a = [ |
为了按照传统行号输出,可以传递一个开始参数,示例如下:
1 | 'a','b','c'] a = [ |
这种情况在你遍历文件时想在错误消息中使用行号定位时非常有用,示例如下:
1 | def parse_data(filename): |
还有一点可能并不很重要,但是也值得注意, 有时候当你在一个已经解压后的元组序列上使用enumerate()
函数时很容易调入陷阱。 你得像下面正确的方式这样写:
1 | data = [ (1, 2), (3, 4), (5, 6), (7, 8) ] |
同时迭代多个序列
为了同时迭代多个序列,可以使用zip()
函数,示例如下:
1 | 1,3,5,7,9] a = [ |
zip(a, b)
会生成一个可返回元组(x, y)
的迭代器,其中x
来自a
,y
来自b
。 一旦其中某个序列到底结尾,迭代宣告结束。 因此迭代长度跟参数中最短序列长度一致。
1 | 1,3,5] a = [ |
如果不希望和最短序列保持一致,可以使用itertools.zip_longest()
函数来代替,示例如下:
1 | 1,3,5] a = [ |
使用zip()
函数可以将数据打包并生成一个字典,示例如下:
1 | 'name','age','sex'] key=[ |
最后强调一点就是,zip()
会创建一个迭代器来作为结果返回。 如果你需要将结对的值存储在列表中,要使用list()
函数。示例如下:
1 | 1,3,5,7,9] a = [ |
不同集合上元素的迭代
itertools.chain()
方法接受一个可迭代对象列表作为输入,并返回一个迭代器,示例如下:
1 | from itertools import chain |
使用chain
的一个常见场景是当你相对不同的集合中所有元素执行某些操作时,示例如下:
1 | active_items = set() |
这种解决方案要比使用两个单独的循环处理更加优雅。
展开嵌套的序列
如果想将一个多层嵌套的序列展开成一个单层列表,可以写一个包含yield from
语句的递归生成器来处理,示例如下:
1 | from collections.abc import Iterable |
在上述代码中,isinstance(x,Iterable)
检查某个元素是否是可迭代的,如果是,yield from
就会返回所有子例程的值,最终返回结果就是一个没有嵌套的简单序列,额外的参数ignore_types
和检测语句isinstance(x,ignore_types)
用来将字符串和字节排除在可迭代对象外,防止将它们再展开成单个字符,示例如下:
1 | from collections.abc import Iterable |
语句yield from
在你想在生成器中调用其他生成器作为子例程时非常有用,如果你不想在代码中使用,那么需要写额外的for
循环,示例如下:
1 | def flatten(items, ignore_types=(str, bytes)): |
顺序迭代合并后的排序迭代对象
如果有一系列的排序序列,想将它们合并后得到一个排序序列并在上面迭代遍历。可以使用heapq.merge()
函数,示例如下:
1 | import heapq |
数据编码与处理
将字典转换为XML
xml.etree.ElementTree
库通常用来做解析工作,其实也可以创建XML
文档。如下函数所示:
1 | from xml.etree.ElementTree import Element,tostring |
函数使用示例如下:
1 | 'name': 'GOOG', 'shares': 100, 'price':490.1 } s = { |
转换结果是一个Element
实例。对于I/O
操作,使用xml.etree.ElementTree
中的tostring()
函数很容易就能将它转换成一个字节字符串。示例如下:
1 | from xml.etree.ElementTree import tostring |
如果想给某个元素添加属性值,可以使用set()
方法:
1 | '_id','1234') e.set( |
解析和修改XML
使用xml.etree.ElementTree
模块可以完成该操作,假设有如下名为pred.xml
的文档内容如下:
1 |
|
下面是一个利用ElementTree
来读取这个文档并对它做一些修改的例子:
1 | from xml.etree.ElementTree import parse,Element |
新生成的newpred.xml
文档内容如下:
1 |
|
修改一个XML
文档结构是很容易的,但是你必须牢记的是所有的修改都是针对父节点元素, 将它作为一个列表来处理。例如,如果你删除某个元素,通过调用父节点的remove()
方法从它的直接父节点中删除。 如果你插入或增加新的元素,你同样使用父节点元素的insert()
和append()
方法。 还能对元素使用索引和切片操作,比如element[i]
或element[i:j]
类与对象
改变对象的字符串显示
要改变一个实例的字符串表示,可以重新定义它的__str__()
和__repr__()
方法,例如:
1 | class Pair: |
__repr__()
方法返回一个实例的代码表示形式,通常用来重新构造这个实例。 内置的repr()
函数返回这个字符串,跟我们使用交互式解释器显示的值是一样的。__str__()
方法将实例转换为一个字符串,使用str()
或print()
函数会输出这个字符串。
1 | 3, 4) p = Pair( |
上面的format()
方法的使用看上去很有趣,格式化代码{0.x}
对应的是第1
个参数的x
属性。 因此,在下面的函数中,0
实际上指的就是self
本身:
1 | def __repr__(self): |
作为这种实现的替代,也可以使用%
操作符,示例如下:
1 | def __repr__(self): |
自定义字符串的格式化
为了自定义字符串的格式化,我们需要在类上面定义__format__()
方法,实例如下:
1 | _formats = { |
现在Date
类的示例可以支持格式化操作,如下所示:
1 | 2023, 10, 11) d = Date( |
__format__()
方法给Python
的字符串格式化功能提供了一个钩子。 这里需要着重强调的是格式化代码的解析工作完全由类自己决定。因此,格式化代码可以是任何值。 例如,参考下面来自datetime
模块中的代码:
1 | from datetime import date |
让对象支持上下文管理协议
为了让一个对象兼容with
语句,你需要实现__enter__()
和__exit__()
方法。例如,考虑如下的一个类,它能为我们创建一个网络连接:
1 | from socket import socket, AF_INET, SOCK_STREAM |
这个类的关键特点在于它表示了一个网络连接,但是初始化的时候并不会做任何事情。 连接的建立和关闭是使用with
语句自动完成的,例如:
1 | from functools import partial |
编写上下文管理器的主要原理是你的代码会放到with
语句块中执行。 当出现with
语句的时候,对象的__enter__()
方法被触发, 它返回的值会被赋值给as
声明的变量。然后,with
语句块里面的代码开始执行。 最后,__exit__()
方法被触发进行清理工作。
在类中封装属性名
Python
程序员不去依赖语言特性去封装数据,而是通过遵循一定的属性和方法命名规约来达到这个效果。 第一个约定是任何以单下划线_
开头的名字都应该是内部实现。示例如下:
1 | class A: |
还可能会遇到在类定义中使用两个下划线(__)
开头的命名。比如:
1 | class B: |
使用双下划线开始会导致访问名称变成其他形式。 比如,在前面的类B
中,私有属性会被分别重命名为_B__private
和_B__private_method
。 这时候你可能会问这样重命名的目的是什么,答案就是继承——这种属性通过继承是无法被覆盖的。比如:
1 | class C(B): |
这里,私有名称__private
和__private_method
被重命名为_C__private
和_C__private_method
,这个跟父类B
中的名称是完全不同的。
大多数而言,你应该让你的非公共名称以单下划线开头。但是,如果你清楚你的代码会涉及到子类, 并且有些内部属性应该在子类中隐藏起来,那么才考虑使用双下划线方案。
创建可管理的属性
自定义某个属性的一种简单方法是将它定义为一个property
。 例如,下面的代码定义了一个property
,增加对一个属性简单的类型检查:
1 | class Person: |
上述代码中有三个相关联的方法,这三个方法的名字都必须一样。 第一个方法是一个getter
函数,它使得first_name
成为一个属性。 其他两个方法给first_name
属性添加了setter
和deleter
函数。 需要强调的是只有在first_name
属性被创建后, 后面的两个装饰器@first_name.setter
和@first_name.deleter
才能被定义。property
的一个关键特征是它看上去跟普通的attribute
没什么两样, 但是访问它的时候会自动触发getter
、setter
和deleter
方法。示例如下:
1 | 'Guido') a = Person( |
调用父类方法
为了调用父类(超类)的一个方法,可以使用super()
函数,比如:
1 | class A: |
super()
函数的一个常见用法是在__init__()
方法中确保父类被正确的初始化了:
1 | class A: |
super()
的另外一个常见用法出现在覆盖Python
特殊方法的代码中,比如:
1 | class Proxy: |
在上面代码中,__setattr__()
的实现包含一个名字检查。如果某个属性名以下划线(_)
开头,就通过super()
调用原始的__setattr__()
, 否则的话就委派给内部的代理对象self._obj
去处理。因为就算没有显式的指明某个类的父类,super()
仍然可以有效的工作。
子类中扩展property
如下代码定义了一个property
:
1 | class Person: |
下面是一个示例类,它继承自Person
并扩展了name
属性的功能:
1 | class SubPerson(Person): |
如果仅仅只想扩展property
的某一个方法,那么可以像下面这样写:
1 | class SubPerson(Person): |
创建新的类或实例属性
如果想创建一个全新的实例属性,可以通过一个描述器类的形式来定义它的功能,示例如下:
1 | class Integer: |
一个描述器就是一个实现了三个核心的属性访问操作(get,set,delete
)的类,分别为(__get__(),__set__(),__delete__()
)这三个特殊的方法,这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。为了使用一个描述器,需将这个描述器的实例作为类属性放到一个类的定义中,例如:
1 | class Point: |
这样做所有对描述器属性(如x或y)的访问会被(__get__(),__set__(),__delete__()
)方法捕获到,示例如下:
1 | 2, 3) p = Point( |
作为输入,描述器的每一个方法会接受一个操作实例。为了实现请求操作,会相应的操作实例底层的字典(__dict__
属性)。 描述器的self.name
属性存储了在实例字典中被实际使用到的key
。
需要注意的是:描述器只能在类级别被定义,而不能为每个实例单独定义。因此,下面的代码是无法工作的:
1 | class Point: |
使用延迟计算属性
如果想将一个只读属性定义成一个property
,并且只在访问的时候才会计算结果。 但是一旦被访问后,你希望结果值被缓存起来,不用每次都去计算。可以使用一个描述器类,如下所示:
1 | class lazyproperty: |
使用上述描述器类示例如下:
1 | import math |
交互环境演示如下:
1 | 4.0) c = Circle( |
可以发现Computing area
和Computing perimeter
只出现了一次。
这种方案有一个小缺陷就是计算出的值被创建后是可以被修改的。例如:
1 | c.area |
如果想要修改这个问题可以使用如下方式:
1 | def lazyproperty(func): |
这种方式不允许修改:
1 | 4.0) c = Circle( |
简化数据结构的初始化
如果不想写太多__init__()
函数,可以在一个基类中写一个公用的__init__()
函数:
1 | import math |
然后让写的类继承上述基类:
1 | class Stock(Structure1): |
使用示例如下:
1 | 'ACME', 50, 91.1) s = Stock( |
除此之外,还可以将不在_fields
中的名称加入到属性中去,示例如下:
1 | class Structure3: |
定义接口或者抽象基类
使用abc
模块可以很轻松的定义抽象基类,示例如下:
1 | from abc import ABCMeta, abstractmethod |
抽象类的一个特点是它不能直接被实例化,如下方式是错误的:
1 | a = IStream() |
抽象类的目的就是让别的类继承它并实现特定的抽象方法:
1 | class SocketStream(IStream): |
抽象基类的一个主要用途是在代码中检查某些类是否为特定类型,实现了特定接口:
1 | def serialize(obj, stream): |
实现自定义容器
collections
定义了很多抽象基类,当你想自定义容器类的时候它们会非常有用。 比如你想让你的类支持迭代,那就让你的类继承collections.Iterable
即可:
1 | import collections |
不过需要实现collections.Iterable
所有的抽象方法,否则会报错:
1 | a = A() |
示例如下:
1 | from collections.abc import Iterable |
collections
中很多抽象类会为一些常见容器操作提供默认的实现,这样一来你只需要实现那些你最感兴趣的方法即可。假设你的类继承自collections.MutableSequence
,如下:
1 | class Items(collections.MutableSequence): |
如果你创建Items
的实例,你会发现它支持几乎所有的核心列表方法(如append()、remove()、count()
等)。 使用示例如下:
1 | 1, 2, 3]) a = Items([ |
属性的代理访问
简单来说,代理是一种编程模式,它将某个操作转移给另外一个对象来实现。 最简单的形式可能是像下面这样:
1 | class A: |
如果有大量的方法需要代理, 那么使用__getattr__()
方法会更好:
1 | class B2: |
示例如下:
1 | b = B2() |
另外一个代理例子是实现代理模式,示例如下:
1 | class Proxy: |
使用这个代理类时,你只需要用它来包装下其他类即可:
1 | class Spam: |
通过自定义属性访问方法,你可以用不同方式自定义代理类行为。
代理类有时候可以作为继承的替代方案。例如,一个简单的继承如下:
1 | class A: |
使用代理的话,就是下面这样:
1 | class A: |
当实现代理模式时,还有些细节需要注意。首先,__getattr__()
实际是一个后备方法,只有在属性不存在时才会调用。因此,如果代理类实例本身有这个属性的话,那么不会触发这个方法的。另外,__setattr__()
和__delattr__()
需要额外的魔法来区分代理实例和被代理实例_obj
的属性。一个通常的约定是只代理那些不以下划线_
开头的属性。
还有一点需要注意的是,__getattr__()
对于大部分以双下划线(__
)开始和结尾的属性并不适用。 例如考虑如下的类:
1 | class ListLike: |
如果是创建一个ListLike
对象,会发现它支持普通的列表方法,如append()
和insert()
, 但是却不支持len()
、元素查找等。例如:
1 | a = ListLike() |
为了让它支持这些方法,必须手动实现这些方法代理:
1 | class ListLike: |
创建不调用init方法的实例
可以通过__new__()
方法创建一个未初始化的实例。例如考虑如下这个类:
1 | class Date: |
下面为不调用__init__()
方法来创建这个Date
实例:
1 | d = Date.__new__(Date) |
结果可以看到,这个Date
实例的属性year
还不存在,所以你需要手动初始化:
1 | 'year':2023, 'month':10, 'day':11} data = { |
实现状态对象或者状态机
在很多程序中,有些对象会根据状态的不同来执行不同的操作。比如考虑如下的一个连接对象:
1 | class Connection: |
这样写有很多缺点,首先是代码太复杂了,好多的条件判断。其次是执行效率变低, 因为一些常见的操作比如read()、write()
每次执行前都需要执行检查。一个更好的办法是为每个状态定义一个对象:
1 | class Connection1: |
使用示例如下:
1 | c = Connection() |
通过字符串调用对象方法
有一个字符串形式的方法名称,如果想通过它调用某个对象的对应方法。最简单的情况,可以使用getattr()
:
1 | import math |
另外一种方法是使用operator.methodcaller()
,示例如下:
1 | import operator |
当你需要通过相同的参数多次调用某个方法时,使用operator.methodcaller
就很方便了。 比如你需要排序一系列的点,就可以这样做:
1 | points = [ |
调用一个方法实际上是两步独立操作,第一步是查找属性,第二步是函数调用。 因此,为了调用某个方法,你可以首先通过getattr()
来查找到这个属性,然后再去以函数方式调用它即可。operator.methodcaller()
创建一个可调用对象,并同时提供所有必要参数, 然后调用的时候只需要将实例对象传递给它即可,示例如下:
1 | 3, 4) p = Point( |
让类支持比较操作
Python
类对每个比较操作都需要实现一个特殊方法来支持。例如为了支持>=
操作符,你需要定义一个__ge__()
方法。装饰器functools.total_ordering
就是用来简化这个处理的。使用它来装饰一个类,你只需定义一个__eq__()
方法, 外加其他方法(__lt__, __le__, __gt__, or __ge__
)中的一个即可。 然后装饰器会自动为你填充其它比较方法。示例如下:
1 | from functools import total_ordering |
这里我们只是给House
类定义了两个方法:__eq__()
和__lt__()
,它就能支持所有的比较操作:
1 | h1 = House('h1', 'Cape') |
total_ordering
装饰器也没那么神秘。它就是定义了一个从每个比较支持方法到所有需要定义的其他方法的一个映射而已。比如你定义了__le__()
方法,那么它就被用来构建所有其他的需要定义的那些特殊方法。 实际上就是在类里面像下面这样定义了一些特殊方法:
1 | class House: |