愚公系列2021年12月 Python教学课程 12-Python函数

网友投稿 695 2022-05-30

一、什么是函数?

函数(function)是用于完成特定任务的程序代码的自包含单元。在面向对象编程的类中,函数通常被称作方法。不同的函数在程序中扮演着不同的角色,起着不同的作用,执行不同的动作。比如 print()函数可以将对象打印到屏幕上;还有一些函数能够返回一个值以供程序使用,比如 len()将可计算长度的对象的元素个数返回给程序。

二、为什么要使用函数呢?

第一、函数的使用可以重用代码,省去重复性代码的编写,提高代码的重复利用率。如果程序中需要多次使用某种特定的功能,那么只需要编写一个合适的函数就可以了。程序可以在任何需要的地方调用该函数,并且同一个函数可以在不同的程序中调用,就像我们经常使用的 print()和 input()函数一样。

第二、函数能封装内部实现,保护内部数据,实现对用户的透明。很多时候,我们把函数看做“黑盒子”,即对应一定的输入会产生特定的结果或返回某个对象。往往函数的使用者并不是函数的编写者,函数的使用者对黑盒子的内部行为并不需要考虑,可以把精力投入到自身业务逻辑的设计而不是函数的实现细节。只有函数的设计者或者说编写者,才需要考虑函数内部实现的细节,如何暴露对外的接口,返回什么样的数据,也就是 API 的设计。

第三、即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序模块化,从“一团散沙”变成“整齐方队”,从而有利于程序的阅读、调用、修改和完善。例如,假设你正在编写一个实现下面功能的程序:

def read_numbers(): """ 读入一行数字 :return: """ pass def sort_numbers(lis): """ 对数字进行排序 :return: """ pass def avg_numbers(lis): """ 求数字们的平均值 :return: """ pass def show_number(lis): """ 打印柱状图 :return: """ pass if __name__ == '__main__': number_list = read_numbers() sort_numbers(number_list) avg_numbers(number_list) show_number(number_list)

当然,其中的 4 个函数 read_numbers()、sort_numbers()、avg_numbers()和show_numbers()的实现细节需要你自己编写。描述性的函数名可以清楚地表明函数的功能和组织结构,然后可以对每个函数进行独立设计直至完成需要的功能。如果这些函数足够通用化,那么还可以在其他程序中调用它们。

三、 定义函数:

def 函数名(参数): # 内部代码 return 表达式

例如:

def summer(lis): """ 这里是函数的说明文档,doc 的位置 :param lis: 参数列表的说明 :return: 返回值的说明 """ total = 0 for i in lis: total += i return total

在定义函数的过程中,需要注意以下几点:

-函数代码块以 def 关键词开头,一个空格之后接函数标识符名称和圆括号(),再接个冒号。

任何传入的参数必须放在圆括号中间。

函数的第一行语句后可以选择性地使用文档字符串—用于存放函数说明。

函数内容以冒号起始,并且缩进。

使用 return 结束函数。默认返回 None。

return 语句依然在函数体内部,不能回退缩进。直到函数的所有代码写完,才回退缩进,表示函数体结束。

四、 如何调用函数?

函数编写出来就是给人调用的。要调用一个函数,必须使用函数名后跟圆括号的方式才能调用函数。调用的同时要根据函数的定义体,提供相应个数和类型的参数,每个参数之间用逗号分隔。Python 由于动态语言的特点,在做语法和词法分析检查的时候,并不会对参数类型进行检查,但在执行过程中,如果参数类型不符合函数内部运行机制的话,会弹出相应的错误,例如:

>>> all(0, -1, 3) Traceback (most recent call last): File "", line 1, in all(0, -1, 3) TypeError: all() takes exactly one argument (3 given) >>> all([0, -1, 3]) False

Python 内置函数 all()要求提供一个参数,但我们一开始给了 3 个。后面,我们将三个参数作为一个整体列表提供就没有问题了。

1.return 语句:

return 语句用于表示函数执行到此结束,并且返回 return 后面的对象。有时候,函数不需要返回任何值,此时可以不需要 return 语句,它在后台默认给你返回个 None,并且不给任何提示。但是更多的时候我们还是需要 return 一些东西。一旦函数执行过程遇到 return 语句,那么之后函数体内的所有代码都会被忽略,直接跳出函数体。哪怕你现在正在一个循环内。

def func(): pass return # 此时,后面的代码其实是永远无法执行的。 # 但从语法和词法层面,这些没有错误。 print(1) abs(-1) pass

2.return 可以返回什么?

什么都不返回,仅仅 return:return

数字/字符串/任意数据类型: return ‘hello’

一个表达式:return 1+2

一个判断语句:return 100 > 99

一个变量:return a

一个函数调用:return func()

多个返回值,以逗号分隔:return a, 1+2, “hello”

