Python 函数

函数相关思维导图

https://www.processon.com/mindmap/5e327935e4b096de64c9bc3f

函数简介

一、什么是函数

函数是对代码块或功能的封装和定义。

函数必须先定义,再使用。可以形象的将函数理解为一个工具,我们必须先把要用的工具找齐准备好,这样等到我们要用工具的时候就可以直接拿来用。

在函数定义阶段中,只检测语法,不执行代码,即语法错误在函数定义阶段就会检测出来,而代码的逻辑错误只有在执行时才会知道。

二、为什么要使用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 制造一辆汽车
print("制造车壳")
print("制造轮胎")
print("安装发动机")
print("安装玻璃")
print("喷上车漆")

# 制造第二辆车
print("制造车壳")
print("制造轮胎")
print("安装发动机")
print("安装玻璃")
print("喷上车漆")

......

在上述制造汽车的例子中,我们的代码都是从上而下顺序执行的。当需要制作几辆汽车时,就需要把整个流程重复执行几次;并且当我们需要修改某一步骤时,又要把所有相关的代码部分统统修改。因此,不使用函数去管理代码,便会存在以下的问题:

  • 代码的组织结构不够清晰,可读性差;
  • 遇到重复的功能只能重复编写实现代码,代码冗余;
  • 功能需要扩展时,需要找出所有实现该功能的地方修改之,无法统一管理且维护难度极大。

三、函数的分类和定义

  • 内置函数:为了方便我们的开发,针对一些简单的功能,python 解释器已经为我们定义好了的函数即内置函数。对于内置函数,我们可以拿来就用而无需事先定义,如 len(),sum(),max()
  • 自定义函数:内置函数所能提供的功能是有限的,我们自己需要根据需求,事先定制好我们自己的函数来实现某种功能。
1
2
3
4
5
6
7
8
9
# 函数定义语法
def 函数名(参数1,参数2,参数3,...):
'''注释'''
函数体
return 返回的值

# python3中新添加了"类型注解"特性,因此函数的定义方式还可以写成
def my_min(a:int, b:int)->int: # 规定参数的数据类型,"->"表示返回值的类型
print(a if a <= b else b)

注:函数并不一定都要指定返回值。函数如果没有显示指定返回值(即函数定义中没有写 return 或者 return语句后没有值),默认返回 None。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 函数具体的三种定义方式
# 1.定义无参函数,应用场景仅仅只是执行一些操作
def tell_msg():
print('hello world')

# 2.定义有参函数,需要根据外部传进来的参数,才能执行相应的逻辑
def tell_tag(tag,n): #有参数
print(tag*n)

# 3.定义空函数,设计代码结构
def auth(user,password):
'''
auth function
:param user: 用户名
:param password: 密码
:return: 认证结果
'''
pass

调用函数

1
2
3
4
5
6
7
8
9
10
# 按照在程序中的出现形式和位置,函数的调用大体可以分为三种
# 1.语句形式调用
func()

# 2.表达式形式
m = func() # 将func函数执行的结果传给变量m
n = 10*func() # 将func函数执行的结果乘以10后,传给变量n

# 3.函数调用作为参数的形式
func2(a, func()) # 将func函数执行的结果作为一个参数传给func2函数

函数的参数

一、实参和形参

  • 形参(形式参数)是在定义函数时,括号内声明的参数。形参的本质是一个变量名,用于接收外部传进来的值;
  • 实参(实际参数)是在调用函数时,括号内传入的值,实参可以是变量、常量、表达式或者三者的组合
  • 在调用有参函数时,实参(值)会赋值给形参(变量名)。在python中,变量名和值只是单纯的绑定关系,而对于函数来说,这种关系只在调用时生效,在调用后就解除。

二、形参的具体使用

2.1 位置参数

  • 位置参数,即按顺序定义的参数
  • 在定义函数时,按照从左往右定义的形式参数称为位置形参,凡是按照这种形式定义的形参都必须被传值
  • 在调用函数时,按照从左往右传入的实际参数称为位置实参,凡是按照这种形式定义的实参会跟形参一一对应
1
2
3
4
5
def register(name,age,sex):   #  定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))

register() # TypeError:缺少3个位置参数
register('lili',18,'male') # 对应关系为:name=’lili’,age=18,sex=’male’

