作者: Daniel Meng
GitHub: LibertyDream
博客:明月轩
所谓函数,就是能执行特定任务的一段被命了名的程序。不管什么编程语言都会有函数这一概念,因为其有很重要的功能:
如定义所说,一个函数一定具备解决某一特定场景下问题的能力。或者说一个函数就是一个小工具、一个套路
一个良好的函数无论多么复杂,都能很好的隐藏自身的细节,暴露在外面的只是返回值、接受参数和函数说明而已,具体的功能实现外面的人都不用了解。
函数的存在大大降低了重复编码带来的成本损耗,从而提升了效率。而经常用于解决某类问题的函数们通常会进一步封装成一个函数库供人们使用。
一个标准的函数定义形式如下:
def funcname(parameter_list):
pass
这里注意几点:
parameter_list
可以没有return
进行返回,如果没有返回值,python会返回一个None
函数可以嵌套使用,但为了良好的代码阅读体验和编程规范,不推荐超过两层的嵌套调用
func(func1(func2(func(...))))
下面简单实现两个函数以作示例。
def add_two_num(x, y):
result = x + y
return result
def print_code(code):
print(code)
result_add = add_two_num(1, 2)
result_print = print_code('Python')
print(result_add, result_print)
# 无限递归的错误示例
def print(code):
print(code)
print('Python')
这里注意几点:
add_two_num()
有且只能接受两个参数,多了少了都不行,而内置函数print()
通过,
分隔可以接受任意数量的参数python会监测递归动态,根据机器性能状况的差异,当发现一段代码自循环超过一定次数(995,998,1000等),就会报错。可以通过`sys.setrecursionlimit(recur_nums)指定递归深度。但python内部有额外机制,并不会允许特别大的深度
对于返回值,传统语言一般只允许返回一个值,也有通过out/ref
返回多值的方法。python想要返回多个值,方法很简单,通过,
分隔开即可,如下代码所示:
def damage(skill_one, skill_two):
damage_one = skill_one * 3
damage_two = skill_two + 2
return damage_one, damage_two
# 下标解包
damages = damage(4,8)
print(type(damages),damages[0],damages[1])
# 序列解包
skill_damage1,skill_damage2 = damage(4,8)
print(skill_damage1,skill_damage2)
多返回值的函数会将多个返回值封装成一个元组。注意样例代码中获取返回值的方式,第一种具有传统语言和新手风格,但并不赞成使用,当返回值众多,每个都用下标访问调用,日后维护代码时很难以理解。
第二种就是一种 pythonic 风格的解包方式了,通过明晰、有意义的变量接收返回值,易于理解且代码简介。这种方式叫做序列解包
更好的理解序列解包,先来了解下pythonic的赋值方式
# 传统方式
a = 1
b = 2
c = 3
# pythonic
a,b,c = 1,2,3
两种方式的效果等价,但后一种更显“优美”。而进一步的
d = 1,2,3
print(type(d))
a,b,c = d
print(a,b,c)
def add(x, y):
result = x + y
return result
对上述函数定义,指定了函数接收的两个参数x
和y
,定义里指定的参数也叫形式参数。当调用该函数时,如c = add(x_value,y_value)
,必须传入和函数定义中数量一致的参数,默认从左向右依次赋值。函数调用时传入的参数也叫实际参数,即这里的x_value
,y_value
。
这种函数定义多少就必须传入多少否则报错的参数类型就是必须参数
上面提到调用函数时传入参数默认是自左向右依次赋值,对函数add
来说就是先给x
赋值,再给y
赋值。如果想要改变赋值顺序,或者说自定义赋值顺序,可以这样:
c = add(y=3, x=2)
这样的参数形式就是关键字参数,通过参数名 = 参数赋值
的形式明确各个参数的传入值,可阅读性良好,也能改变传参顺序。
很多时候对于一个函数的调用,多数参数值是一样的,重复输入相同的内容开发效率很低。这时候就可以使用默认参数了,义同其名,当没有明确指定参数值时,解释器会将默认值赋值给参数
def print_student_files(name, gender, age, college):
print('我叫' + name)
print('我今年' + str(age) + '岁')
print('我是' + gender + '生')
print('我在' + college + '上学')
print_student_files('小旋风', '男', 18, '狮驼岭')
比如对如上函数定义,在狮驼岭
上学的学生在gender,age,college
一栏或几栏里内容都是一致的。这时就可以修改函数定义如下
def print_student_files(name, gender='男', age=18, college='狮驼岭'):
print('我叫' + name)
print('我今年' + str(age) + '岁')
print('我是' + gender + '生')
print('我在' + college + '上学')
这时如果个人数据和默认值无异,就可以只传入唯一的必须参数print_student_files('奔波灞')
。如果有数据不同,则将对应值传入即可print_student_files('蜘蛛精','女',16,'盘丝洞')
print_student_files('奔波灞')
print_student_files('蜘蛛精', '女', 16, '盘丝洞')
目前为止默认参数看起来很简单,但有这么一些坑必须小心:
当不显式的给参数赋值时,默认从左到右依次赋值,所以不能自以为是的跳跃赋值。比如对上面的函数,有一个学生糊涂虫
,它的数据只有年龄和默认值不同,但函数调用不能这么写print_student_files('糊涂虫',17)
,解释器会将实参17
赋值给gender
一栏。
print_student_files('糊涂虫', 17)
正确的方式是print_student_files('糊涂虫', age = 17)
print_student_files('糊涂虫', age=17)
函数定义时,默认参数和非默认参数不能穿插,下面的定义形式是非法的:
def print_student_files(name, gender='男', age=18, college='狮驼岭',teacher):
print('我叫' + name)
print('我今年' + str(age) + '岁')
print('我是' + gender + '生')
print('我在' + college + '上学')
如果要加入一个必须参数,其应该在默认参数之前
def print_student_files(name, num_id, gender='男', age=18, college='狮驼岭'):
print('我叫' + name)
print('我今年' + str(age) + '岁')
print('我是' + gender + '生')
print('我在' + college + '上学')
print('我的id是' + num_id)
和上一个坑类似,调用时默认的传参和关键字传参不能穿插,下面的调用是非法的:
print_student_files('糊涂虫', gender='男', 17, college='魔王寨')
在我们已接触的函数中,有一个很特殊的函数print()
,它可以接受任意多个参数。那么我们能否自己设计一个这样的函数,每次接收一组参数且数量不限,答案是肯定的。
一般的实现思路是通过元组()
和列表[]
进行传参,但Python内通过*param_name
形式定义的可变参数,可以自动的将一组参数平铺式的逐个传入函数中
def var_param(*param):
print(param)
print(type(param))
var_param(1,2,3,4,5)
Python自动将可变参数列表中给出的实参封装成一个元组,如果不喜欢这样的形式,可以自行将要传递的参数封装成一个元组,然后传给函数
def other_var_param(param):
print(param)
print(type(param))
other_var_param((1,2,3,4,5)) # 直接传入封装后的元组
如果已经定义了可变参数*param_name
,如果将元组传入,将会构成二维元组
var_param((1,2,3,4,5))
*param_name
中*
的作用类似于解包,所以如果自己想要传递的参数中有序列类型,不想构成二维元组,可以将其赋值给一个变量,然后将变量当做参数传递给函数,并在其前加上*
seris_var = (1,2,3,4,5)
var_param(*seris_var)
seris_var = [1,2,3,4,5]
var_param(*seris_var)
当一个函数的参数中既有可变参数,也有其他类型参数时,必须参数放在前面,其他类型参数顺序没有强制要求,但要注意与赋值方式匹配
def mixed_param(m_param, def_param=2, *var_param):
print(m_param)
print(def_param)
print(var_param) # 逐类打印参数
mixed_param('a', 1,2,3)
如上例所示,定了一个同时接收多种参数类型的函数mixed_param
,我们希望逐类打印参数,并跳过默认参数,只赋值必须采参数和可变参数,但Python还是默认依次赋值
def mixed_param(m_param, *var_param, def_param=2):
print(m_param)
print(var_param)
print(def_param) # 逐类打印参数
mixed_param('a', 1,2,3, 'param')
调换可变参数和默认参数的位置,还是逐类打印,希望var_param
接收1,2,3
,def_param
接收param
。但是因为var_name
是可以接收任意多个参数的,所以除了必须参数拿走的,其余参数都被Python分配给了var_param
,以至于def_name
只能使用默认值。
如果想让Python按照我们的意图赋值,必须显示的使用关键字参数给默认参数赋值
mixed_param('a', 1,2,3, def_param='param')
传参时是无法预知实际参数都会分配给什么类型的参数的,所以再次注意传参顺序
mixed_param('a', def_param='param', 1,2,3)
上述案例只是展示不同参数类型在一起的场合,编程实践中,强烈建议参数列表尽可能的简单,不要混合多种类型的参数
在实际应用中,可变参数通常通过for循环来遍历使用
def square_sum(*nums): # 求数列平方和
num_sum = 0
for num in nums:
num_sum += num * num
return num_sum
square_sum(1,2,3)
上面的传参都还只是序列类型的实参,那么当传入参数是任意多个关键字参数,或者干脆是字典类型会怎么样呢?
def city_temp(*citys): # 输出城市气温
pass
city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
可以看到,*param_name
形式的可变参数不能应付任意多个的关键字参数列表。这种情况下需要**param_name
形式
def city_temp(**citys): # 输出城市气温
print(citys)
print(type(citys))
city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
可以看到Python将关键字参数列表转换成了字典进行存储,当我们使用的时候需要遍历字典来使用。
字典的遍历方式很pythonic,类似于序列解包,大多是key,value in dict.items()
形式
def city_temp(**citys): # 输出城市气温
print('Weather Now')
for city, temp in citys.items():
print('%s: %s' % (city, temp))
city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
当字典作为可变参数的接收对象的时候,类似于序列,可以将字典赋值给一个变量,然后以**param
的方式使用
eg_citys={'Beijing':'36c', 'Shanghai':'32c', 'Wuhai':'27c'}
city_temp(**eg_citys)
可变参数既然可以接收任意多个参数,那么一个都不传自然也是能够接受的
eg_citys={}
city_temp(**eg_citys)
因此,就有了常见的参数接收形式(must_param, **var_param)
,可以只传递必须参数。
def func_show(func_name, **des):
print(func_name)
print(des)
func_show(print)
每一个变量都有自己能够起作用的范围,我们称之为作用域,作用域的概念所有编程语言内都有。通常在变量作用域之外访问不到该变量
c = 0
def add(x, y):
c = x + y
print(c)
add(1, 2)
print(c)
函数内定义的 c 外部无法访问,同名变量优先使用内部定义的变量。所以函数内的 c 打印值为3,外部 c 的打印值为0。
还有更加直观方式的展示作用域
def demo_func():
a = 10
print(a)
在一个模块内,.py
文件内定义的变量称之为全局变量,像函数内定义的变量称之为局部变量。全局变量覆盖整个模块,能被多个函数调用;局部变量只能作用在自己所处的局部
所谓局部是一个相对概念,比如对于函数内的语句来说,函数内定义的变量当然是可以访问的
def demo_func():
b = 0
for x in range(1,10):
b += 1
print(b)
demo_func()
注意,Python没有块级作用域的概念,即if elif else
,for while
这样的选择、循环语句不构成作用域,即期内定义的变量,在语句外部是可以访问的。有其他语言经验的人要格外注意
def demo_func():
for x in range(0, 9):
a = 'a'
print(a)
demo_func()
变量的作用域有链式特性,也叫做作用域链,即局部同名变量会屏蔽全局同名变量,嵌套层数多的作用域内的同名变量会屏蔽掉嵌套层数少的作用域内的变量,访问变量时从最近的作用域开始寻找变量
d = 0
def func1():
d = 1
def func2(): # 函数内定义函数是允许的
d = 2
print(d)
func2()
func1()
d = 0
def func1():
d = 1
def func2():
# d = 2
print(d) # 本作用域没有,向上一级寻找
func2()
func1()
d = 0
def func1():
# d = 1 # 本作用域没有,向上一级寻找
def func2():
# d = 2
print(d) # 本作用域没有,向上一级寻找
func2()
func1()
上面全局变量作用于整个模块内的函数,实际上全局变量在整个应用程序内都是可以被访问到的,即不同的包间也可以互相访问彼此定义的全局变量。比如说aaa.bbb
内定义的全局变量,在ccc.ddd
内,在导入aaa.bbb
模块后即可访问
此外 Python 还提供了global
关键字申明变量是全局变量,这时即使变量是在aaa.bbb
下某一函数内定义的,ccc.ddd
导入后依旧可以访问到,前提是该函数至少执行过一次以初始化变量
def demo_func():
global e
e = 2
demo_func() # 至少执行一次函数,否则 e 是不存在的
print(e)