[EuroPython2021笔记] functools 漫游指南

Posted 有数可据

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[EuroPython2021笔记] functools 漫游指南相关的知识,希望对你有一定的参考价值。

为什么我这篇是笔记,不是翻译,因为这并不是一篇严格意义的翻译文章。我也觉得,如果我一字一句的翻译英文,翻译出来的都是英式汉语。而且,这里也有我自己的一些思考和实践,或者,从写作的角度,叫做在创作。我自己的部分用括号表示。

英文简介

A Hitchhiker’s Guide to functools [EuroPython 2021 - Talk - 2021-07-29 - Brian] [Online]

By Scott Irwin

One of the concepts we learn early in our Python journey is functions. However, Python’s idea of functions goes beyond basic functions; it also supports the idea of higher-order functions - functions that act on or return other functions. Higher-order functions are useful, powerful, and can save a lot of typing. So, of course, Python’s standard library contains a module of higher-order functions waiting to be used (hint: its called functools). In this talk, we will explore functools and look at how its functions can be used to enhance and improve our code.

翻译

Functool 漫游指南

EuroPython 2021

2021年7月29日

作者: Scott Irwin

我们都知道function。但是python远不止这些基础用法。它支持高阶函数,它作用于其他函数,并返回function。高阶函数非常有用,十分强大,可以节省大量的打字时间。Python的标准库,就包含这样的高阶函数,它叫functools。

在这次谈话中,我们将探索 functools,并研究如何利用其功能来增强和改进我们的代码。

小知识美国有一本畅销书,叫做《The Hitch-Hiker’s Guide to the Galaxy》,中文叫做《银河系漫游指南》,所以,我也把这次的演讲翻译成《functools漫游指南》。

Hitch-Hiker是指哪些在路边搭便车的人。

定义

functool包含一些高阶函数。高阶函数作用于,并返回其他函数。decorator就是高阶函数。

历史

下表列举了Python 各版本对应的Functools功能,我们可见,functools一直在发展。

版本功能
Python 3.0reduce
Python 3.2total_ordering, cmp_to_key
Python 3.4partialmethod, singledispatch
Python 3.8cached_property, singledispatchmethod
Python 3.9cache

简化函数签名

这里我们用partial和partialmethod实现。

Partial的签名为

partial(func, *args, **kwargs)

partial的第一个参数是一个函数func,然后接受位置参数和关键字参数。这些参数被锁定到了func。patial返回一个partial对象,它表现的和原来的函数一样,唯一的区别是有些参数已经被提前定义了。

在下面的示例中,我们把幂函数pow函数传给partial,我们定义指数为2。这样,我们就得到一个指数为2的幂函数,在之后使用的时候,就无需指定指数了。我们只需要传一个5,就可以得到5的平方25了。

from functools import partial
pow_2 = partial(pow, exp=2)
print(f'{pow_2(5)=}') # pow_2(2)=25

partial的用处,就是把一个有很多参数的复杂函数变成一个只有少数参数的简单函数。而有些地方,只接受一个参数的简单函数,比如map。

from functools import partial
list(map(partial(pow, exp=3), range(10)))
# 结果
# [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

如果没有partial,会麻烦/啰嗦一些,你需要写成:

def pow_3(x):
    return pow(x, 3)
list(map(pow_3, range(10)))
# 结果
# [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

或者写成

list(map(lambda x: pow(x, 3), range(10)))
# 结果
# [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

partial让我们打字更简单了。我们可以利用代码自动完成功能。

import sys
from functools import partial
print_stderr = partial(print, file=sys.stderr)
print_stderr("This output goes to stderr")

我们可以简化函数,那么我们可以简化类里面的method么?当然可以,答案就是使用partialmethod。

它的签名和partial一样:

partialmethod(func, *args, **kwargs)

它的第一个参数是一个method,同时返回一个method。

# 来自官方文档的例子
from functools import partialmethod
class Cell:
    def __init__(self):
        self._alive = False
    @property
    def alive(self):
        return self._alive
    def set_state(self, state):
        self._alive = bool(state)
    set_alive = partialmethod(set_state, True)
    set_dead = partialmethod(set_state, False)

c = Cell()
c.alive
# False
c.set_alive()
c.alive
# True

在上面的例子中,我们有一个method,叫做set_state,它可以改变Cell的state,即是否是alive的。我们还定义了两个partialmethod,分别是set_alive和set_dead,他们和set_state方法的区别是,state的值被锁定了,前者为True,后者为False。

函数包装器

这里我们介绍wraps和update_wrapper。

wraps是一个函数装饰器。它使得包装前后的两个函数属性一致。即把包装前的函数的属性,给包装后的函数的属性。

举例:

def my_decorator(f): 
    def wrapper(*args, **kwargs): 
        '''wrapper doc string''' 
        print('wrapper called') 
        return f(*args, **kwargs) 
    return wrapper 