简而言之,函数可以 return 几乎任意 Python 对象。

3. 如何接收函数的返回值?

我们在调用函数的时候,可以将函数的返回值保存在变量中。

def func(): pass return "something" result = func()

而对于同时返回多个值的函数,需要相应个数的变量来接收,变量之间以逗号分隔:

def func(): return 1, [2, 3], "haha" a, b, c = func()

4.参数的传递

函数通常都有参数,用于将外部的实际数据传入函数内部进行处理。但是,在处理不

同数据类型的参数时,会有不同的情况发生。这一切都是因为以下两点。

-Python 的函数参数传递的是实际对象的内存地址。

-Python 的数据类型分可变数据类型和不可变数据类型。

看下面的例子

a = 1 def func(a): print("在函数内部修改之前,变量 a 的内存地址为: %s" % id(a)) a = 2 print("在函数内部修改之后,变量 a 的内存地址为: %s" % id(a)) print("函数内部的 a 为: %s" % a) print("调用函数之前,变量 a 的内存地址为: %s" % id(a)) func(a) print("函数外部的 a 为:%s" % a)

打印结果为:

调用函数之前,变量 a 的内存地址为: 1401140288 在函数内部修改之前,变量 a 的内存地址为: 1401140288 在函数内部修改之后,变量 a 的内存地址为: 1401140320 函数内部的 a 为: 2 函数外部的 a 为:1

为什么当 a = 2 之后,函数内外的 a 的内存地址就不一样了呢?也就是说此后函数内外的 a 是两个不同的对象了。

很多时候,我们被这种类似的问题困惑是因为函数参数的命名不恰当造成的。如果我们把上面的参数名改为 b,可能就好理解多了(注意其中文字的变化)。执行结果是一样的。

a = 1 def func(b): print("在函数内部修改之前,变量 b 的内存地址为: %s" % id(b)) b = 2 print("在函数内部修改之后,变量 b 的内存地址为: %s" % id(b)) print("函数内部的 b 为: %s" % b) print("调用函数之前,变量 a 的内存地址为: %s" % id(a)) func(a) print("函数外部的 a 为:%s" % a)

刚才说的是不可变类型参数,如果是可变类型的,比如列表呢?

a = [1, 2, 3] def func(b): print("在函数内部修改之前,变量 b 的内存地址为: %s" % id(b)) b.append(4) print("在函数内部修改之后,变量 b 的内存地址为: %s" % id(b)) print("函数内部的 b 为: %s" % b) print("调用函数之前,变量 a 的内存地址为: %s" % id(a)) func(a) print("函数外部的 a 为:%s" % a)

执行结果是:

调用函数之前,变量 a 的内存地址为: 34875720 在函数内部修改之前,变量 b 的内存地址为: 34875720 在函数内部修改之后,变量 b 的内存地址为: 34875720 函数内部的 b 为: [1, 2, 3, 4] 函数外部的 a 为:[1, 2, 3, 4]

调用函数时将列表对象 a 的地址传递给了函数内部的变量 b。b.append(4)的时候,根据传进来的内存地址,找到[1,2,3]这个列表对象,在它的后面添加了 4。

可以看出,此时的 a 和 b 实际指向了同一个对象。为什么会这样?因为最关键的b.append(4)这句代码,它不同于“=”赋值语句,不会创建新的变量,而列表作为可变类型,具有 append 方法,这个方法只是对列表的一种调用而已。因此,a 和 b 实际还是同一个对象。

五、 参数类型

绝大多数函数接收一定数量的参数,然后根据实际调用时提供的参数的值的不同,输出不同的结果。前面我们说过,将函数内部的参数名字,定义得和外部变量的名字一样是一种不好的习惯,它容易混淆思维,甚至发生错误。通常我们定义和给函数传递参数是这样的:

x, y, z = 1, 2, 3 def add(a, b, c): return a+b+c add(x, y, x) # 使用变量,传递参数 add(4, 5, 6) # 直接传递值也是可以的。

在上面的例子中,a,b,c 叫做形式参数,简称形参。而 x,y,z 和 4,5,6 叫做实际参数,简称实参,也就是实际要传递的值。而我们通常讨论的参数,指的都是形参。

定义函数时,参数的名字和位置确定下来,函数的接口就固定了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。Python 函数的参数定义灵活度非常大。除了正常定义的位置参数外,还可以使用默认参数、动态参数和关键字参数,这些都是形参的种类。

1.位置参数

也叫必传参数,顺序参数,是最重要的,也是必须在调用函数时明确提供的参数!位置参数必须按先后顺序,一一对应,个数不多不少的传递!

