Python的函数

函数及特性

函数是指将一组语句的集合通过一个名字(函数名)封装起来,要想执行这个函数,只需调用其函数名即可。

函数的特性:

  • 减少重复代码,提高应用的模块性和代码的重复利用率,用来实现单一,或相关联功能。

  • 可扩展性:方便修改,更容易扩展代码的功能。

  • 保持代码的一致性,修改函数功能之后所有调用函数的地方都会随之修改。

函数的定义和调用

在 Python 中定义函数使用 def 关键字,一般格式如下:

1
2
3
4
5
6
7
def 函数名(参数列表):
函数体

def hello():
print('hello')

hello()#调用

函数名的命名规则,与变量名类似:

  • 函数名区分大小写。

  • 函数名只能是 字母、数字 或 下划线 的任意组合,不能使用任何的标点符号。

  • 函数名不能以数字开头。

  • 函数名不能与 Python 的关键字同名。

函数的参数

任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。

形参和实参

形参指的是形式参数,不是实际存在,是虚拟变量。在定义函数和函数体的时候使用形参,目的是在函数调用时接收实参(实参个数,类型应与实参一一对应)。

实参是指实际参数,调用函数时传给函数的参数,可以是常量,变量,表达式,函数,传给形参。

形参和实参的区别是,形参是虚拟的,不占用内存空间,形参变量只有在被调用时才分配内存单元,实参是一个变量,占用内存空间,数据传送单向,实参传给形参,不能形参传给实参。

1
2
3
4
5
6
7
8
9
10
import time

times = time.strftime('%Y-%m-%d')


def f(time_str):
print('Now time is : %s' % time_str)


f(times)

传入函数的参数

位置参数

位置参数也叫必备参数,是必须要传给函数的参数,并且在传参时必须以正确的顺序传入函数,调用时的数量必须和函数声明时的一样。

1
2
3
4
5
6
7
8
9
10
11
def f(name, age):
print('I am %s,I am %d years old.' % (name, age))


f('Tom', 18)
f('Jerry', 16)

'''
I am Tom,I am 18 years old.
I am Jerry,I am 16 years old.
'''

关键字参数

关键字参数和函数调用关系紧密,函数调用使用关键字参数来确定传入的参数值。使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。

1
2
3
4
5
6
7
8
9
10
def f(name, age):
print('I am %s, I am %d years old.' % (name, age))


# f(16,'Tom') #报错
f(age=16, name='Tom')

'''
I am Tom, I am 16 years old.
'''

定义函数时的参数

默认参数

默认参数也叫缺省参数,同时也是关键字参数,调用函数时缺省参数的值如果没有传入,则被认为是默认值。默认参数常用于多数情况下对象的值是固定的情况,例如一些女子学校中,学生的性别是固定的。

1
2
3
4
5
6
7
8
9
def echo_info(name, age, sex='male'):
print('-'.center(15, '-'))
print('Name:%s' % name)
print('Age:%d' % age)
print('Sex:%s' % sex)


echo_info('Tom', 18)
echo_info('Jerry', 40, 'female')

默认参数的陷阱: 在定义函数时,如果默认参数的值是可变数据类型,那么每一次调用函数的时候如果不传入值,就会公用这个数据类型的资源,即每次都操作的是同一个对象。例如:

1
2
3
4
5
6
7
8
def test(names=[]):
names.append('Tom')
print(names)

test()
test()
test()
test()

输出结果:

1
2
3
4
['Tom']
['Tom', 'Tom']
['Tom', 'Tom', 'Tom']
['Tom', 'Tom', 'Tom', 'Tom']

再比如:

1
2
3
4
5
6
7
8
def atest(k, data={}):
data[k] = 'v'
print(data)


atest(1)
atest(2)
atest(3)

输出结果:

1
2
3
{1: 'v'}
{1: 'v', 2: 'v'}
{1: 'v', 2: 'v', 3: 'v'}

不定长参数

有时候可能需要一个函数能处理比当初声明时更多的参数,这些参数就叫不定长参数,也叫动态参数。动态参数和位置参数、关键字参数不同,声明时不会命名。例如下面的加法器,能且仅能接收三个参数,并求出和:

1
2
3
4
5
def add(x, y, z):
print(x + y + z)


add(1, 2, 3)

如果在变量名前面加了星号 * ,则变量会以元组的形式存放所有 未命名 的变量参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add(*args):
print('args =', args)
the_sum = 0
for i in args:
the_sum += i
print('the_sum =', the_sum)


add(1, 2, 3, 4, 6, 9, 10)

'''
args = (1, 2, 3, 4, 6, 9, 10)
the_sum = 35
'''

下面的打印基本信息的函数,最多只能接收三个参数,如果再想添加其他参数就需要重新定义函数的参数:

1
2
3
4
5
6
7
def echo_info(name, age, sex='male'):
print('Name : %s' % name)
print('Age : %d' % age)
print('Sex : %s' % sex)


echo_info('Tom', 18)

而如果在变量名前面加了 ** , 则变量会以字典的形式存放 有命名 的变量参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def echo_info(*args, **kwargs):
print('args = ', args)
print('kwargs = ', kwargs)
for i in kwargs:
print('%s : %s' % (i, kwargs[i]))


echo_info('Tom', 18, 'male', Job='IT', Height='188')

'''
args = ('Tom', 18, 'male')
kwargs = {'Job': 'IT', 'Height': '188'}
Job : IT
Height : 188
'''

参数的位置关系

定义函数时应该 *args 放左边,**kwargs 放右边,下面是错误的写法:

1
def print_info(name, **kwargs, *args): # 报错

按照规范,定义函数时默认参数应该放在位置参数之后。

1
2
3
4
5
def test(name, age, *args, sex='fmale', **kwargs):
def test(*args, sex='fmale'):
# 不规范的写法:
# def test(name, age, sex='fmale', *args, **kwargs):
# def test(sex='fmale', *args):

调用函数进行传参时,关键字参数应该放在位置(必备)参数之后。

1
2
3
echo_info('Tom', 18, hobby='girl', nationality='Chinese', ability='Python')
# echo_info(hobby='girl', 'alex', 18, nationality='Chinese', ability='Python') # 报错
# echo_info('Jerry', hobby='girl', 18, nationality='Chinese', ability='Python') # 报错

其他传参方式

函数在接收动态参数时,会把接收的参数聚合成一个元组,例如:

1
2
3
4
5
6
7
8
9
10
11
def func(*args):
print('args =', args)


func(1, 2, 3)
func(['a', 'b', 'c'])

'''
args = (1, 2, 3)
args = (['a', 'b', 'c'],)
'''

而在函数中使用 *args 的方式调用参数时,会把聚合的元组打散,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def func(*args):
print('args =', args)
print('*args =', *args)


func(1, 2, 3)

'''
args = (1, 2, 3)
*args = 1 2 3
'''

func(['a', 'b', 'c'])

'''
args = (['a', 'b', 'c'],)
*args = ['a', 'b', 'c']
'''

这种方式其实又还原成了最原始传入参数的方式,这种情况最常用的情形是在装饰器内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def wrapper(funcname):
def inner(*args, **kwargs): # 函数接收参数,聚合成一个元组
retval = funcname(*args, **kwargs) # 调用参数,打散元组,还原初始传入形式
print('Something')
return retval

return inner


@wrapper
def func(s):
print(s)


func([1, 2, 3])

如果传给函数的参数是个列表或元组,还可以使用 * 将其打散之后传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func(*args):
print('args =', args)


func([1, 2, 3])

func(*[1, 2, 3]) # 将列表打散: *[1, 2, 3] < == > 1, 2, 3
func(1, 2, 3)

'''
args = ([1, 2, 3],)
args = (1, 2, 3)
args = (1, 2, 3)
'''

如果函数接收的是 **kwargs ,那么单纯地传入字典会被识别为位置参数,必须打散才能被接收。例如:

1
2
3
4
5
6
7
8
9
10
11
def func(**kargs):
print('kwargs =', kargs)


# func({'Name': 'Jerry'}) # 错误传参

func(**{'name': 'Tom'})

'''
kwargs = {'name': 'Tom'}
'''

函数的返回值

要想获取函数的执行结果,就可以用 return 语句把结果返回。

注意:

  • 函数在执行过程中只要遇到 return 语句,就会停止执行并返回结果,也可以理解为 return 语句代表着函数的结束。

  • 如果未在函数中指定 return,那这个函数的返回值为 None

  • return 可以接收多个对象,此时 Python 会将多个对象封装成一个元组,并将这个元组对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


def get_os_release(fpath='/etc/redhat-release'):
with open(fpath, 'rt', encoding='utf-8') as fr:
return fr.readline().strip()


x = get_os_release()
y = get_os_release('/etc/issue')

print(x)
print(y)

'''
CentOS Linux release 7.6.1810 (Core)
\S
'''

函数的注释

定义了函数后,应该对整个函数作注释而不是逐行注释。以下是建议的注释方式:

1
2
3
4
5
6
7
def func(name,sex):
'''
函数的作用
参数1:数据类型,用途
参数2:数据类型,用途
'''
pass

命名空间

从 python 解释器开始执行代码之后,就在内存中开辟了一个空间,每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来。

但是当遇到函数定义的时候解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心。

等执行到函数调用的时候,Python 解释器会再开辟一块内存来存储这个函数里的内容,这个时候才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中。函数中的变量只能在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。