@my_decorator 
def func(): 
    '''func doc string''' 
    print('func called')
>>> func() 
wrapper called 
func called 
>>> func.__name__ 
'wrapper' 
>>> func.__doc__ 
'wrapper doc string'

在上面的例子中,我们用一个包装器,对一个函数进行包装。如果我们包装成功,就会打印出"wrapper called"这句话,接着执行被包装的函数本身。

我们可见,在执行函数时,没有问题。但是当我们查看函数的属性的时候,比如函数的名字,却发现给我们的不是被包装的函数的名字,而是包装的名字。

(这就好比,你买了珠,放在椟,尔妻手指椟,问尔,”此何也?“,你回答,”椟也“。尔妻吐血而亡。所以呀,要学会说人话,你就应该回答,这是珠。怎么实现呢?这里,我们就可以用wraps了。)

用法特别简单,只需用wraps去装饰wrapper。

def my_decorator(f): 
    @wraps(f)
    def wrapper(*args, **kwargs): 
        '''wrapper doc string''' 
        print('wrapper called') 
        return f(*args, **kwargs) 
    return wrapper 

@my_decorator 
def func(): 
    '''func doc string''' 
    print('func called')
>>> func() 
wrapper called 
func called 
>>> func.__name__ 
'func' 
>>> func.__doc__ 
'func doc string'

和上面的例子比,函数的名字和doc_string已经更新成了被包装的函数。(即我们在珠宝盒子外面写了两个字:珠宝。)

假如,我们没有函数func的修改权限,那么,我们怎么才能对他进行装饰和包装呢?

我们可以这样做。首先,我们用我们的函数,调用被包装的函数。

(这里是调用,不是包装,不是装饰)

def func(): 
    '''func doc string''' 
    print('func called')

def my_func(): 
    """my_func doc string"""
    print("my func")
    func()

>>>my_func()
my func
func called
>>>my_func.__name__
'my_func'
>>>my_func.__doc__
'my_func doc string'

这样,我们的代码,和被包装的函数都被执行了。

接着,我们调用update_wrapper。其参数如下:

update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

from functools import update_wrapper
update_wrapper(my_func, func)
>>>my_func.__name__
'func'
>>>my_func.__doc__
'func doc string'

这是my_func的name和doc都会更新成func的了。

(Scott Irwin的原文update_wrapper和wraps用了不同的例子,我改成了同一个例子。更容易理解。Effective Python里面也有讲到decorator和wraps,如果我这里你没看到,可以去看书。我认为对于一个知识点的理解,可能要在不同的地方看几遍,才能变得熟悉。)

(再补充一点,是我自己悟出来的。就算我们没有被包装函数的访问权限,我们也不需要update_wrapper,上面的例子只是为了介绍update_wrapper。我们回到事情的本源,我们应该注意到,装饰器本身是一个高阶函数,他接收一个函数,返回一个函数。那么,我们可以直接把装饰器当函数用,而不是装饰。)

(我们再回到最初的例子,这时,我们假设我们没有func的访问权限,无法给他装饰器。)

(func的代码,假如是某个第三方库。)

def func(): 
    '''func doc string''' 
    print('func called')

(我们的装饰器:)

from functools import wraps
def my_decorator(f): 
    @wraps(f)
    def wrapper(*args, **kwargs): 
        '''wrapper doc string''' 
        print('wrapper called') 
        return f(*args, **kwargs) 
    return wrapper 

(我们可以直接调用装饰器高阶函数,得到wrapper:)

my_func = my_decorator(func)

(然后,我们再执行wrapper,或者my_func。)

>>>my_func()
wrapper called
func called
>>>my_func.__name__
'func'
>>>my_func.__doc__
'func doc string'

(千万别被各种大词忽悠,忘了本质。)

缓存

和缓存有关的functool成员有:

  • lru_cache
  • cache
  • cached_property

lru_cache的签名为:

lru_cache(maxsize=128, typed=False)

先说什么是lru,他是Least Recent Used的缩写。就是最近使用的。

它是一个包装器。它试用的场景是,一个函数被反复调用,但是参数就那么几个。

如果Typed为True,那么函数会根据参数的类型分别记录。

假如,我们有一个很慢的幂函数。

from functools import lru_cache
import time

def slow_pow(a: int, b: int) -> int:
    time.sleep(5)
    return a ** b

每次执行,都需要5秒。

我们可以给他加上@lru_cache

@lru_cache
def slow_pow2(a: int, b: int) -> int:
    time.sleep(5)
    return a ** b

这时,第一次执行,还是5秒,但是第二次就是0秒了。

>>>slow_pow2(2, 3) # 5秒
8
>>>slow_pow2(2, 3) # 0秒
8

如果你没有slow_pow的源代码,或者出于某种原因,不能修改它。你可以直接把装饰器当高阶函数。

>>>slow_pow3 = lru_cache(slow_pow)
>>>slow_pow3(2, 3) # 5秒
8
>>>slow_pow3(2, 3) # 0秒
8

