函数
函数
homepage
#函数 ##函数 在我们实际编写程序的时候,我们会发现,很多时候需要我们对大量的变量进行类似的操作。如果我们每次都用一行行指令代码去对每一个变量进行操作, 程序会变得非常庞大而非常混乱。因而,我们可以将某些固定的操作或运算的代码封装起来,在每次需要的时候进行调用,这就是定义并调用函数。 在这节中,我们将学习如何调用函数,定义函数及一些与函数有关的高级操作。函数的编写,将是我们之后进行面向对象编程的重要基础。 这一部分的基本操作,请大家反复练习,熟练掌握。 ##函数的调用 在Python中已经预先内建了一些函数,下面是Python的官方文档中列出的内建函数:  在这一节中,我们会用其中的一些作为例子,来学习函数的调用。 例如,在Python的内建函数中,有一个非常常用的函数sum()。这个函数的作用是求和。下面我们来看一个例子: ``` l = [1, 2, 3, 4, 5] print(sum(l)) ``` 我们可以看到打印出了最终结果15。这里要强调的是,在我们调用函数,尤其是不是我们自己编写的函数的情况下,一定要搞清楚这个函数的输入的类型是什么,输出的类型又是什么。 以sum()函数为例,我们看官方文档中对函数sum()的描述  我们看到,这个函数,可以接受两个参数,其中一个是必要的,一个是可选的。第一个参数是“iterable”,也就是“可迭代的”,例如, 一个list或者一个tuple。第二个可选的参数是“起始数值”,即前面可迭代变量中加和,加在起始数值之上。它的输出就是一个数字。例如: ``` l = [1, 2, 3, 4, 5] print(sum(l, 100)) ``` 输出的结果就是115。 而如果我们输入的参数的类型或者使它输出的参数的类型与它的设计不符的时候,就会报错。例如下面的代码: ``` l = '12345' print(sum(l)) ``` 其中l是一个string,而不是一个可迭代的对象。又或者下面的情况: ``` l = [1, 2, 3, 4, 5] x, y = sum(l) ``` 这段代码会报错就是因为sum()的输出是一个数字,而不是一个可迭代的对象。 这些错误看起来非常离谱,是因为sum()这个函数非常简单。而在更加复杂的情况下,因为输入参数或者输出变量的类型与设计不符而出现问题是非常常见的。 所以当我们调用函数,尤其是调用不是自己编写的函数的时候,一定要注意查询函数设计的输入参数的类型和输出的结果的类型。如果是Python的内建函数, 我们还可以用help函数来查询相关文档。例如: ``` help(sum) ``` 我们就可以得到如下的输出 ``` Return the sum of a 'start' value (default: 0) plus an iterable of numbers When the iterable is empty, return the start value. This function is intended specifically for use with numeric values and may reject non-numeric types. ``` ##函数的定义 尽管Python有很多内建函数,但是更多的功能,需要我们自己实现。因此,我们可以自己定义函数来使用。例如,我们如果想要完成一个非常简单的任务, 我们希望建立一个函数“f”,这个函数接受一个参数x,而输出这个参数的平方加1。我们可以用下面的代码来实现: ``` def f(x): a = x ** 2 + 1 return a ``` 如果我们调用这个新建的函数,我们就可以看到一系列输出结果: ``` print(f(5)) print(f(7)) ``` 可以得到26和50的输出。 我们也可以让函数接受多个参数,返回多个结果。例如,我们可以定义一个函数“g”, 这个函数接受x,y,z三个参数,并且依次返回 x+y,y+z,x+z。 ``` def g(x, y, z): a = x + y b = y + z c = x + z return a, b, c ``` 同学们可以试着自己定义并调用以下这个函数。 这里需要强调的一点是,我们在定义函数时,尽量不要使用内建函数的函数名。Python在进行编写时,对于自建函数和内建函数是“一视同仁”的, 并不会因为已经有了同名的内建函数就阻止你去创建一个同名的函数。 尽管一部分比较优秀的IDE可以提醒你这点(例如Pycharm),但Python本身不会阻止你这么编写或者运行。 可以说在Python中,是一切都可以编辑的。而这样做的结果,就是一个内建函数被你新建的函数替代了。 如果我们只是自己去编写一段代码自己运行的话,这样做的影响可能还不大。但是如果我们编写的代码被别人维护的时候,这么做就很可能给代码的可读性造成比较坏的影响。 并且,如果某个项目是我们与其他人共同开发的话,当我们的代码被复用时,在这之后编写的程序中如果按照原来的定义的理解来调用了某个同名的内建函数, 整个代码的运行就会出现错误。 这里建议当我们不太确定时,可以进行一次检测,例如,如果我们想要定义一个名为“abs”的函数时,我们可以在console里面用下面的代码进行测试: ``` print('abs' in dir(__builtins__)) ``` 我们会发现,结果是布尔值True,也就是说,已经有被命名为“abs”这个函数名的内建函数了,尽量不要使用了。 ###默认参数 在我们定义函数时,我们可能为了使用方便,而对某些参数有一个默认的值。这个时候,我们就可以使用默认参数。例如,我们要写一个函数, 来计算某一个数的n次幂。而在实际的使用场景中,我们知道平方的使用频率要比其他运算多一些,那么,我们希望默认计算2次幂,但是在我们需要的时候呢, 也可以进行更改,这样,我们就有下面的例子: ``` def expo(x, n=2): a = x ** n return a print(expo(2)) print(expo(2, 3)) ``` 当我们运行这段代码,就会得到4和8的输出。这里可以清楚地看到,我们不对默认参数进行赋值的时候,默认参数就是我们在进行定义时的赋值。 而当我们对它进行赋值,它的值就变成了我们在调用时的赋值。 在使用默认参数对函数进行定义的时候,有一点需要特别注意的是,默认参数尽量是一个不可编辑的对象,否则,可能会出现一些我们预想之外的问题。 比如在下面这段代码中: ``` def test_f(x, l=[1, 2, 3, 4]): l[x] = 5 return l a = test_f(0) print(a) b = test_f(1) print(b) c = test_f(2) print(c) ``` 我们可以看到,输出分别是[5, 2, 3, 4],[5, 5, 3, 4],[5, 5, 5, 4]。这里可以看到,只要我们不对默认参数进行赋值,无论第几次调用, 它总指向相同的内存位置。这就导致了当我们的函数中存在对默认参数**编辑**的操作时,默认参数的值的改变就被下一次调用继承了。 这就会出现很多我们在使用时无法控制的情况。实际上,在编程过程中,一般的IDE都会将默认参数可变这个问题标示出来,提醒你这种情况不应该出现。 我们在编程过程中,要对IDE中的各种报错或者提示予以一定重视。 ###可变参数 在我们定义一个函数时,可能会遇到我们需要赋值一组数的情况,例如下面的代码中。 ``` def f(x, l): for i in l: i = i * x print(i) f(5, [1, 2, 3, 4]) ``` 这里我们定义一个参数l,赋值时给它赋值一个list(或者是tuple),完成相应的操作。而这种需求,我们也可以用可变参数来完成。例如, 我们可以这样改写这段代码: ``` def f(x, *l): for i in l: i = i * x print(i) f(5, 1, 2, 3, 4) ``` 要注意到,这里我们就定义了一个可变参数l(形式是l的前面有**一个**星号),然后当我们传入1,2,3,4这四个参数的时候, 程序自动地给我们把这四个参数包装成了一个tuple,(1, 2, 3, 4)。 可变参数可以接收任意个参数,并且把它们包装成一个tuple以传入程序中。 ###关键字参数 在定义函数地时候,我们还可以传入这样一种参数,我们在定义函数时,并不确定将来一定能传入这种参数,但是存在这样的可能, 那么这种信息我们也需要接收下来并进行保存。例如,我们定义一个函数 staff_info,来接收员工信息。我们要求, 每个员工必须传入的信息包括了姓名,性别,年龄,ID。但是有些情况下,我们可能还需要传入员工的身高、级别等。那么我们可以像下面这样定义这个函数: ``` def staff_info(name, gender, age, staff_id, **others): print('Name: ', name, 'Gender: ', gender, 'Age: ', age, 'ID: ', staff_id) if 'height' in others: print('Height: ', others['height']) print(others) staff_info('Sam', 'Male', 37, '0001', height=187, weight=88, level='P1') ``` 可以看到,我们在这段代码中用变量名前面加上**两个**星号的方式,定义了一个关键字参数“others”。这个操作在实际上的运行中, 是将输入的相应的内容,保存成了一个名为others的dict。这种情况常见于,当我们定义一个函数,其中含有一些非必要选项时使用。 并且在后续的编程中检测某些特定参数是否被传入,以进行相关必要的操作。我们还可以控制可以传入的关键字参数的类型, 如果传入了我们规定以外的参数,就要报错。例如我们改写上面的代码: ``` def staff_info(name, gender, age, staff_id, *, level, height): print('Name: ', name, 'Gender: ', gender, 'Age: ', age, 'ID: ', staff_id) print('Height: ', height) print('Level: ', level) staff_info('Sam', 'Male', 37, '0001', height=187, level='P1') staff_info('Sam', 'Male', 37, '0001', height=187, weight=88, level='P1') ``` 但是要注意的是,使用这种方式定义关键字参数的时候,所定义的关键字参数必须被赋值,否则就会报错。如果我们希望有些参数不必要传入又要怎么办呢? 例如,我们可以给这个关键字参数一个默认值None。 ``` def staff_info(name, gender, age, staff_id, *, level=None, height=None): print('Name: ', name, 'Gender: ', gender, 'Age: ', age, 'ID: ', staff_id) if height: print('Height: ', height) else: pass if level: print('Level: ', level) else: pass staff_info('Sam', 'Male', 37, '0001', height=187) staff_info('Sam', 'Male', 37, '0001', height=187, level='P1') staff_info('Sam', 'Male', 37, '0001', height=187, weight=88, level='P1') ``` 这种形式的关键字参数被称为“命名关键字参数”。要注意的是它的命名方法,在命名关键字参数之前要有一个星号和位置参数相隔开。 有的同学可能会产生疑问,既然命名关键字参数也必须传入,那么,我们为什么不用一般的位置参数呢? 其原因就是,由于命名关键字参数是在调用时必须使用相关的关键字来进行,而不能根据位置直接赋值。这样,如果我们定义一个可以接收很多参数, 作用非常多样,非常庞大的数量的参数的话,那么,命名关键字参数就会变得非常有用。 ###参数的顺序 我们在定义函数时,可以使用位置参数、默认参数、可变参数、关键字参数等多种参数,但是在定义时要注意这几种参数的顺序。 排在最先的必须是位置参数,之后是默认参数,然后是可变参数,最后是关键字参数。在没有可变参数的情况下, 命名关键字参数需要使用星号来和前面的参数隔开。在有可变参数的情况下则可以直接定义命名关键字参数。 这个部分,需要强化记忆和经常练习。 ###匿名函数 我们在之前学习函数的定义时,学习的是函数的最标准的定义方式,例如我们可以定义一个函数sum_function,它接收两个参数,x和y, 并输出它们的加和: ``` def sum_function(x, y): return x + y print(sum_function(3, 4)) ``` 而实际上,当我们定义一个像这样很简单的函数的时候,也可以使用lambda语句来进行定义,使代码形式变得简洁,例如上面的函数 sum_function,我们就可以这样定义: ``` sum_function = lambda x, y: x + y print(sum_function(3, 4)) ``` ##函数式编程 函数式编程是编程中一种重要的思想,它强调了程序本身蕴含的映射的关系,强调映射关系的建立而非指令与步骤。在初学阶段, 我们可能无需如此深入地纠结函数式编程到底是什么,理解到“函数是函数,但函数不止是函数”这样一个层面,就已经足够了。 ###高阶函数 我们提到“函数是函数,但函数不止是函数”时,这里面有一点我们需要理解地就是,函数不止是函数,它也可以作为参数传入另外一个参数, 而以此来定义“函数的函数”。而这种“函数的函数”,或者说接收函数作为参数的函数,就是高阶函数。 ####内建高阶函数 Python中有许多内建高阶函数。其中最常见的是map和filter两个,这两个经常会被用在实际的编程过程中。 #####map函数 map函数的作用是把一个函数逐次运用在一个**可迭代对象**上。例如下面的例子里: ``` def f(x): return x ** 2 a = map(f, [1, 2, 3, 4]) print(list(a)) ``` 这一小段代码中,我们先定义了一个函数f,然后map把它逐个运用在list[1, 2, 3, 4]上。这里要注意的是,map返回给我们的是一个 **map对象**,如果我们直接打印它,出来的不是结果[1, 4, 9, 16]。我们需要把这个map对象传进list,这样就会打印我们期望的结果。 我们之前学过了匿名函数,这段代码如果使用了匿名函数甚至可以写的更加简洁: ``` print(list(map(lambda x: x ** 2, [1, 2, 3, 4]))) ``` #####filter函数 对于一个进行一系列判断,并最终返回布尔数True或者False的函数f,filter可以把它依次运用在一个可迭代对象上,并返回一个filter对象, 留下其中能让f返回True的元素,而除去其中让f返回False的元素。这个高阶函数filter正像一个滤网,过滤掉其中不符合要求的内容, 留下其中符合要求的内容。例如,我们想要过滤掉0-99范围内,不能被3整除的数,我们可以用下面一行代码来实现: ``` print(list(filter(lambda x: not x % 3, range(100)))) ``` 得到的结果就是 [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99]。 ###偏函数 偏函数是指,我们定义一个新函数,而这个函数是一个既存函数指定一部分参数的形式。 我们来看下面一段代码: ``` def expo(x, y): return x ** y def expo3(x): return expo(x, 3) print(expo3(15)) ``` 这里我们先定义了一个函数expo,用来计算幂函数。然后我们定义了一个新的函数,用来计算3次幂。而Python中还提供了一个工具专门用来定义偏函数。 上面的一段代码我们也可以这样改写: ``` from functools import partial def expo(x, y): return x ** y expo3 = partial(expo, y=3) print(expo3(15)) ``` 偏函数的主要作用是当我们需要多次以某个固定参数调用某一函数时,新建偏函数就能让调用变得简单一些。 ###函数修饰器 函数修饰器的主要作用是在一个函数运行的前后做一些工作,以增强函数本身的功能。一般来说是用一个函数来包裹要增强的函数。 函数修饰器的典型形式是在定义新函数前面使用“@”符号,来使用修饰器,我们来从下面一个例子中学习以下它的使用方法。 我们希望编写一个函数修饰器func_declare,使得一个函数在调用时,会打印相应的提示,申明一下。 ``` # 修饰器的实质还是函数,所以我们用定义函数的形式,来定义一个修饰器,传入它的对象是一个函数func def func_declare(func): # 在函数中定义一个函数,用来申明调用的函数func的名字, 注意其使用的参数是把所有参数正常地传入 def declare_name(*args, **kwargs): # 在调用函数之前打印函数的名称信息,func.__name__表示了这个函数的名称属性 print('Now calling', func.__name__) # 打印信息之后,正常地调用函数,正常地传入所有参数 return func(*args, **kwargs) # 这个修饰器的作用是调用内部定义的函数declare_name return declare_name # 使用修饰器 @func_declare # 正常地定义函数 def expo(x, ex): return x ** ex # 测试一下,调用使用了修饰器地函数expo a = expo(3, 3) # 打印一下结果a print(a) ``` 我们看到,实际上在这个修饰器中,我们是用declare_name这个函数包裹了需要修饰的函数,让它在运行之前,打印了一条信息。 然而,我们在实际使用的时候,可能会发现一个问题,如果我们在上面定义的函数下面追加一条命令: ``` print(expo.__name__) ``` 会发现,与我们预想的不一致的是,这里打印出来的函数expo的函数名不是“expo”了,变成了“declare_name”了。 这是因为我们实际上是用一个函数declare_name包裹了expo之后,连同相应的函数的信息,也只显示declare_name的了。如果你为expo编写了文档的话, 你会发现,expo.__doc__也无法调用它了。那么为了解决这个问题,我们要怎么做呢?在Python的function tools库中,定义了一个修饰器wrap可以解决这个问题。 我们可以在定义修饰器时调用这个修饰器,来提示程序,这个函数只是“包裹”了里面的函数,里面的函数才是真正的内容。例如我们去改写上面的代码: ``` # 从function tools中读取wrap from functools import wraps def func_declare(func): # 提示这个修饰器是包裹了func函数 @wraps(func) def declare_name(*args, **kwargs): print('Now calling', func.__name__) return func(*args, **kwargs) return declare_name # 使用修饰器 @func_declare # 正常地定义函数 def expo(x, ex): return x ** ex # 测试一下,调用使用了修饰器地函数expo a = expo(3, 3) # 打印一下结果a print(a) # 测试一下现在expo的名称信息是不是可以正常传递出来了 print(expo.__name__) ``` 可以看到,现在函数的名称信息已经可以被正确地打印出来了。
content
戻る