2.2 关键字参数

  • 调用函数时,实参也可以是键值对方式传入,称为关键字参数,可以不按照从左往右的顺序传入,但是也能为对应的形参赋值
  • 当实参是位置参数和关键字参数混合使用时,要保证关键字参数在位置参数后面,且不能对一个形参重复赋值
1
2
3
4
5
6
7
def register(name,age,sex):   #  定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))

register(sex='male',name='lili',age=18) # 正确使用
register('lili',sex='male',age=18) # 正确使用
register(name='lili',18,sex='male') # SyntaxError:关键字参数name='lili'在位置参数18之前
register('lili',sex='male',age=18,name='jack') # TypeError:形参name被重复赋值

2.3 默认参数

  • 在定义函数时就已经为形参赋值,这类形参称为默认参数
  • 在函数调用时,若不给默认参数重新赋值,则默认参数就使用默认的值,否则使用新的赋值
  • 默认参数必须在位置参数之后
  • 默认参数的值通过应该设置为不可变类型
1
2
3
4
5
def register(name,age,sex="male"):   #  定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))

register(name='lili',age=18) # 不传sex的值,结果为 Name:lili Age:18 Sex:male
register(name='lili',age=18, sex="female") # 重传sex的值,结果为 Name:lili Age:18 Sex:female
  • 可变长位置参数

    当调用函数时,传入的位置实参个数多于位置形参个数时,就会报溢出错误。可变长位置参数就是为了解决此类问题。如果在最后一个形参前加*号,溢出的位置参数都会被该形参接收,并以元组形式保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def foo(x,y,z=1,*args): #  在最后一个形参名args前加*号
print(x)
print(y)
print(z)
print(args)

foo(1,2,3,4,5,6,7) # 实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,即args=(4, 5, 6,7)

"""
运行结果
1
2
3
(4, 5, 6, 7)
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 定义的列表实参,也可以传值给*args
def foo(x,y,*args):
print(x)
print(y)
print(args)

L=[3,4,5]
foo(1,2,*L) # *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)

"""
运行结果
1
2
(3, 4, 5)
"""

# 如果传入列表的时候没有加*,相当于就是一个普通位置参数
foo(1,2,L) # 仅多出一个位置实参L

"""
1
2
([1, 2, 3],)
"""

# 如果形参为常规的参数(位置参数或者默认参数),实参仍可以是*的形式
def foo(x,y,z=3):
print(x)
print(y)
print(z)

foo(*[1,2]) # 等同于foo(1,2)

"""
1
2
3
"""
1
2
3
4
5
6
7
8
# 多个值求和示例
def add(*args):
res=0
for i in args:
res+=i
return res

add(1,2,3,4,5) # 15
  • 可边长关键字参数

    如果在最后一个形参前加 ** 号,所有溢出的关键字参数都会被该形参接收,并以字典的形式保存

1
2
3
4
5
6
7
8
9
10
def foo(x,**kwargs): # 在最后一个参数kwargs前加**
print(x)
print(kwargs)

foo(y=2,x=1,z=3) # 溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs

"""
1
{'z': 3, 'y': 2}
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 定义的字典实参,也可以传值给**kwargs
def foo(x,y,**kwargs):
print(x)
print(y)
print(kwargs)

dic={'a':1,'b':2}
foo(1,2,**dic) # **dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)

"""
1
2
{'a': 1, 'b': 2}
"""

# 如果传入字典的时候没有加**,相当于就是一个普通位置参数
foo(1,2,dic) # TypeError:函数foo只需要2个位置参数,但是传了3个

# 如果形参为常规的参数(位置参数或者默认参数),实参仍可以是**的形式
def foo(x,y,z=3):
print(x)
print(y)
print(z)

foo(**{'x':1,'y':2}) # 等同于foo(y=2,x=1)

"""
1
2
3
"""
  • 命名关键字参数

    当函数调用者将关键字参数放入 **kwargs 中进行传参,我们又想知道我们需要的关键字参数是否被包含在了可变长关键字参数内,我们还需要对接收的值进一步判断,增加了代码的复杂度

1
2
3
4
5
6
7
def register(name,age,**kwargs):
if 'sex' in kwargs:
#有sex参数
pass
if 'height' in kwargs:
#有height参数
pass

因此,为限定函数调用者必须对必要的关键字参数进行传值,python3 提供了命名关键字参数。

