8.函数
- 函数是带名字的代码块,用于完成具体的工作。def函数定义,指出函数名。定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
- 要执行函数定义的特定任务,可调用该函数。在程序中多次执行同一项任务时,无需反复编写完成该任务的代码,只需调用执行该任务的函数,让Python运行其中的代码。
- 高阶函数英文叫Higher-order function
8.1实参和形参
形参:函数完成其工作所需要的一项信息。
实参:调用函数时专递给给函数的信息。
8.2传递参数
函数定义中可能包含多个形参,因此函数调用时也可能包含多个实参。向函数传递实参的方式很多,可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由变量名和值组成;还可使用列表和字典。
8.2.1位置实参
1.你调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。简单的关联方式是基于实参的顺序。这种关联方式被称为位置实参。
2.在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。
3.使用位置实参来调用函数时,如果实参的顺序不正确,输出也不会正确。
8.2.2关键字实参和关键字参数
关键字实参是传递给函数的(名称—值)对。你直接在实参中将名称和值关联起来了,因此向函数传递实参时不会混淆。关键字实参让你无需考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。
1 describe_pet(animal_type='hamster', pet_name='harry')
可以传入任意个数的关键字参数:
>>> person('Bob', 35, city='Beijing') name: Bob age: 35 other: {'city': 'Beijing'} >>> person('Adam', 45, gender='M', job='Engineer') name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
8.2.3默认值
编写函数时,可给每个形参指定默认值 。在调用函数中给形参提供了实参时,Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。最大的好处是能降低调用函数的难度。
默认参数必须指向不变对象!
1 def add_end(L=None): 2 if L is None: 3 L = [] 4 L.append('END') 5 return L
8.2.4可变参数
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个 * 号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数。
1 def calc(*numbers): 2 sum = 0 3 for n in numbers: 4 sum = sum + n * n 5 return sum
>>> calc(1, 2) 5 >>> calc() 0
Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3] >>> calc(*nums) 14
*nums表示把nums这个list的所有元素作为可变参数传进去。
8.2.5命名关键词参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
1 def person(name, age, *, city, job): 2 print(name, age, city, job)
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
1 def person(name, age, *args, city, job): 2 print(name, age, args, city, job)
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
>>> person('Jack', 24, 'Beijing', 'Engineer') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: person() takes 2 positional arguments but 4 were given
由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。
命名关键字参数可以有缺省值(默认值),从而简化调用:
1 def person(name, age, *, city='Beijing', job): 2 print(name, age, city, job)
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
>>> person('Jack', 24, job='Engineer') Jack 24 Beijing Engineer
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。
8.2.5等效的函数调用
输出结果相同,调用方法不同。
注意:使用哪种调用方式无关紧要,只要函数调用能生成你希望的输出就行。使用对你来说最容易理解的调用方式即可。
8.2.6空函数(pass)
如果想定义一个什么事也不做的空函数,可以用pass语句:
1 def nop(): 2 pass
pass语句什么都不做,pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
8.2.7避免实参错误
没有给定实参,实参顺序错误,没有注意实参格式(引号等)。
8.2.8 global语句(全局变量)
如果想在一个函数中修改全局变量中存储的值,就必须对该变量使用 global 语句。尽量避免使用全局变量,因为它引入了多余变量到全局作用域。
1 # 1 2 def add(value1, value2): 3 return value1 + value2 4 result = add(3, 5) 5 print(result) 6 # Output: 8 7 8 # 2 9 def add(value1, value2): 10 global result # 全局变量,可以在函数外访问变量 11 result = value1 + value2 12 add(3,5) 13 print(result) # 在函数外访问变量,可以输出结果 14 # Output: 8 15 16 # 3 17 def add(value1, value2): 18 result = value1 + value2 19 add(2, 4) # 没有设置全局变量 20 print(result) 21 Traceback (most recent call last): # 返回错误提示,不能再函数外访问 22 File "", line 1, in result 23 NameError: name 'result' is not defined
8.3返回值
函数并非总是直接显示输出,相反,它可以处理一些数据,并返回一个或一组值。函数返回的值被称为返回值。函数的返回值用return语句返回。
在函数中,可使用return 语句将值返回到调用函数的代码行。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。
8.3.1返回多个值
import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。就可以返回多个值了。
>>>import math >>> x, y = move(100, 100, 60, math.pi / 6) >>> print(x, y) 151.96152422706632 70.0
但其实这只是一种假象,Python函数返回的仍然是单一值:
>>> r = move(100, 100, 60, math.pi / 6) >>> print(r) (151.96152422706632, 70.0) # 返回一个元组
返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
8.3.2让实参变得可选
通过if语句,来判断是否需要这个实参。
8.3.3返回字典
函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。
8.3.4返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数:
1 def lazy_sum(*args): 2 def sum(): 3 ax = 0 4 for n in args: 5 ax = ax + n 6 return ax 7 return sum
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9) >>> f <function lazy_sum.<locals>.sum at 0x101c6ed90>
调用函数f时,才真正计算求和的结果:
>>> f()
25
注意:当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9) >>> f2 = lazy_sum(1, 3, 5, 7, 9) >>> f1==f2 False
f1()和f2()的调用结果互不影响。
8.3.5结合使用函数和while
1 def get_formatted_name(first_name, last_name): 2 """返回整洁的姓名""" 3 full_name = first_name + ' ' + last_name 4 return full_name.title() 5 while True: 6 print(" Please tell me your name:") 7 print("(enter 'q' at any time to quit)") 8 f_name = input("First name: ") 9 if f_name == 'q': 10 break 11 l_name = input("Last name: ") 12 if l_name == 'q': 13 break 14 formatted_name = get_formatted_name(f_name, l_name) 15 print(" Hello, " + formatted_name + "!")
8.3.6闭包
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变,缺点是代码较长,可利用lambda函数缩短代码。
1 def count(): 2 def f(j): 3 def g(): 4 return j*j 5 return g 6 fs = [] 7 for i in range(1, 4): 8 fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f() 9 return fs 10 >>> f1, f2, f3 = count() 11 >>> f1() 12 1 13 >>> f2() 14 4 15 >>> f3() 16 9
8.3.7匿名函数lambda
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) [1, 4, 9, 16, 25, 36, 49, 64, 81]
通过对比可以看出,匿名函数lambda x: x * x实际上就是:
def f(x): return x * x
关键字lambda表示匿名函数,冒号前面的x表示函数参数。匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x >>> f <function <lambda> at 0x101c6ef28> >>> f(5) 25
8.4传递列表
for name in names:你经常会发现,向函数传递列表很有用,这种列表包含的可能是名字、数字或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容。下面使用函数来提高处理列表的效率。
8.4.1在函数中修改列表
1.将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。
2.增加或删除, append ; pop
3.一个函数完成一个任务,函数可以相互调用。优化函数,方便维护和修改。
8.4.2禁止修改函数
为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。
function_name(list_name[:]),[:]相当于复制。
8.5传递任意数量的实参(可变参数)
有时候,你预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参。
def make_pizza(*toppings):
形参名*toppings 中的星号让Python创建一个名为toppings 的空元组,并将收到的所有值都封装到这个元组中。函数体内的print 语句通过生成输出来证明Python能够处理 使用一个值调用函数的情形,也能处理使用三个值来调用函数的情形。它以类似的方式处理不同的调用,注意,Python将实参封装到一个元组中,即便函数只收到一个值也如此。
8.5.1结合使用位置实参和可变实参
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。
8.5.2使用可变实参和关键字实参
def build_profile(first, last, **user_info):
函数build_profile() 的定义要求提供名和姓,同时允许用户根据需要提供任意数量的名称—值对。形参**user_info 中的两个星号让Python创建一个名为user_info 的 空字典,并将收到的所有名称—值对都封装到这个字典中。在这个函数中,可以像访问其他字典那样访问user_info 中的名称—值对。
for key, value in user_info.items(): profile[key] = value user_profile = build_profile('albert', 'einstein',location='princeton', field='physics')
8.5.3使用参数组合
1.在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
2.python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
3.默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
4.要注意定义可变参数和关键字参数的语法:
5.*args是可变参数,args接收的是一个tuple;
6.**kw是关键字参数,kw接收的是一个dict。
7.以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3));
关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})。
使用*args和**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
8.5.4递归参数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
1 def fact(n): 2 if n==1: 3 return 1 4 return n * fact(n - 1)
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
8.6高阶函数
8.6.1高阶函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
1 def add(x, y, f): 2 return f(x) + f(y)
8.6.2内建函数
1.求绝对值的函数abs(),只有一个参数。
2.判断对象类型,使用type()函数:
>>> type(123) <class 'int'> >>> type('str') <class 'str'> >>> type(None) <type(None) 'NoneType'>
3.计算平方根可以调用math.sqrt()函数。
4.lower()返回小写的字符串。
5.__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC') 3 >>> 'ABC'.__len__() 3
6.max函数max()可以接收任意多个参数,并返回最大的那个。
7.如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
8.Python内置的hex()函数把一个整数转换成十六进制表示的字符串。
9.对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现。使用内建的isinstance函数可以判断一个变量是不是字符串。
10.配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态。
>>> hasattr(obj, 'x') # 有属性'x'吗? True >>> obj.x 9 >>> hasattr(obj, 'y') # 有属性'y'吗? False >>> setattr(obj, 'y', 19) # 设置一个属性'y' >>> hasattr(obj, 'y') # 有属性'y'吗? True >>> getattr(obj, 'y') # 获取属性'y' 19 >>> obj.y # 获取属性'y' 19
通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。
11.Python内建了map()和reduce()函数。map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
>>> def f(x): ... return x * x ... >>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> list(r) [1, 4, 9, 16, 25, 36, 49, 64, 81]
reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。
>>> from functools import reduce >>> def fn(x, y): ... return x * 10 + y ... >>> reduce(fn, [1, 3, 5, 7, 9]) 13579
12.Python内建的filter()函数用于过滤序列。filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。关键在于正确实现一个“筛选”函数。注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。
1 def is_odd(n): 2 return n % 2 == 1 3 4 list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])) 5 # 结果: [1, 5, 9, 15]
13.Python内置的sorted()函数就可以对list进行排序。
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
14.sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序。要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True。
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) ['Zoo', 'Credit', 'bob', 'about']
15.Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身。
16.round()函数可以返回任意位的小数。例如:round(2.33333333, 3) 返回 2.333,也可以用来四舍五入,round(1.6) 输出:2
8.6.3装饰器
在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,decorator就是一个返回函数的高阶函数。装饰器:不改变函数本身,增加新的功能。
函数对象有一个__name__属性,可以拿到函数的名字:
>>> def now(): ... print('2015-3-25') ... >>> f = now >>> f() 2015-3-25 >>> now.__name__ 'now' >>> f.__name__ 'now'
Python内置的@property装饰器就是负责把一个方法变成属性调用的:
1 class Student(object): 2 3 @property 4 def score(self): 5 return self._score 6 7 @score.setter 8 def score(self, value): 9 if not isinstance(value, int): 10 raise ValueError('score must be an integer!') 11 if value < 0 or value > 100: 12 raise ValueError('score must between 0 ~ 100!') 13 self._score = value
@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
@unique装饰器可以帮助我们检查保证没有重复值。
8.7将函数储存在模块中
1.为了编写可维护的代码,可以把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少。在Python中,一个.py文件就称之为一个模块(Module)。
2.函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块的独立文件中, 再将模块导入到主程序中。import 语句允许在当前运行的程序文件中使用模块中的代码。
3.通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重点放在程序的高层逻辑上。这还能让你在众多不同的程序中重用函数。将函数存储在独立文件中后,可与其他程序员共享这些文件而不是整个程序。知道如何导入函数还能让你使用其他程序员编写的函数库。
4.为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz。
5.注意:每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块。
8.7.1导入整个模块
要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件,包含要导入到程序中的代码。
1 if __name__=='__main__': 2 test()
使用模块的第一步就是导入模块,当我们在命令行运行模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
8.7.2作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名;类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
private函数或变量不应该被别人引用:
1 def _private_1(name): 2 return 'Hello, %s' % name 3 4 def _private_2(name): 5 return 'Hi, %s' % name 6 7 def greeting(name): 8 if len(name) > 3: 9 return _private_1(name) 10 else: 11 return _private_2(name)
我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
8.7.3导入特定模块
把多个函数储存在一个模块中,用逗号隔开可以从模块中导入任意数量函数。
8.7.4使用as给函数指定别名
1 from pizza import make_pizza as mp 2 3 import pandas as pd 4 5 import numpy as np
如果要导入的函数的名称可能与程序中现有的名称冲突,或者函数的名称太长,可指定简短而独一无二的别名——函数的另一个名称,类似于外号。要给函数指定这种特殊外号,需要在导入它时这样做。
8.7.5使用as给模块指定别名
你还可以给模块指定别名。通过给模块指定简短的别名(如给模块pizza 指定别名p ),让你能够更轻松地调用模块中的函数。相比于pizza.make_pizza() ,p.make_pizza() 更为简洁。
8.7.6使用*导入模块中所有函数
使用星号(* )运算符可让Python导入模块中的所有函数。
然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法:如果模块中有函数的名称与你的项目中使用的名称相同,可能导致意想不到的结果:Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。
8.8函数编写指南
编写函数时,需要牢记几个细节。
1.应给函数指定描述性名称,且只在其中使用小写字母和下划线。描述性名称可帮助你和别人明白代码想要做什么。给模块命名时也应遵循上述约定。
2.每个函数都应包含简要地阐述其功能的注释,该注释应紧跟在函数定义后面,并采用文档字符串格式。文档良好的函数让其他程序员只需阅读文档字符串中的描述就能够使用它:他们完全可以相信代码如描述的那样运行;只要知道函数的名称、需要的实参以及返回值的类型,就能在自己的程序中使用它。
3.给形参指定默认值时,等号两边不要有空格。