上面例子中的 a,b,c 就是位置参数,我们在使用 add(4, 5, 6)调用时,就是将 4 传给a,5 传给 b,6 传给 c 的一一对应传递。类似 add(4, 5, 6, 7)、add(4)和 add(5, 4, 6)这种“画蛇添足”、“缺胳膊少腿”和“嫁错郎”类型的调用都是错误的。其中,add(5, 4, 6)的调用在语法上没问题,但是输出结果可能和预期的不一致。

注意: Python 在做函数参数传递的时候不会对数据类型进行检查,理论上你传什么类型都可以!

def add(a, b, c): return a+b+c result = add("haha", 2, 3)

但是,上面的 add 函数,如果你传递了一个字符串和两个数字,结果是弹出异常,因为字符串无法和数字相加。这就是 Python 的弱数据类型和动态语言的特点。在简单、方便的时候,需要你自己去实现数据类型检查。

2.默认参数

在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。

def power(x, n = 2): return x**n ret1 = power(10) # 使用默认的参数值 n=2 ret2 = power(10, 4) # 将 4 传给 n,实际计算 10**4 的值

上面例子中的 n 就是个默认参数。默认参数可以简化函数的调用,在为最常用的情况提供简便调用的同时,还可以在特殊情况时传递新的值。但是在设置默认参数时,有几点要注意:

[ ] 默认参数必须在位置参数后面!

如果你违反了这点,在语法层面直接是通不过的。

# 这是一个错误的例子 def power(n = 2,x): return x**n

[ ] 当有多个默认参数的时候,通常将更常用的放在前面,变化较少的放后面。

def student(name, sex, age, classroom="101", tel="88880000", address="..."): pass

[ ] 在调用函数的时候,尽量给实际参数提供默认参数名。

def student(name, sex, age, classroom="101", tel="88880000", address="..."): pass student('jack','male',17) # 其它全部使用默认值 student('tom','male',18,'102','666666','beijing') # 全部指定默认参数的值 student('mary','female',18,'102',tel='666666') # 挑着来 student('mary','female',18,tel='666666','beijing') # 这是错误的参数传递方式 student("mary","female",18,tel="666666",address="beijing")

注意最后两种调用方式,倒数第二种是错误的,而最后一种是正确的。为什么会这样?因为一切没有提供参数名的实际参数,都会当做位置参数按顺序从参数列表的左边开头往右匹配!

[ ] 使用参数名传递参数

通常我们在调用函数时,位置参数都是按顺序先后传入,而且必须在默认参数前面。但如果在位置参数传递时,给实参指定位置参数的参数名,那么位置参数也可以不按顺序调用,例如:

def student(name, age, classroom, tel, address="..."): pass student(classroom=101, name="Jack", tel=66666666, age=20)

注意指定的参数名必须和位置参数的名字一样。

[ ] 默认参数尽量指向不变的对象!

下面是一道 Python 面试真题:

def func(a=[]): a.append("A") return a print(func()) print(func()) print(func())

不要上机测试,仅凭代码,你能说出打印的结果吗?

因为 Python 函数体在被读入内存的时候,默认参数 a 指向的空列表对象就会被创建,并放在内存里了。因为默认参数 a 本身也是一个变量,保存了指向对象[]的地址。每次调用该函数,往 a 指向的列表里添加一个 A。a 没有变,始终保存的是指向列表的地址,变的是列表内的数据!我们可以测试一下:

def func(a=[]): print("函数内部 a 的地址为:%s" % id(a)) a.append("A") return a b = func() print('此时 b 的值为:%s' % b) print("函数外部 b 的地址为:%s" % id(b)) print("-------------") c = func() print('此时 c 的值为:%s' % c) print("函数外部 c 的地址为:%s" % id(c)) print("-------------") d = func() print('此时 d 的值为:%s' % d) print("函数外部 d 的地址为:%s" % id(d))

打印结果是:

函数内部 a 的地址为:39287880 此时 b 的值为:['A'] 函数外部 b 的地址为:39287880 ------------- 函数内部 a 的地址为:39287880 此时 c 的值为:['A', 'A'] 函数外部 c 的地址为:39287880 ------------- 函数内部 a 的地址为:39287880 此时 d 的值为:['A', 'A', 'A'] 函数外部 d 的地址为:39287880

那么如何避免这个问题呢?

使用不可变的数据类型作为默认值!

def func(a=None): # 注意下面的 if 语句 if a is None: a = [] a.append("A") return a print(func()) print(func()) print(func())

将默认参数 a 设置为一个类似 None,数字或字符串之类的不可变对象。在函数内部,将它转换为可变的类型,比如空列表。这样一来,不管调用多少次,运行结果都是[‘A’]了。

3.动态参数

顾名思义,动态参数就是传入的参数的个数是动态的,可以是 1 个、2 个到任意个,还可以是 0 个。在不需要的时候,你完全可以忽略动态函数,不用给它传递任何值。