在定义形参时,用 * 作为一个分割符号,* 之后的形参称为命名关键字参数。对于这类参数,进行函数调用的时候,必须以key=value的形式进行传值,且必须被传值

1
2
3
4
5
6
def register(name,age,*,sex,height): #sex,height为命名关键字参数
pass

register('lili',18,sex='male',height='1.8m') # 正确使用
register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
register('lili',18,height='1.8m') # TypeError没有为命名关键字参数sex传值。

命名关键字参数也可以有默认值

1
2
3
4
def register(name,age,*,sex='male',height):
print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))

register('lili',18,height='1.8m') # Name:lili,Age:18,Sex:male,Height:1.8m

注意:sex不是默认参数,height也不是位置参数,两者都是命名关键字参数,”male”只是sex的默认值,因此即便将 sex放置在 height 前面也不会有问题(按照正常的逻辑,位置参数必须在默认参数前面,但是sex和height都是命名关键字参数,因此不需要遵守这个规则)

如果形参中已经有 *args 了,命名关键字参数就不需要一个单独的 * 来作为分隔符了

1
2
3
4
def register(name,age,*args,sex='male',height):
print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))

register('lili',18,1,2,3,height='1.8m') # sex与height仍为命名关键字参数
  • 组合使用

    定义时各类形参的顺序:位置参数、默认参数、*args、命名关键字参数、**kwargs

命名空间和作用域

一、命名空间

​ 在 python 解释器开始执行之后,就会在内存中开辟一个空间,每当遇到一个变量的时候,就把变量名和值之间的关系记录下来;当遇到函数定义的时候,解释器只是把函数名读入内存,表示这个函数存在了,至于函数内部的变量和逻辑,解释器是不关心的。即一开始的时候函数只是加载进来,只有当函数被调⽤和访问的时候,解释器才会根据函数内部声明的变量来进行开辟变量的内部空间。随着函数执行完毕,这些函数内部变量占用的空间也会随着函数执行完毕而被清空。

存放名字和值对应关系的空间叫命名空间,在python中,一共有三种命名空间:

  • 内置命名空间:存放python解释器为我们提供的名字,如 print,list,tuple,str 等
  • 全局命名空间:函数外部声明的变量和最外层的函数均属于全局命名空间
  • 局部命名空间:函数内部定义的变量和内部定义的函数均属于局部命名空间,局部命名空间之间相互独立
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = 10  # 全局命名空间

def func(): # 全局命名空间
b = 20 # 局部命名空间
print("哈哈哈")
print(b)

def func2(): # 局部命名空间
print("呵呵")

print(a)
func()

####################################################

def func2():
print(a)
print(b) # 报错,变量b未声明,因为b属于func的局部名称空间,因此在func2中无法使用

命名空间加载顺序:内置命名空间 –> 全局命名空间 –> 局部命名空间

上述的例子中,当python解释器开始运行时,先会加载 print 等python解释器内置的命名空间,接着再从上到下加载全局名称空间(变量a和函数名 func),当执行函数时,最后再为函数内部的变量和嵌套的函数开辟命名空间,即最后加载局部命名空间

命名空间的取值顺序:局部命名空间 –> 全局命名空间 –> 内置命名空间(就近原则)

1
2
3
4
5
6
7
8
a = 10

def func():
a = 20
print(a) # 输出20

func()
print(a) # 输出10

上述例子中,函数内部的 print(a) 会先在当前的局部命名空间中查找a,如果查找不到就会往全局命名空间中查找;对于 print 函数,现在 func 函数的局部命名空间查找,查找不到就往全局命名空间查找,还是查找不到函数的定义和声明,就会往 python 解释器的内置命名空间进行查找。

二、作用域

**作用域:**作用域就是作用范围,按照生效范围来看分为全局作用域和局部作用域

  • 全局作用域:包含内置命名空间和全局命名空间,在整个文件的任何位置都可以使用(遵循从上到下逐行执行)
  • 局部作用域:在函数内部可以使用

作用域命名空间

  • 全局作用域:全局命名空间 + 内置命名空间
  • 局部作用域:局部命名空间

我们可以通过 globals() 函数来查看全局作用域中的内容, 也可以通过 locals() 来查看局部作用域中的变量和函数信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = 10 
def func():
a = 40
b = 20

def abc():
print("哈哈")
print(a, b) # 这里使用的是局部作用域