lru_cache包装出来的函数,会附件上3个新的method。cache_info(), cache_parameters(), 和 cache_clear()。cache_clear()就是清除缓存,有的时候,你还是需要的。比如,用户更新了数据库。

>>>slow_pow2.cache_info() # 缓存信息
CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
>>>slow_pow2.cache_parameters() # 缓存参数
{'maxsize': 128, 'typed': False}
>>>slow_pow2.cache_clear() # 清除缓存
>>>slow_pow2(2,3) #又变成5秒了。
8

cache和lru_cache差不多,但是cache没有内存限制。

cached_property 和 property差不多,区别是,第二次访问的时候,method的内部逻辑将不再执行,而是直接返回缓存值。

(比如,我们有以下的类。)

class Circle:
    def __init__(r):
        self._r = r
    
    @property
    def radius():
        return self._r
    
	@property
    def area():
        return 0.5 * 3.14 * self._r ** 2

(这里,每次访问area的时候,都要执行其内部逻辑,比较耗时。于是,我们通常会这样写:)

class Circle:
    def __init__(r):
        self._r = r
        self._area = None
    
    @property
    def radius():
        return self._r
    
	@property
    def area():
        if not self._area:
	        return 0.5 * 3.14 * self._r ** 2
        else:
            self._area

(这时,这里的self._area,就是我们手动写的cache。因为每次都这样写,比较啰嗦,于是,我们有了cached_property。)

class Circle:
    def __init__(r):
        self._r = r
    
    @property
    def radius():
        return self._r
    
	@cached_property
    def area():
        return 0.5 * 3.14 * self._r ** 2

(怎么样,看起来舒服多了吧。)

total_ordering

total_ordering是个类装饰器。我们知道类里面可以定义__lt__(), __le__(), __gt__(), __ge__(), 四个用于比较的函数。如果没有total_ordering,你需要分别实现这四个。有了total_ordering,你只需要实现一个,另外几个就由total_ordering负责了。

from functools import total_ordering

@total_ordering
class Car():

    def __init__(self, year, make, model):
		self.year, self.make, self.model = year, make, model
	
    def __eq__(self, o):
		if not isinstance(o, Car):
			return NotImplemented
		return ((self.year, self.make, self.model) == (o.year, o.make, o.mo
	
    def __lt__(self, o):
		if not isinstance(o, Car):
			return NotImplemented
		return ((self.year, self.make, self.model) < (o.year, o.make, o.mod
>>> from car import Car
>>> car_1 = Car(2020, 'BMW', '530i')
>>> car_2 = Car(2020, 'BMW', '330i')
>>> (car_1 < car_2), (car_1 > car_2)
(False, True)

Reduce

Reduce的签名是:

reduce(func, iterable[, initializer])

这里func是一个有两个参数的函数,iterable是一个list或者其他可以被遍历的对象。

比如,我们可以求一个list的product。

import operator
from functools import reduce

def product(iterable):
    return reduce(operator.mul, iterable)

使用:

>>>product([2, 3, 4])
24

函数重载

我们用singledispatch来实现函数重载。首先,我们声明一个函数入口点。用@singledispatch装饰。然后,我们写几个重载函数,用@fun.register装饰。这时,

from functools import singledispatch

@singledispatch
def fun(arg):
	print(f"Let me just say, {arg}")

@fun.register
def _(arg: int):
	print(f"Strength in numbers, eh? {arg}")

@fun.register
def _(arg: list):
	print("Enumerate this:")
	for i, elem in enumerate(arg):
		print(i, elem)

@fun.register(complex)
def _(arg):
	print(f"Better than complicated. {arg.real} {arg.imag}")

使用:

>>> from fun import fun
>>> fun(9)
Strength in numbers, eh? 9
>>> fun([9,7])
Enumerate this:
0 9
1 7
>>> fun(3.4)
Let me just say, 3.4
>>> fun(3.4 + 6j)
Better than complicated. 3.4 6.0

总结

functools包含很多高阶函数,他们功能强大。利用好它,可以让你的代码更加可读,可维护。

参考

演讲主页:

https://ep2021.europython.eu/talks/a-hitchhikers-guide-to-functools/

视频地址(优酷):

https://v.youku.com/v_show/id_XNTgxMjI1NTc2OA==.html

以上是关于[EuroPython2021笔记] functools 漫游指南的主要内容,如果未能解决你的问题,请参考以下文章

[EuroPython 2021笔记] Python 3.10新功能开发者亲述:模式匹配案例实战

[EuroPython2021笔记] functools 漫游指南

[EuroPython2021笔记] functools 漫游指南

[EuroPython 2021笔记] Python 3.10新功能开发者亲述:模式匹配案例实战

[EuroPython 2021笔记] Python 3.10新功能开发者亲述:模式匹配案例实战

[EuroPython2021笔记] Yoichi Takai: 在python 3.10中使用静态类