查看原文
其他

python标准库系列教程(二)——functools (下篇)

草yang年华 机器学习与python集中营 2021-09-10

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

续接前面两篇:

python标准库系列教程(二)——functools (上篇)

python标准库系列教程(二)——functools (中篇)

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

@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(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).

发现它是一个“类装饰器”,这是他的本质,那它的功能又怎么描述呢?

@functools.total_ordering

'''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

@functools.total_ordering
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([52413], cmp=numeric_compare) 
[12345]

# 我也可以倒过来排序

>>> def reverse_numeric(x, y):
...     return y - x
>>> sorted([52413], cmp=reverse_numeric) 
[54321]

从上面可以看出,cmp接受的函数是两个参数的,当然我可以使用lambda表达式也是一样的,如下:

a=sorted([52413], cmp=lambda x,y:x-y) 
print(a)

b=sorted([52413], cmp=lambda x,y:y-x) 
print(b)
'''运行结果为:和上面一样
[1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
'''

但是,我们似乎并没有用到cmp内置函数啊,接下来使用内置函数cmp也是一样的。

a=sorted([52413], cmp=cmp) 
print(a)

b=sorted([52413], 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([52413], key=functools.cmp_to_key(numeric_compare)) 
print(a)

def reverse_numeric(x,y):
    return y-x

b=sorted([52413], 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.『双旦福利』年终干货大回馈,更有超多奖品等你拿!!

2.『福利』圣诞礼物已送达,请注意查收!

1. numpy高级教程(一)——高级数组操作与广播broadcast

2. numpy高级教程(二)——通用函数ufunc与结构化数组structured array

3. python标准库系列教程(一)——itertools

4. python标准库系列教程(二)——functools (上篇)

5.Python高级编程——描述符Descriptor超详细讲解(补充篇之底层原理实现)

6.Python高级编程——描述符Descriptor超详细讲解(中篇之属性控制)


 

关注我们送礼品哦


: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存