print(globals()) # 打印全局作⽤用域中的内容
print(locals()) # 打印当前局部作⽤用域中的内容,即打印func中的局部命名空间

func()

# 输出的结果是一样的,因为locals()是查看当前的局部命名空间,而此时locals函数的位置就处在全局中,因此查看的也是全局
print(globals())
print(locals())

关键字global和nonlocal

一、global

global 表示不再使用局部作用域中的内容, 而改用全局作用域中的变量。当在局部作用域中改变了该变量的值,则全局中该变量的值也会发生改变。

1
2
3
4
5
6
7
8
9
10
a = 100 
def func():
global a # 加了个global表示不再局部创建这个变量了,而是直接使用全局的a
print(a) # 100
a = 28 # 此时将a重新赋值28,由于引用的是全局变量,因此全局中的a也会发生改变
print(a) # 28

print(a) # 100 此时还未执行函数,a的值还未被修改
func()
print(a) # 28

二、nonlocal

nonlocal 表示在 局部作用域 中,调用命名空间中最近的变量(即若父级函数中查找不到,继续往父级的父级查找)。如果在子函数中修改了该变量,则原来作用域中的该变量的值也会发生响应的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
###########  报错,a是属于全局作用域的  ##########
a = 10
def func1():
def func2():
nonlocal a
a = 30
print(a)
func2()
print(a)

func1()


def func():
a = 10
def func2():
nonlocal a
print(a) # 10
a = 20
print(a) # 20
func2()
print(a) # 20



a = 1
def fun_1():
a = 2
def fun_2():
a = 3
def fun_3():
nonlocal a
a = 4
print(a) # 4
print(a) # 3
fun_3()
print(a) # 4
print(a) # 2
fun_2()
print(a) # 2

print(a) # 1
fun_1()
print(a) # 1

# 执行结果:1,2,3,4,4,2,1

匿名函数

匿名函数是为了解决一些简单的需求而设计的一句话函数,用lambda关键字来声明,不需要用def来声明。

语法: 函数名 = lambda 参数: 返回值

1
2
3
4
5
6
7
8
# 计算n的n次方 
def func(n):
return n**n
print(func(10))

# 匿名函数的写法
f = lambda n: n**n
print(f(10))

注意:

  • 函数的参数可以有多个,多个参数之间⽤逗号隔开
  • 匿名函数不管多复杂,只能写⼀行, 且逻辑结束后直接返回数据
  • 返回值和正常的函数一样,可以是任意数据类型
  • 匿名函数并不是说⼀定没有名字,这里前⾯的变量就是⼀个函数名。说他是匿名原因是通过__name__查看的时候,函数是没有名字的,统⼀都叫 lambda。在调用的时候没有什么特别之处,像正常的函数调用即可。

递归

  • 递归函数就是函数在内部调用自己,必须的有递归的出口,否则会就是死循环
  • 在python中递归的最大深度是998层
  • 递归主要用于遍历树形结构
1
2
3
4
5
def func():
print("哈哈哈")
func()

func()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def func(count):
print("哈哈哈" + str(count))
func(count + 1)

func(1)


# 可以调整递归深度,但是不一定能跑到指定的深度
import sys
sys.setrecursionlimit(10000)
def func(count):
print("哈哈哈" + str(count))
func(count + 1)

func(1)

为什么要限制递归的深度?我们来观察以下两段代码:

1
2
3
4
5
6
7
8
9
10
11
12
# 死循环
while True:
a = 10
print(a)

# 递归死循环
def func():
a = 10
print(a)
func()

func()

while 死循环重复使用的是同一个变量,因此不会对内存造成影响;而递归由于每次都是调用一个函数,每次都会为新函数中的变量开辟新的内存,如果不限制递归的深度,很容易造成内存的崩溃。且如果一个功能使得递归的深度过大,其实也表示该功能不太适合用递归实现。

1
2
3
4
5
6
7
8
9
10
11
# 使用递归遍历文件夹
import os
file_path = "F:/学习代码/python"
def traverse_dir(file_path):
for dir in os.listdir(file_path):
if os.path.isdir(os.path.join(file_path, dir)):
traverse_dir(os.path.join(file_path, dir)) # 递归入口
else:
print(os.path.join(file_path, dir)) # 递归出口

traverse_dir(file_path)

Python 函数
https://clark-cdc.github.io/2019/04/06/0006-函数/
作者
clark
发布于
2019年4月6日
许可协议