Python函数拾遗 一等函数 --流畅的python
这次的python回顾来到函数的知识,首先来探讨一下作为一等兑现的函数。
相关的jupyte notebook文件在这里。
函数作为对象
python中,函数是一等对象(first-class object),一等对象的定义为满足以下条件的程序实体:
- 在运行时创建
- 能够赋值给其他元素
- 能作为参数传递给函数,且能作为函数的返回结果
下面以一个阶乘的例子来说明函数如何作为对象:
1 | from IPython.core.interactiveshell import InteractiveShell |
1 | def factorial(n): |
1 | fact = factorial #将函数赋值给其他元素 |
<function __main__.factorial>
1 | map(factorial, range(11)) #将函数作为参数 |
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
高阶函数
在上面的代码中,我们将factorial
函数作为参数传给了map
,后者将它的第一个参数(factorial
)应用在第二个参数上(range(11)
)。像map
这样能够接受函数作为参数,或者能够将函数作为结果返回的函数称作高阶函数。
例如sorted
函数就是这样一个高阶函数,它支持接受函数作为参数:它的key
参数可以是一个函数,返回待排序元素的序列值,例如我们将文本的长度作为序列值(默认对文本排序是按照字典序),既可以将key
设为长度函数len
。
1 | fruits = ['fig', 'respberry', 'strawberry', 'apple', 'watermelon', 'cherry', 'banana'] |
['apple', 'banana', 'cherry', 'fig', 'respberry', 'strawberry', 'watermelon']
1 | print(sorted(fruits, key=len)) #接受len函数作为key参数,按长短排序 |
['fig', 'apple', 'cherry', 'banana', 'respberry', 'strawberry', 'watermelon']
函数式编程范式中,常用的高阶函数有map, filter, reduce, apply
,在python3中移除了apply
,其他几个高阶函数也都有了现代替代品。
例如,对于map, filter
,可以用列表推导和生成式表达式来替代,可读性更强:
1 | # 比较以下两组代码的可读性 |
[1, 1, 4, 18, 96, 600]
[1, 1, 4, 18, 96, 600]
[1, 6, 120]
[1, 6, 120]
在python3中,reduce
函数从内置韩式被移到了functools
模块中,该函数常被用来求和。除了reduce
,any, all
也是常见的规约函数。
上面的代码中,用到了lambda
表达式,它常用来创建匿名函数。匿名函数是一个一次性的函数,在参数列表中比较适合使用。
可调用对象
python中的可调用对象(即可使用调用运算符()
的对象)有:
- 用户自定义函数 包括
def
创建的具名函数和lambda
表达式。 - 内置函数
- 内置方法
- 类 调用类时运行
__new__
创建实例然后运行__init__
初始化 - 方法 在类的定义体中定义的函数
- 类的实例 如果类定义了
__call__
函数,则它的实例可以当作函数调用 - 生成器函数 使用
yield
关键字的函数或方法
上面可以看到,正如一个类的实例,任何python对象如果定义了__call__
函数,则其变得可调用。
1 | class Bird(): |
上面我们定义另一个鸟类,它有一个sing
函数,并且我们定义了它的__call__
函数,使其返回sing
函数。下面我们将创建一个Bird的实例:
1 | bird = Bird() |
调用sing
Balabalabala...
调用Bird类的实例bird
Balabalabala...
这样我们就方便地创建了一个函数类对象。
函数内省
下面探讨几个将函数作为对象的相关属性。
首先__dict__
属性存储了函数的用户属性,利用它我们可以知道任何对象的属性,由此,我们来关注一下函数专有而用户定义的一般对象没有的属性。
下面列出几个重要的属性:
属性 | 类型 | 说明 |
---|---|---|
__annotations__ |
dict | 参数和返回值的注解 |
__closure__ |
tuple | 函数闭包 |
__code__ |
code | 编译成字节码的函数元数据和函数定义体 |
__defaults__ |
tuple | 形式参数的默认值 |
__name__ |
str | 函数名称 |
为了深入了解它们我们先讨论一下python的参数机制。
python的参数处理非常灵活,调用函数时还可以传入可迭代对象*和**,前者为列表对象,后者为字典对象,举例如下:
1 | def lunch(*food_name, cost_time=10, **lunch_attrs): |
1 | lunch('apple', 'rice', 'meat') |
apple
rice
meat
cost time: 10
1 | food_num = {'apple':1, 'rice':2, 'meat':2} |
apple 1
rice 2
meat 2
cost time: 10
1 | lunch('apple', 'rice', 'meat', **food_num) |
apple
rice
meat
apple 1
rice 2
meat 2
cost time: 10
在传入的参数前加入**,则该参数的每个元素都会被作为单个参数传入,同名键会绑定到对应的具名参数上,其他的会被**attrs捕获。例如:
1 | food_num = {'apple':1, 'rice':2, 'meat':2, 'cost_time':20} |
apple 1
rice 2
meat 2
cost time: 20
那么在函数内省时,如何知道函数需要哪些参数呢?其中哪些参数又有默认值呢?
函数的__defaults__
属性里存了定位参数和关键词参数的默认值,__kwdefaults__
存储了关键词参数,参数名称在__code__
属性中。
1 | # 为了方便说明,重新定义lunch |
('参数默认值', (20, 10))
('参数名称',
<code object lunch at 0x000001F48F958810, file "<ipython-input-57-ee16f5a3102b>", line 2>)
('code.co_varnames', ('start_time', 'cost_time', 'lunch_attrs', 'i'))
('code.co_argcount', 2)
上面可以看到函数的相关信息,其中co_varnames
除了函数的参数外还包含了函数的局部变量,其他信息查看起来又不是很方便。因此我们通常用inspect
模块来提取函数相关信息,这样做更加高效。详细做法如下:
1 | from inspect import signature |
<Signature (start_time=20, cost_time=10, **lunch_attrs)>
'(start_time=20, cost_time=10, **lunch_attrs)'
POSITIONAL_OR_KEYWORD : start_time = 20
POSITIONAL_OR_KEYWORD : cost_time = 10
VAR_KEYWORD : lunch_attrs = <class 'inspect._empty'>
python3提供了为函数声明中的参数和返回值附件元数据的句法。例如下面的函数声明加入了声明,其不同仅在于第一行给各个参数和返回值都加了注解,注解可以是任何类型,常见的是类和字符串。
对于参数,只要在参数后面加:
然后加上注解,返回值则在函数声明末尾和冒号之间加入->
和注解。注解不会对函数造成任何功能影响,只是存储在函数的__annotations__
属性中,我们可以通过inspect
来提取。
1 | def lunch(start_time:int=20, cost_time:'int>0'=10, **lunch_attrs) -> None: |
cost time: 10
cost time: 10
{'cost_time': 'int>0', 'return': None, 'start_time': int}
<class 'int'> : start_time = <class 'inspect._empty'>
'int>0' : cost_time = <class 'inspect._empty'>
<class 'inspect._empty'> : lunch_attrs = <class 'inspect._empty'>
函数式编程
虽然python的目标并不是编程函数式编程语言,但是其中的operator
和functools
等包可以支持函数式编程范式。
下面列举一些常用的函数和例子。
reduce规约,下面展示如何用它来做阶乘:1
2
3from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1,n+1))
operator包
mul乘法,同样是阶乘的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
```
**itemgetter**从对象中取出元素,支持任何实现了`__getitem__`的类。例如`itemgetter(1)`和`lambda fields: fields[1]`都是返回序列索引位1上的元素。下面是排序的例子:
```py
from operator import itemgetter
sorted(data, key=itemgetter(1))
```
**attrgertter**作用和`itemgetter`类似,不过他不是通过索引位而是通过名称提取
**methodcaller**会自动创建一个函数,并在对象上调用参数指定的方法。如下例:
```python
from operator import methodcaller
s = 'Wakaka'
# 以下两种写法等价
#1
upcase = methodcaller('upper')
upcase(s)
#2
s.upper()
'#1'
'WAKAKA'
'#2'
'WAKAKA'
functools包
除了上述的reduce
外,functools
还有一个常用的partial
函数。
该函数可以基于一个函数(我们记为A)创建一个新的可调用对象,并将A函数的某些参数固定住,例如:
1 | def lunch(*food_name, cost_time=10, **lunch_attrs): |
('apple',)
{'cost_time': 20}
apple
{'water': 1}
stackoverflow上有位答主给出了关于partial
必要性的有趣解释。
本章结尾有一则杂谈颇为有趣,摘录如下:
Python 是函数式语言吗 2000 年左右,我在美国做培训,Guido van Rossum 到访了教室(他不是讲师)。在课后的问答环节,有人问他 Python 的哪些特性是从其他语言借鉴而来的。他答 道:“Python 中一切好的特性都是从其他语言中借鉴来的。” 布朗大学的计算机科学教授 Shriram Krishnamurthi 在其论文“Teaching Programming Languages in a Post-Linnaean Age”(http://cs.brown.edu/~sk/Publications/Papers/Published/sk-teach-pl-post-linnaean/) 的开头这样写道:
编程语言“范式”已近末日,它们是旧时代的遗留物,令人厌烦。既然现代语言的 设计者对范式不屑一顾,那么我们的课程为什么要像奴隶一样对其言听计从?
在那篇论文中,下面这一段点名提到了 Python: 对 Python、Ruby 或 Perl 这些语言还要了解什么呢?它们的设计者没有耐心去精 确实现林奈层次结构;设计者按照自己的意愿从别处借鉴特性,创建出完全无视 过往概念的大杂烩。
Krishnamurthi 指出,不要试图把语言归为某一类;相反,把它们视作特性的聚合更有用。