命名空间是指存放名字与值的绑定关系的空间,分为三种:

  • 代码在运行一开始创建的存储“变量名与值的关系”的空间叫做全局命名空间

  • 在函数的运行中开辟的临时的空间叫做局部命名空间

  • 内置命名空间中存放了python解释器为我们提供的名字:input,print,str,list,tuple…。

这三种命名空间之间的加载顺序为:

内置命名空间(程序运行前加载) => 全局命名空间(程序运行中:从上到下加载) => 局部命名空间(程序运行中:调用时才加载)

取值顺序为:

  • 在局部调用:局部命名空间 => 全局命名空间 => 内置命名空间

  • 在全局调用:全局命名空间 => 内置命名空间

作用域

作用域就是作用范围,按照生效范围可以分为两类:

  • 全局作用域:包含内置命名空间、全局命名空间,在整个文件的任意位置都能被引用、全局有效 。

  • 局部作用域:局部命名空间,只能在局部范围生效。

使用 globals()locals() 方法则会以字典类型返回当前位置的全局变量和局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 全局调用
print(globals())
print(locals())


def func():
a = 12
b = 20
# 局部调用
print(locals())
print(globals())


func()

作用域介绍

python中的作用域分4种情况:

  • L:local,局部作用域,即函数中定义的变量;

  • E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的;

  • G:global,全局变量,就是模块级别定义的变量;

  • B:built-in,系统固定模块里面的变量,比如 intbytearray 等。 搜索变量的优先级顺序依次是:局部作用域 => 外层作用域 => 当前模块中的全局 => Python 内置作用域,也就是 LEGB 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
x = int(2.9)  # int built-in

g_count = 0 # global


def outer():
o_count = 1 # enclosing

def inner():
i_count = 2 # local
print(o_count)

# print(i_count) 找不到
inner()


outer()

# print(o_count) #找不到

并且,localenclosing 是相对的,enclosing 变量相对上层来说也是 local

作用域产生

在 Python 中只有模块(module),类(class)以及函数(deflambda)才会引入新的作用域,其它的代码块(如 iftryfor 等)是不会引入新的作用域的,如下代码中 if 并没有引入一个新的作用域,x 仍处在当前作用域中,后面代码可以使用:

1
2
3
if 2>1:
x = 1
print(x) # 1

但是函数 defclasslambda 会引入新的作用域:

1
2
3
def test():
x = 2
print(x) # NameError: name 'x' is not defined

变量的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
x = 6


def f2():
print(x)
x = 5


f2()

'''
UnboundLocalError: local variable 'x' referenced before assignment
'''

上述代码出现错误的原因在于 print(x) 时,解释器会先在局部作用区域找到变量 x 。这是因为在进行函数调用( f2() )之前,其前面的代码包括函数都会先加载到内存,因此执行到 print(x) 会先在局部区域找到 x = 5 ,但 x 的使用出现在 x 的声明前了,所以报错。

1
2
3
4
5
6
7
8
9
10
11
# x=6
def f2():
print(x)
# x=5


f2()

'''
NameError: name 'x' is not defined
'''

上述代码出现错误的原因在于 print(x) 时,解释器会在局部作用域找,找不到 变量 x 的声明,然后去全局作用域找也找不到,最后在 Python 的内置区域找也找不到,因此会报变量未声明的错误。

1
2
3
4
5
6
7
8
9
10
11
12
x = 6


def f2():
x += 1


f2()

'''
UnboundLocalError: local variable 'x' referenced before assignment
'''

x += 1x = x + 1 是先加再赋值,解释器会在局部作用域找,会找到 x += 1 (函数已经加载到内存),但 x 的加法运算出现在赋值前了,所以报错。

关键字 global

当内部作用域想修改外部作用域的变量时,就要用到 globalnonlocal 关键字了,当修改的变量是在全局作用域(global 作用域)上的,就要使用 global 先声明一下。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
count = 10


def outer():
global count
print(count)
count = 100
print(count)


outer()
# 10
# 100

关键字 nonlocal

关键字 global 声明的变量必须在全局作用域上,不能应用在嵌套作用域上,当要修改嵌套作用域( enclosing 作用域,外层非全局作用域)中的变量怎么办呢,这时就需要 nonlocal 关键字了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def outer():
count = 10

def inner():
nonlocal count
count = 20
print(count)

inner()
print(count)


outer()
# 20
# 20

总结

  • 对于一个变量,内部作用域先声明就会覆盖外部变量,不在内部声明而直接使用,就会使用外部作用域的变量。
  • 内部作用域要修改外部作用域变量的值时,全局变量要使用 global 关键字,嵌套作用域变量要使用 nonlocal 关键字。nonlocal 是 Python3 新增的关键字,有了这个关键字就能完美的实现闭包了。
有钱任性,请我吃包辣条
0%