前言
打算写一系列文章来记录自己学习 Python 3 的点滴;本篇将会重点介绍 python 有关函数的基本内容;
本文为作者的原创作品,转载需注明出处;
参数
归纳起来,python 总共支持五种不同的参数形式;
位置参数(必选参数)
先看一个计算 $x^2$ 的函数,1
2def power(x):
return x * x
对于函数power(x)
,参数x
就是一个位置参数;其特性就是,每次调用的时候,必须传入一个合法的参数,才能调用,1
2
3
4
5
6
7
85) power(
25
15) power(
225
power()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'x'
可见,位置参数(positional argument)是必填的,所以,也称作必选参数
;
默认参数
1 | def power(x, n=2): |
上面就定义了一个默认参数 n,如果当调用该函数 power 的时候,如果不指定参数 n,那么其默认值的就是 2;
1 | 5) power( |
可见,默认参数传递的过程中,可以是2
也可以是n=2
的形式;
注意
一条约定俗成的规则
,默认参数最好使用不可变对象,否则会出现 unexpected 的情况(这里只是最佳实践,当然你也可以不用遵守);举个例子,
1 | def f(a, L=[]): |
输出,1
2
3[1]
[1, 2]
[1, 2, 3]
按照我们正常的编程思维,期望的结果自然是1
2
3[1]
[2]
[3]
但是,为什么每次函数调用,会使用到上次调用的参数 list 对象呢?答案就在 Python 解释器是如何解释该方法定义的了;Python 解释器会将方法1
2
3def f(a, L=[]):
L.append(a)
return L
解释为1
2
3l = []
def f(a, l):
....
也就是说,方法f
的参数L=[]
实际上被定义为了一个独立于方法体的全局变量
,自然每次函数f
调用的时候,都会重复引用到相同的参数,既全局变量 l
;Ok,那既然默认参数的最佳实践是,不要去使用可变类型对象作为默认参数,那么,如果,我就是有这么一个可变类型 list 需要作为参数呢?那么就需要使用一些变通的方式了,下面,一般惯用的方式是使用None
对象来替换可变类型参数,如下所述,1
2
3
4
5def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
这样,就能确保,在每次调用方法f
之后,都在局部方法的内部创建了一个局部 list 变量 L;
可变参数
1 | def calc(*numbers): |
如上所述,在方法参数名前面跟一个*
号,表示该参数为可变参数;这样,我们就可以输入任意多个参数,包括传入 0 个参数;
1 | >>> calc() |
那么,可变参数执行的原理是什么呢?其实,python 在执行上述可变参数的函数调用过程中,直接将所有的参数转换为一个tuple
,既 (1,2,3),要注意的是,既然是tuple
,那么在函数内部,是不允许对可变参数 numbers 进行更改的;
如果,我们已经有一个 list 或者 tuple 对象了,那么如何调用该可变参数函数呢?一种调用可变参数的方式是1
2
3
4
51, 2, 3] nums = [
0], nums[1], nums[2]) calc(nums[
1
2
3
但是,上面的调用方式过于笨拙,Python 为了简化其调用方式,可以直接在 list 和 tuple 对象的参数名前面加上*
使其变为可变参数,这样我们就可以直接调用了;1
2
3
4
51, 2, 3] nums = [
calc(*nums)
1
2
3
或者,1
2
3
41,2,3]) calc(*[
1
2
3
综上,可变参数其实就是 Python 为了提供了使得数组能够成为参数的能力;
关键字参数
1 | def person(name, age, **kw): |
如上,我们通过**kw
定义了一个关键字参数,与任意参数不同,方法的执行过程中,关键字参数被转换为一个 Dict 对象;来看看下面调用的例子,
可以不传入关键字参数,
1 | 'Michael', 30) person( |
可以传入任意个数的关键字参数,注意
,特别注意
,这里约定,参数传递必须给出键的名字
;
1 | 'Bob', 35, city='Beijing') person( |
类似于可变参数直接将 list 和 tuple 对象作为参数进行传递(调用过程中,在参数名前面加上*
),这里同样可以在 dict 对象的参数名前面加上**
作为参数直接调用;
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
可以看到通过**extra
将 Dict 对象转换为关键字参数传入;不过这里要特别注意的是,通过**extra
将 Dict extra 转变为关键自参数的传递过程,是值传递
,也就是说,这里传递的是是 Dict extra 的一份副本,函数内部对 extra 的改变不会改变外部的 extra;为了验证,我们看看下面这个例子,
笔者对 person 方法内部做了些许变动,对传入的 extra 对象的键值做了变更,
1 | def person(name, age, **kw): |
调用,可以看到,内部的 extra 对象的 city 改成了 Chengdu;
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
可以看到,外部 extra 保持不变;
1 | >>> extra |
这就说明了,上述的参数传递过程是值传递
;
命名关键字参数
由于关键字参数,函数的调用者可以传入任意不受限制的关键字参数,所以,想要知道到底传入了哪些关键字参数,就必须在函数内部进行判断,
1 | def person(name, age, **kw): |
所以,要知道关键字参数中是否传入了 city 或者 job,我们必须对传入的关键字参数 kw 的内容进行判断;
往往,我们需要限制关键字参数的名字,以限定传入的关键字参数;比如,我要限定传入的关键字参数仅有city
和job
两个参数,该如何做呢?
命名关键字的常规定义方式
1 | def person(name, age, *, city, job): |
python 规定,通过一个符号*
作为区分,后续的参数 city 和 job 就是命名关键字参数;
1 | 'Jack', 24, city='Beijing', job='Engineer') person( |
并且只能有 city 和 job 两个关键字参数;如果传入第三个为定义的关键字参数 age 则报错;1
2
3
4person('Jack', 24, city='Beijing', job='Engineer', age=20 )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() got multiple values for argument 'age'
含有可变参数的情况
如果参数中包含有一个可变参数,那么后续的参数自动的就成为命名关键字参数,也就是说 city 和 job 就是命名关键字参数;
1 | def person(name, age, *args, city, job): |
命名关键字参数可以有缺省值
1 | def person(name, age, *, city='Beijing', job): |
调用
1 | 'Jack', 24, job='Engineer') person( |
混合参数
上述五种参数类型都可以组合使用,但是,组合使用的时候,其参数定义的顺序必须是,位置参数(必选参数)、默认参数、可变参数、命名关键字参数和关键字参数;下面我们定义出两个不同的函数 _f1_ 和 _f2_ 来看看他们相关的调用情况,
1 | def f1(a, b, c=0, *args, **kw): |
1 | 1, 2) f1( |
1 | def f2(a, b, c=0, *, d, **kw): |
1 | 1, 2, d=99, ext=None) f2( |
tuple 和 dict 作为调用参数;
1 | 1, 2, 3, 4) args = ( |
由可变参数一节我们知道,可以通过在 tuple 或者 list 前面加上 *
使其变为可变参数,通过关键字参数小节我们知道,在 dict 前面加上 **
使其变为关键字参数;上面,将 args 和 kw 分别作为可变参数和关键字参数传入方法 _f1_,好玩的事情便发生了,args 的头三个元素分别作为了位置参数值、默认参数值,最后一个作为可变参数值.. 这的确是好玩的地方;
1 | 1, 2, 3) args = ( |
调用 _f2_ 的情况就更好玩了,将关键字参数 kw 的一部分作为默认参数值,一部分作为关键字参数值;
由此,我们可以知道,对一任意的函数,都可以通过 *args
和 **kw
的方式去调用他们;
内嵌函数和闭包
内嵌函数
闭包是嵌入式函数调用过程中的必然产物,也是函数式编程得以实现的底层基础;那么闭包是如何产生,它的作用范围又是如何的呢?笔者试图通过两个简单的程序来说明,进入 Python 3 shell,
1 | def game(): |
笔者写该函数的目的是,试图返回一个内嵌函数 increase_score 的引用,并通过该引用调用此内嵌函数,在调用过程中,引用外部函数 game 的局部变量 score;
执行,1
2
3
4
5
6 f = game()
f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in increase_score
UnboundLocalError: local variable 'score' referenced before assignment
调用不成功,报错,说局部变量score
在还没有被赋值的情况下就被引用了,所以不合法;不过,这个错误没有说清根本的原因,根本原因是,Python 在解释执行到 increase_score 内嵌函数内部语句 score = score + 1
时候,产生了歧义,等号左边的 score 告诉 python 解释器这是一个 increase_score 内嵌函数的局部变量,所以,python 解释器会把这里的 score 当做是函数 increase_score 的局部变量,而不再是引用的外部函数 game() 的局部变量 score,所以,自然也就报错,局部变量 score 在还没有被赋值的前提下(既是没有被初始化的前提下)就提前赋值了;
从上面的错误例子中,我们要得到的经验是,内嵌函数是无法直接通过赋值的方式去修改外部函数的局部变量的;这点,与 javascript 有很大的不同;所以,上述的代码应修改为,
1 | def game(): |
以同样的方式执行,
1 | >>> f = game() |
这样呢,当 python 解释器解释执行到 increase_score 内嵌函数内部语句 s = score + 1
的时候,就会把 score 当做是外部函数 game() 的局部变量的引用,不再有之前所导致的歧义了;
闭包
那么闭包呢?你想说的闭包在哪呢?ok,别急,我们再来看看如下的几次调用,也许你会明白闭包的作用了,
1 | >>> f() |
可见,无论我们执行多少次,返回的总是 101,这能说明什么呢?对呀,是不是和你之前所认知的局部变量的概念有所矛盾呢?局部变量不就是用完既被释放掉吗?那为什么每次调用 f() 的时候,外部函数 game() 的局部变量 score = 100 一直没有被释放掉呢?每次调用,它都在那儿;是的,你真的发现了什么;对,你发现的就是闭包的特质,笔者试图用一句话来总结,那就是,“一个局部变量如果被其内嵌函数的引用所引用,如果内嵌函数的引用一直存在,那么该局部变量一直不会被销毁,除非,该内嵌函数的引用被销毁了。”
好的,我们将上述的例子在做一次调整,
1 | def game(score): |
python 对基本变量的传递是值传递,所以,score 依然是 game 的局部变量;
变量作用域以及关键字 nonlocal 和 global 的作用
变量在 Python 的作用域非常的特殊,它的作用域可以是模块级别的,可以是类级别(或者实例级别),也可以是方法级别;这里,作者通过作用域在方法级别的变量来详细的描述一下nonlocal
和global
关键字的作用;
1 | def scope_test(): |
如上所述,我们在方法 scope_test() 中定义了局部变量 spam;看看执行调用的情况
1 | scope_test() |
我们一条一条的来分析对应的输出结果,
第一行输出结果,对应方法 do_local(),可见,内嵌方法是不能访问外部方法的局部变量 spam 的,可以通过在方法内部的第一行添加 print(spam) 来检验,添加以后,会报错,提示找不到 spam 变量;所以,do_local() 内部方法对 spam 的赋值并不会影响到 scope_test() 方法中的局部变量 spam,因为 python 解释器会将此两个变量视为两个不同的变量;
第二行输出结果,对应方法 do_nonlocal(),与 #1 不同的是,这里通过关键字
nonlocal
改变了 spam 的作用域,之前的作用域是绑定在方法 scope_test() 上的,nonlocal
关键字的作用就是取消此绑定,可以供 scope_test() 方法内部的其它嵌入方法调用;注意,仍然不能在全局范围内被访问;第三行输出结果,对应方法 do_global(),通过关键字
global
将局部变量 spam 的作用范围提升到了模块级别,可以通过如下的输出语句进行验证;1
"In global scope:", spam) print(
匿名函数(lambda)
Python 通过关键字lambda
来定义匿名函数,看个例子
1 | lambda x: x * x f = |
执行一下,
1 | 2) f( |
可见,上述lambda
函数的定义等价于1
2def f(x):
return x * x
要注意两点,
- 冒号
:
前面的 x 表示的是参数 - 由于
lambda
规定,只能写一行,所以,x * x
的计算结果既作为返回值;
高阶函数
能够把函数作为参数传入的函数,这样的函数,我们称之为高阶函数;来看一个最简单的高阶函数,
1 | def add(x, y, f): |
可见,函数 add() 的第三个参数 _f_ 接受的是一个函数的引用;执行以下,
1 | >>> add(-5, 6, abs) |
map/reduce
Python 的 map/reduce 衍生自 MapReduce: Simplified Data Processing on Large Clusters,也就是 10 多年前,Google 的那篇 Map/Reduce 的论文;所以,大体上,map 就是对输入的数据按照某种规则进行转换,可以理解为对原始数据进行清洗,并运算出想要得到的结果;然后 reduce 就是将各个节点上 map 以后的结果进行归并;
map
直接来看一个例子,
先定义一个函数 _f_,该函数就是后续对原始数据进行转换和清洗的规则;
1 | >>> def f(x): |
执行,
1 | >>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) |
从上述执行的结果中,不难发现,通过函数map
将输入的原始数据 [1, 2, 3, 4, 5, 6, 7, 8, 9] 通过规则 _f_ 进行了转化,进而得到了输出结果 [1, 4, 9, 16, 25, 36, 49, 64, 81];
Ok,那么下面,我们就来总结一下什么是map
,map
函数接收两个参数,一个是转换规则
,通常以一个函数的形式表示(备注,该函数只能接受一个参数),另外一个是需要被转换或者清洗的原始数据
,接收的是一个 Iterable 类型的对象;map
函数返回的是一个经过清洗和转换的 Iterable 对象;上面的例子中,通过list()
函数将返回结果r
既 Iterable 对象的结果都打印了出来;
再来看一个典型的例子,我们需要把一个整形数组的内容全部替换为字符,怎么做呢?
1 | >>> r = map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]) |
Ok,map
归纳起来,就是对原始数据通过某种规则进行转换,然后再输出转换以后的数据;
reduce
reduce
的作用就是,将输入的数据,通过某种规则,两两归并,并输出归并以后的结果;
看一个非常简单的例子,对一个数组求和,来理解reduce
的内部运行机制,
首先,定义归并的规则,该规则是一个函数
,并且必须接受两个参数
1 | def add(x, y): |
再次,通过reduce
函数对数组求和;1
2
3>>> from functools import reduce
>>> reduce(add, [1, 3, 5, 7, 9])
25
Ok,从这个例子中,我们可以窥探到reduce
的内部运行的机制,按照数组的顺序,进行两两求和,显示头两个求和,然后将其求和的结果再与第三个元素进行求和,以此类推,直到数组中的最后一个元素求和完毕;当然,上面的例子可以简单的用函数 sum 来完成,但是通过这个简单的例子,我们能够非常直接和容易的理解到reduce
函数内部的运行机制;上述的运行机制可以通过下面的执行过程来归纳,
1 | reduce(add, [1, 3, 5, 7, 9]) = add(add(add(add(1, 3), 5), 7), 9) |
归纳起来就是,每次执行的结果作为过滤规则的下一次输入,与下一个元素进行归并运算;
map 和 reduce 结合使用
python 内置了int(str)
方法,能够将字符串转换为 int;那么有没有一种非常简单的方式,来模拟实现 该 python 的内置方法int(str)
呢?答案是有的,最简单的方式,就是使用map
和reduce
的组合方式;看下面的这个例子,
1 | from functools import reduce |
函数 str2int 返回的是一个 reduce
函数,该reduce
函数接受一个map
函数的返回结果作为原始数据的输入,map
函数将输入的字符数组转换为对应的数字数组,并作为reduce
的输入,最后,通过fn
函数将数字数组进行归并运算,最终得到相应的整数 int;看看,执行的结果,
1 | >>> str2int('12345678') |
filter
filter
的调用方式与map
和reduce
的调用方式非常的类似,也是接受两个参数,一个是过滤规则(一个函数
,该函数只接受一个参数),一个是输入的原始数据(一个Iterator
类型的对象);filter
的作用是,根据过滤规则的返回值是True
还是False
决定保留还是丢弃该元素;
看一个非常简单的例子,过滤一个 list 中的元素,只保留偶数,去掉奇数;
首先,定义过滤规则,
1 | def is_even(n): |
其次,调用filter
开始对偶数进行过滤,并得到筛选的结果,
1 | >>> r = filter(is_even, [1, 2, 4, 5, 6, 9, 10, 15]) |
sorted
顾名思义,sorted
内置函数就是对某个数组进行排序所使用的;
1 | >>> sorted([36, 5, -12, 9, -21]) |
但是,如果我们想对该数组中的绝对值进行排序呢?sorted
函数同样可以接受一个规则,在排序之前,对原始数据进行转换,该规则对应一个函数,该函数仅接受一个参数;看看,我们如何对数组的绝对值进行排序;
1 | >>> sorted([36, 5, -12, 9, -21], key=abs) |
通过默认参数 key 传入函数abs
,该函数的作用就是为每一个元素进行绝对值转换;并将转换结果进行排序,也就得到了上述的结果;
再看一个例子,
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit']) |
可以看到,默认情况下,sorted
是根据字符的 ASCII 码值进行比较排序的,由于Z
< a
,所以,Z
排到了前面;
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) |
这样,在排序之前,对输入字母进行 lowercase 的转换,通过函数str.lower
进行转换,那么就可以得到想要的输出了;
如果需要倒叙排序呢?
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) |
通过传入 reverse = True
参数即可实现;
装饰器
装饰器 Decorator,类似于 Spring 的 AOP,在不改动原有函数的前提下,对现有函数添加新的功能点;
两层嵌套
来看下面的这个例子,
1 | def say_hello(): |
如果我们想要在不改动该方法的基础上,在调用函数 say_hello 方法以前,输出当前的时间戳,该怎么做呢?如果是 Java,大家自然会想到 AOP 切面,那如果是 python 呢?那就是笔者要给大家所要介绍到的,那就是装饰器(Decorator);下面我们就来写这么一个装饰器,
1 | import time |
上面,我们定义了一个装饰器 log,接受一个方法的引用作为参数 func,返回装饰器 log 的嵌入函数引用,在嵌入函数内部,我们追加了 AOP 的相关的逻辑,打印出当前的时间戳,然后再调用方法 func;可以看到,装饰器 log 就是一个包含嵌入函数的高阶函数;下面,我们看看如何来使用它;
1 |
|
是的,使用它会非常的简单,直接在原有的方法上像注解一样加入该装饰器,@log
;使用它,
1 | say_hello() |
可以看到,我们在方法 say_hello 方法中织入了新的逻辑,打印出了当前的时间戳;那么 python 是如何做到的呢?背后的原理是怎样的呢?很简单,当 python 的解释器解释执行到上述的@log
语句的时候,会将其解释为调用如下的操作,
1 | say_hello = log(say_hello) |
ok,这下就非常清楚了,这就是设计模式中典型的装饰器模式,通过封装原有的对象,在调用过程中,返回一个新的对象给当前的引用 say_hello;
三层嵌套
如果,我们需要给装饰器log
传入参数,该怎么做呢?这个时候,我们需要三层嵌套函数来实现了;那么下面,笔者将试图将二层嵌套中的例子中的业务规则稍加改动,除了时间以外,我们希望加入是谁在调用,而谁调用,根据不同的用户将有不同的输入,所以,我们需要在装饰器log
上传入参数;
1 | import time |
1 |
|
下面我们来执行一下,
1 | >>> say_hello() |
可以看到,我们为装饰器传入了参数;
总结
装饰器的强大之处远不止于此,在调用原生方法之前,我们可以拦截参数,对参数进行过滤或者修改等;
偏函数
偏函数的作用就是将原有函数的某个参数进行固定,然后返回一个新的函数的引用;比如,我们有下面的这样一个简单的例子,函数 person,有一个默认参数是 gender,目前设置为’男’性;
1 | def person(name, gender='男'): |
那么,如果现在我们有这样一个新的需求,就是说,在沿用原来的方法 person 的前提下,但又不想改动源码的前提下,想新增一个默认参数 gender = ‘女’,那么该如何做呢?这个时候,就是偏函数
发挥作用的地方了;
1 | import functools |
这样,我们通过偏函数,在原有带默认参数 gender=’男’ 的 person 函数的基础上,新建了一个默认参数 gender=’女’ 的一个新的函数的引用 person2;看看执行的效果,
1 | 'Lucy') > person2( |
这样,person2 就在原有的函数 person 的基础上,构造得到了一个默认参数 gender = ‘女’ 的新的 person 函数的引用;it’s cool,至少,笔者在写到这里的时候,觉得 python 在各方面的扩展性上面做的非常的灵活;