Python 的动态参数有两种,分别是args 和**kwargs,这里面的关键是一个和两个星号的区别,而不是 args 和 kwargs 在名字上的区别,实际上你可以使用any 或whatever 的方式。但就如 self 一样,默认大家都使用*args 和kwargs。

注意:动态参数,必须放在所有的位置参数和默认参数后面!

def func(name, age, sex='male', *args, **kwargs): pass

*args

一个星号表示接收任意个参数。调用时,会将实际参数打包成一个元组传入形式参数。

如果参数是个列表,会将整个列表当做一个参数传入。例如:

def func(*args): for arg in args: print(arg) func('a', 'b', 'c') li = [1, 2, 3] func(li)

运行结果是:

a b c [1, 2, 3]

通过循环 args,我们可以获得传递的每个参数。但是 li 这个列表,我们本意是让它内部的 1,2,3 分别当做参数传递进去,但实际情况是列表本身被当做一个整体给传递进去了。怎么办呢?使用一个星号!调用函数,传递实参时,在列表前面添加一个星号就可以达到目的了。实际情况是,不光列表,任何序列类型数据对象,比如字符串、元组都可以通过这种方式将内部元素逐一作为参数,传递给函数。而字典,则会将所有的 key 逐一传递进去。

def func(*args): for arg in args: print(arg) li = [1, 2, 3] func(*li)

**kwargs

两个星表示接受键值对的动态参数,数量任意。调用的时候会将实际参数打包成字典。例如:

def func(**kwargs): for kwg in kwargs: print(kwg, kwargs[kwg]) print(type(kwg)) func(k1='v1', k2=[0, 1, 2])

运行结果是:

k1 v1 k2 [0, 1, 2]

而如果我们这样传递一个字典 dic 呢?我们希望字典内的键值对能够像上面一样被逐一传入。

def func(**kwargs): for kwg in kwargs: print(kwg, kwargs[kwg]) dic = { 'k1': 'v1', 'k2': 'v2' } func(dic)

实际结果却是弹出错误,为什么?

Traceback (most recent call last): File "F:/Python/pycharm/201705/func.py", line 10, in func(dic) TypeError: func() takes 0 positional arguments but 1 was given

因为这时候,我们其实是把 dic 当做一个位置参数传递给了 func 函数。而 func 函数并不接收任何位置函数。那怎么办呢?使用两个星号!

def func(**kwargs): for kwg in kwargs: print(kwg, kwargs[kwg]) dic = { 'k1': 'v1', 'k2': 'v2' } func(**dic)

有了前面一个星号的基础,这里我们应该很好理解了。两个星号能将字典内部的键值对逐一传入**kwargs。

“万能”参数

当*args 和kwargs 组合起来使用,理论上能接受任何形式和任意数量的参数,在很多代码中我们都能见到这种定义方式。需要注意的是,*args 必须出现在kwargs之前。

def func(*args, **kwargs): for arg in args: print(arg) for kwg in kwargs: print(kwg, kwargs[kwg]) lis = [1, 2, 3] dic = { 'k1': 'v1', 'k2': 'v2' } func(*lis, **dic)

现在我们结合一下普通参数和万能参数,看看会有什么情况发生:

def func(a, b, c=1, *args, **kwargs): for arg in args: print(arg) for kwg in kwargs: print(kwg, kwargs[kwg]) lis = ['aaa', 'bbb', 'ccc'] dic = { 'k1': 'v1', 'k2': 'v2' } func(1, 2, *lis, **dic)

打印结果是:

bbb ccc k1 v1 k2 v2

【愚公系列】2021年12月 Python教学课程 12-Python函数

列表 lis 中的第一个元素‘aaa’怎么没有打印出来?

我们改一下代码,打印一下参数 c 的结果就知道了:

def func(a, b, c=1, *args, **kwargs): print('c 的值是:', c) for arg in args: print(arg) for kwg in kwargs: print(kwg, kwargs[kwg]) lis = ['aaa', 'bbb', 'ccc'] dic = { 'k1': 'v1', 'k2': 'v2' } func(1, 2, *lis, **dic)

打印结果为:

c 的值是: aaa bbb ccc k1 v1 k2 v2

原来,lis 的第一个元素被传递给参数 c 了!这就是 Python 的参数传递规则之一。

为了避免出现这种情况,默认参数在传递实参时尽量指定写上形参的名字。

Python 的函数参数种类多样、形态多变,既可以实现简单的调用,又可以传入非常复杂的参数。需要我们多下功夫,多写实际代码,多做测试,逐步理清并熟练地使用参数。

5G教育 Python

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:三代开源社区的协作模式
下一篇:【活动结束】【内容共创系列】感恩有你,一路同行,参与签约作者征集令,赢高达500元礼包!
相关文章