python标准库系列教程(二)——functools (下篇)
python进阶教程
机器学习
深度学习
长按二维码关注
进入正文
01
声明
functools, itertools, operator是Python标准库为我们提供的支持函数式编程的三大模块,合理的使用这三个模块,我们可以写出更加简洁可读的Pythonic代码,本次的系列文章将介绍并使用这些python自带的标准模块,系列文章分篇连载,此为第二篇,鉴于内容较多,为了方便阅读,第二篇共分为上、中、下三篇,此为下篇。有兴趣的小伙伴后面记得关注学习哦!
Python标准库之functools
目录
1 partial函数2 partialmethod函数
3 reduce函数
4 wraps函数
5 update_wrapper函数
6 singledispatch函数
7 total_ordering函数
8 cmp_to_key函数
8 namedtuple函数
鉴于篇幅较长,functools模块将分为三篇进行说明,
上篇:partial、partialmethod
中篇:reduce、wraps、update_wrapper
下篇:singledispatch、total_ordering、cmp_to_key
namedtuple
续接前面两篇:
06
singledispatch
我们知道,很多语言都有函数的重载(重写),比如C#,C++,java。即同一个函数名称,因为参数的类型不同而实现不同的功能,这对于静态语言来说是非常重要的,但是python本身是动态语言,在定义函数的时候,本身就不需要指定参数的类型,因此不具备函数重载这种说法。
比如在C#中,如果我要实现函数的重载,我可以像下面这样做:
using System;
class Animal
{
public void test(int a)
{
Console.WriteLine($"我是整数{a}");
}
public void test(double a)
{
Console.WriteLine($"我是小数{a}");
}
public void test(string a)
{
Console.WriteLine($"我是字符串{a}");
}
}
因为python在定义函数的时候并不需要指定参数的类型,所以不存在函数重载,当然我依然可以使用多个if条件在同一个方法里面进行这种实现,如下:
import functools
def test(a):
if isinstance(a,int):
print(f'我是整数{a}')
elif isinstance(a,str):
print(f'我是字符串{a}')
elif isinstance(a,list):
print(f'我是列表{a}')
通过不同的条件来对参数类型进行筛选,然后选择不同的操作,这当然没有问题,但是这么做,显得不那么“高级”。
虽然Python不支持同名方法允许有不同的参数类型,但是我们可以借用singledispatch来动态指定相应的方法所接收的参数类型
,而不用把参数判断放到方法内部去判断从而降低代码的可读性。我们先从一个简单的例子说起:
from functools import singledispatch
def test(obj):
print(f'我是{obj},我的类型为{type(obj)}')
def test_(obj):
print(f'我是{obj},我的类型为{type(obj)}')
def test_(obj):
print(f'我是{obj},我的类型为{type(obj)}')
test(1)
test("iloveu")
test([1,2,3])
'''运行结果为:
我是1,我的类型为<class 'int'>
我是iloveu,我的类型为<class 'str'>
我是[1, 2, 3],我的类型为<class 'list'>
'''
这里需要注意的是:后面在进行重载的时候就不要再用原来的函数名称test了,我可以对几种不同的重载用一个和test不同的名字test_。我甚至还可以给每一种重载都起一个不同的名字,比如test1、test2、test3等等,但是参数是一样的没有关系。如果是所有的重载都用test这一个名字,会出现以下错误:
AttributeError: 'function' object has no attribute 'register'
为什么?
这要从装饰器的本质开始说起了,因为singlepatch本质上是一个装饰器(嵌套函数),里面有一个wrapper函数,查看定义发现,里面还定义了一个register函数,后面的具体解释有兴趣的小伙伴可以查看我的系列文章——python高级编程之装饰器详解。
如下:
Python高级编程——装饰器Decorator超详细讲解(中篇)
Python高级编程——装饰器Decorator超详细讲解(补充篇——嵌套装饰器)
singlepatch的定义里面有这样一句话:
Transforms a function into a generic function, which can have different behaviours depending upon the type of its first argument。
即singlepatch可以根据函数的第一个参数的不同给函数定义不同的行为(即重载),将一个函数转化成一个“泛函数”。这就是singlepatch的作用。下面将在类中的函数加以说明,
from functools import singledispatch
class Test:
@singledispatch #给某一个需要重载的方法包装起来,它是一个装饰器
def test(obj):
print(f'我是{obj},我的类型为{type(obj)}')
@test.register(str) #该方法的第一种重载,这里与@property属性的设置是很类似的
def test_(obj):
print(f'我是{obj},我的类型为{type(obj)}')
@test.register(int) #该方法的第二种重载
def test_(obj):
print(f'我是{obj},我的类型为{type(obj)}')
Test.test(1)
Test.test('iloveu')
Test.test([1,2,3])
'''运行结果为:
我是1,我的类型为<class 'int'>
我是iloveu,我的类型为<class 'str'>
我是[1, 2, 3],我的类型为<class 'list'>
'''
几个注意事项:
(1)因为在类中定义的实例方法,第一个参数一定是对象本身self,定义的类方法一定是类本身cls,所以都没有办法通过第一个参数类型的不同进行重载,所以只能针对类中的“静态方法”进行重载;
(2)虽然是对静态方法进行重载,但是并不能使用@staticmethod修饰test方法,原因参见前面的文章之,装饰器的叠加嵌套文章。
上面的例子都是只是用一个参数,但是并不是说只有一个参数的函数才能够重载,只是说,是通过函数的第一个参数的类型去判断究竟执行哪一个重载版本,下面再举个例子:
from functools import singledispatch
class Test:
@singledispatch #给某一个需要重载的方法包装起来,它是一个装饰器
def test(obj,a,b,c='真的是这样吗?'):
print(f'我是{obj},我的类型为{type(obj)}')
print(a+b)
print(c)
@test.register(str) #该方法的第一种重载,这里与@property属性的设置是很类似的
def test_(obj,a,b,c='真的是这样吗?'):
print(f'我是{obj},我的类型为{type(obj)}')
print(a+b)
print(c)
@test.register(int) #该方法的第二种重载
def test_(obj,a,b,c='真的是这样吗?'):
print(f'我是{obj},我的类型为{type(obj)}')
print(a+b)
print(c)
Test.test(1,100,200)
print('-----------------------------------------')
Test.test('iloveu','i want',' sleep','不存在的!')
print('-----------------------------------------')
Test.test([1,2,3],[11,22],[33,44],'有意思!')
'''运行结果为:
我是1,我的类型为<class 'int'>
300
真的是这样吗?
-----------------------------------------
我是iloveu,我的类型为<class 'str'>
i want sleep
不存在的!
-----------------------------------------
我是[1, 2, 3],我的类型为<class 'list'>
[11, 22, 33, 44]
有意思!
'''
singlepatch总结:
(1)主要作用:实现函数根据函数的第一个参数类型来进行函数的重载——generic function
(2)实现步骤:
第一步:使用@singlepatch来装饰我所需要的函数,(以test为例)
第二步:使用@test.register来装饰test函数的重载版本test_。注意的是,这里要保证重载函数的函数名称与原函数名称不同。
07
total_orderings
上面讲了函数的重载,在面向对象的程序设计中,还有“运算符的重载”,python也不例外,total_ordering函数,顾名思义,是一个“处理所有的的大小排序”的函数。
在Python中,涉及到的比较大小的运算符共有6个,分别是:
(1)__lt__(self,other) self < other 小于
(2)__le__(self,other) self <= other 小于等于
(3)__gt__(self,other) self > other 大于
(4)__ge__(self,other) self >= other 大于等于
(5)__eq__(self,other) self == other 等于
(6)__ne__(self,other) self != other 不等于
比如,我要实现一个自定义的Person类,让Person的对象可以比较大小,我们可以这么做:
class Person:
def __init__(self,name,age):
self.name=name
self.age=age
def __lt__(self,other):
if self.age<other.age:
return True
def __le__(self,other):
if self.age<=other.age:
return True
def __gt__(self,other):
if self.age>other.age:
return True
def __ge__(self,other):
if self.age>=other.age:
return True
def __eq__(self,other):
if self.age==other.age:
return True
def __ne__(self,other):
if self.age!=other.age:
return True
p1=Person('张三',23)
p2=Person('李四',20)
p3=Person('王五',25)
print(p1!=p2)
print(p1<p3)
print(p3>p2)
'''运行结果为:
True
True
True
'''
这样做当然也没什么缺点,但是问题就是比较麻烦,我需要为每一个比较运算符都进行重载一遍,那有没有简单一点的解决方法呢?functools.total_ordering提供了解决方案。
我们查看一下total_ordering的定义可得:
def total_ordering(cls):
"""Class decorator that fills in missing ordering methods"""
# Find user-defined comparisons (not those inherited from object).
发现它是一个“类装饰器”,这是他的本质,那它的功能又怎么描述呢?
'''Given a class defining one or more rich comparison ordering methods,
this class decorator supplies the rest. This simplifies the effort
involved in specifying all of the possible rich comparison operations:
The class must define one of __lt__(), __le__(), __gt__(), or __ge__().
In addition, the class should supply an __eq__() method.
'''
大致的含义如下:只要给一个类定义一个或者是几个比较运算符的重载方法,剩下的一些重载运算方法都由这个“类装饰器”来提供了,即不需要全部都由我自己来定义重载;
而且要求要重载的类必须定义__lt__(), __le__(),__gt__(), or __ge__(),这四个中的一个,另外还应该定义一个__eq__就可以了。
也就是说,只需要定义2个,剩下的四个就可以推断出来了,为什么呢?其实也很好理解。
比如我要比较一个数据的大小,只自己定义了<(小于)和==(等于),下面有一些数据,
10,11,12
10==10,这是定义的==,返回True;
10==11,根据==进行推断,返回False;
10!=11,根据==进行推断,返回True;
10<11,根据<的定义,返回True;
10<=11,根据<和==进行推断,返回True;
10>11,根据<的推断,返回False;
10>=10,根据==的推断,返回True;
从上面我们可以发现,只要知道一个大小关系,一个等于关系,其他关系都可以进行推断了,没有必要对所有的比较运算都重载定义一遍,这正是total_ordering要做的事情。
我们查看total_ordering的源代码,发现源代码中有一个关键的实现,即:
_convert = {
'__lt__': [('__gt__', _gt_from_lt),
('__le__', _le_from_lt),
('__ge__', _ge_from_lt)],
'__le__': [('__ge__', _ge_from_le),
('__lt__', _lt_from_le),
('__gt__', _gt_from_le)],
'__gt__': [('__lt__', _lt_from_gt),
('__ge__', _ge_from_gt),
('__le__', _le_from_gt)],
'__ge__': [('__le__', _le_from_ge),
('__gt__', _gt_from_ge),
('__lt__', _lt_from_ge)]
}
通俗的来说,就是一下两个关系
(1)==与!=可以互相推导;
(2)<、<=、>、>=四者之一推三;
简单实现:
import functools
class Person:
def __init__(self,name,age):
self.name=name
self.age=age
def __lt__(self,other):
print('比较 <、<=、>、>= 四种关系')
if self.age<other.age:
return True
else:
return False
def __eq__(self,other):
print('比较 ==、!= 两种关系')
if self.age==other.age:
return True
else:
return False
print(dir(Person))
p1=Person('张三',23)
p2=Person('李四',20)
p3=Person('王五',25)
print(p1!=p2,end='\n-----------------------------------\n')
print(p1<p3,end='\n-----------------------------------\n')
print(p3>p2,end='\n-----------------------------------\n')
print(p3<p2,end='\n-----------------------------------\n')
print(p3==p2,end='\n-----------------------------------\n')
print(p3!=p3,end='\n-----------------------------------\n')
'''运行结果为:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
比较 ==、!= 两种关系
True
-----------------------------------
比较 <、<=、>、>= 四种关系
True
-----------------------------------
比较 <、<=、>、>= 四种关系
比较 ==、!= 两种关系
True
-----------------------------------
比较 <、<=、>、>= 四种关系
False
-----------------------------------
比较 ==、!= 两种关系
False
-----------------------------------
比较 ==、!= 两种关系
False
-----------------------------------
'''
我们发现,虽然在Person类里面我之定义了__lt__、__eq__这两个魔术方法,但是在使用dir(Person)的时候,依然可以看见里面有__le__、__gt__、__ge__、__ne__这个四个,这四个便是由total_ordering提供的。
08
cmp_to_key
首先从python2版本的cmp函数说起,在python2中,有一个很简单的内置函数cmp,这个函数是用来比较两个对象的,它的函数原型为:cmp(x,y),如果是x<y,则返回-1,如果是x==y,则返回0,如果是x>y,则返回1,示例如下:
>>> cmp(1,2)
-1
>>> cmp(2,2)
0
>>> cmp(2,1)
1
注意:python3中,已经取消了这个内置函数了。
但是这个函数最常见的用法,当然不仅仅是简单的比较两个对象的大小了,更多的是在“排序”中进行使用,这里一sorted排序为例。
在python2中,我们查看sorted的定义,得到:
sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list
发现它有三个参数:
iterable:需要排序的序列
cmp:cmp specifies a custom comparison function of two arguments (iterable elements),这段话是来自于官方文档,意思是说cmp参数接受的值为一个带有两个参数的函数,这两个参数实际上就是序列的两个元素。比如:cmp=lambda x,y: cmp(x.lower(), y.lower())
key:specifies a function of one argument that is used to extract a comparison key from each list element,这段话的意思是他接受一个一个参数的函数,这个参数就是序列的一个元素,比如 key=str.lower
.
cmp和key的默认值为None.
python2版本的排序
(1)key参数的使用
student_tuples = [
... ('john', 'A', 15),
... ('jane', 'B', 12),
... ('dave', 'B', 10),
... ]
>>> sorted(student_tuples, key=lambda student: student[2]) # 根据年龄排序
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
从这里可以看出,key接受的是一个lambda函数,参数为一个,即就是每个元素 student,然后根据每一个元素的age大小进行排序。再看一个例子:
class Student:
... def __init__(self, name, grade, age):
... self.name = name
... self.grade = grade
... self.age = age
... def __repr__(self):
... return repr((self.name, self.grade, self.age))
>>> student_objects = [
... Student('john', 'A', 15),
... Student('jane', 'B', 12),
... Student('dave', 'B', 10),
... ]
>>> sorted(student_objects, key=lambda student: student.age) # 根据年龄排序
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
(2)cmp参数的使用
>>> def numeric_compare(x, y): #定义一个含有两个参数的函数
... return x - y
>>> sorted([5, 2, 4, 1, 3], cmp=numeric_compare)
[1, 2, 3, 4, 5]
# 我也可以倒过来排序
>>> def reverse_numeric(x, y):
... return y - x
>>> sorted([5, 2, 4, 1, 3], cmp=reverse_numeric)
[5, 4, 3, 2, 1]
从上面可以看出,cmp接受的函数是两个参数的,当然我可以使用lambda表达式也是一样的,如下:
a=sorted([5, 2, 4, 1, 3], cmp=lambda x,y:x-y)
print(a)
b=sorted([5, 2, 4, 1, 3], cmp=lambda x,y:y-x)
print(b)
'''运行结果为:和上面一样
[1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
'''
但是,我们似乎并没有用到cmp内置函数啊,接下来使用内置函数cmp也是一样的。
a=sorted([5, 2, 4, 1, 3], cmp=cmp)
print(a)
b=sorted([5, 2, 4, 1, 3], cmp=cmp,reverse=True)
print(b)
'''运行结果为:和上面一样
[1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
'''
通过上面的几个例子,我们可以清楚地知道cmp到底什么作用以及它在排序中的应用。
但是在python3中,取消了内置函数cmp,同时排序函数sorted发生了什么变化呢?先看它的定义:
sorted(iterable,key=None,reversed=False)
我们发现,和python2版本相比,少了一个cmp参数,只有一个key参数,这个参数的含义和之前的是一样的,先看下面的一个例子:
>>> student_tuples = [
... ('john', 'A', 15),
... ('jane', 'B', 12),
... ('dave', 'B', 10),
... ]
>>> sorted(student_tuples, key=lambda student: student[2]) # 根据年龄排序
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
还和之前一样,没有区别。
但是python3里面没有了cmp内置函数,如果我想要像上面python2那样是不是就不行了呢?当然不是,就像python3取消了reduce内置函数一样,同样的在functools里面有一个特殊的函数cmp_to_key来完成同样的事,首先看一下他的定义:
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
他实现的的功能是什么?
即将python2中的 cmp=function,现在在python3中可以写为key=function。
我们通过以下例子一步一步来看
import functools
def numeric_compare(x, y):
return x - y
a=sorted([5, 2, 4, 1, 3], key=functools.cmp_to_key(numeric_compare))
print(a)
def reverse_numeric(x,y):
return y-x
b=sorted([5, 2, 4, 1, 3], key=functools.cmp_to_key(numeric_compare))
print(b)
'''运行结果为:
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5] #此处并没有倒过来哦!要想倒过来只有通过reversed参数设置为True实现了
'''
从上面可以看出,此时我不能再直接令key=numeric_compare,也不能使用lambda表达式,因为key只能接受一个参数,但是这里有两个参数。
总结:functools.cmp_to_key(function)到底有什么作用呢?
即将python2的cmp=function,等效于python3的key=cmp_to_key(function).
09
namedtuple
namedtuple:顾名思义,称之为“有名字的元组”即“命名元组”。很多小伙伴很诧异,在标准库collections里面也有一个namedtuple,它们有啥区别吗?实际上他们是完全一样的,所以这里就在functools开面把它讲解了。
namedtuple和普通的tuple都是tuple,但而这不是兄弟关系哦,namedtuple是tuple的子类。但是功能更为强大。对于namedtuple,你不必再通过索引值进行访问,你可以把它看做一个字典通过名字进行访问,只不过其中的值是不能改变的,我们先从简单的元组tuple开始说起:
a=(1,2,3,4,5)
这就是一个简单的元组,但是我们元组的一般使用不要这么去用,因为元组不可变,我们不要用去表示序列问题,我们常常这么用:
a=('张三','男',25,172),用面向对象的观点看,元组的每一个元素看成是每一个对象的属性,这里的a是一个对象,第一个元素为name属性,第二个为性别属性,第三个为年龄属性,第四个为身高属性。
namedtuple正是基于这种思想。先看一个简单的应用:
import functools
Animal=functools.namedtuple('Animal','name age height')
dog=Animal('小狗',5,23)
print(dog)
print(dog.name)
print(dog.age)
print(dog.height)
'''运行结果为:
Animal(name='小狗', age=5, height=23)
小狗
5
23
'''
为了构造一个namedtuple需要两个参数,分别是tuple的名字和其中域的名字(或称之为字段名)。比如在上例中,tuple的名字是“Animal”,它包括三个域,分别是“name”、“age”和“height”。
下面我们看一下namedtuple的定义:
def namedtuple(typename, field_names, *, verbose=False, rename=False, module=None):
有两个参数是必须的
typename:元组的名称
field_names:这个字段名称可以是下面几种形式,
第一种:写成字符串的形式,“field1 field2 field 3”,中间用空格隔开;
第二种:写成列表的形式,['name','age','height']
如下例子:
import functools
Animal=functools.namedtuple('Animal',['name','age','height' ]) #写成列表的形式
dog=Animal('小狗',5,23)
print(dog)
print(dog.name)
print(dog.age)
print(dog.height)
运行结果同上面完全一样。
namedtuple比普通tuple具有更好的可读性,可以使代码更易于维护。同时与字典相比,又更加的轻量和高效。但是有一点需要注意,就是namedtuple中的属性都是不可变的。任何尝试改变其属性值的操作都是非法的。
dog.name='李四'
显示出如下错误:
AttributeError: can't set attribute
即namedtuple依然是不可变对象。
namedtuple是tuple的一个子类,namedtuple还有一个非常好的一点是,它与tuple是完全兼容的。也就是说,我们依然可以用索引去访问一个namedtuple。
import functools
Animal=functools.namedtuple('Animal',['name','age','height' ]) #写成列表的形式
dog=Animal('小狗',5,23)
print(dog)
print(dog[0]) #通过索引来访问元素
print(dog[1])
print(dog[2])
运行结果和上面一样。
既然namedtuple可以以字典的形式访问元祖信息,自然namedtuple可以与字典进行交互了。
(1)将namedtuple转化为OrderedDict(默认字典)
new_dog=dog._asdict() #将namedtuple转化为默认字典
print(new_dog) # 查看dog的内容
print(type(new_dog))
print(new_dog['name'])
print(new_dog['age'])
print(new_dog['height'])
'''运行结果为:
OrderedDict([('name', '小狗'), ('age', 5), ('height', 23)])
<class 'collections.OrderedDict'>
小狗
5
23
'''
(2)通过字典创建namedtuple
通过映射拆分操作符完成
import functools
Animal=functools.namedtuple('Animal',['name','age','height' ]) #写成列表的形式
d={'name':'小狗','age':5,'height':23} #已知字典
dog=Animal(**d)
print(dog)
print(dog.name)
print(dog.age)
print(dog.height)
'''运行结果为:
Animal(name='小狗', age=5, height=23)
小狗
5
23
'''
namedtuple的特点总结:
(1)用类似与属性访问的形式去访问每一个元素,即 a.field的形式;
(2)与tuple兼容,都是不可变对象,也可以用索引访问元素,即a[1]的形式;
(3)可以与字典互相转化;
我们一起过双旦
2018.12.29
Best Wishes to You
最后,再次感谢一路陪伴的小伙伴,希望2018年每个人都能够收获满满,新的2019年都能够赚的盆满锅满!
猜您喜欢往期精选▼1. numpy高级教程(一)——高级数组操作与广播broadcast
2. numpy高级教程(二)——通用函数ufunc与结构化数组structured array
3. python标准库系列教程(一)——itertools
4. python标准库系列教程(二)——functools (上篇)
5.Python高级编程——描述符Descriptor超详细讲解(补充篇之底层原理实现)
6.Python高级编程——描述符Descriptor超详细讲解(中篇之属性控制)
关注我们送礼品哦