本文主要介绍 Python 函数,包括Python函数定义和调用、仅位置参数、仅关键字参数、可变参数、默认参数、局部变量和全局变量、函数文档说明、PEP 8编程风格要点等。阅读本文大约需要 15 min.
一文了解Python函数
1. 前言
函数(function)是具有独立功能的代码块。每一个函数都可以实现一个独立的功能,比如 print()
函数可以实现输出功能,input
函数可以实现输入功能。
函数的设计是为了提高代码的重用率,避免反复造轮子,提升开发效率
。有了函数,我们就可以像组装汽车一样来组装程序,不用再从 0 开始写,这大大提升了开发效率。
在 Python 中有着非常多的内置函数,提供了非常多的功能,不过有时这些功能还不足以满足我们的需求,这时我们就可以自定义函数。
本文是长文,主要内容:
- 函数的定义和调用
- 函数的参数(仅限位置参数、位置或关键字参数、仅限关键字参数)
2. 函数的定义和调用
函数的定义格式如下:
def funcname():
suite
def
是定义函数的关键字,是 define
的缩写,它后面的 funcname
是函数名(function name),suite 是代码块,跟 while、if 等语句一样,suite 可以是一行或者多行代码,前提是相同的缩进。
一个简单的示例:
# 定义一个函数,实现打印个人信息的功能
def my_Info():
print(f'我的名字是:Jock')
注意我们定义完函数,函数是不会立即被执行的,只有我们调用它,它才会执行。这里调用my_Info()
函数的方法很简单, 通过 funcname()
即 函数名()
就可以调用函数。示列如下:
# 定义一个函数,实现打印个人信息的功能
def my_Info():
print(f'我的名字是:Jock')
my_Info() # 调用 my_Info 函数
结果输出:
我的名字是:Jock
每次调用函数,函数都会从头开始执行,执行到 return
语句就会结束函数,不再继续执行,这里的 return
语句被省略了,如果你不写 return
语句,在 Python 中解释器默认在函数的最后添加 return None
。return
语句晚点我们还会详细说明。
3. 函数的参数
前面演示了最简单的函数,但是我们会发现这个函数的功能非常简单,只能打印 我的名字是:Jock
。如果我们想打印 我的名字是:Jack
那么我们又得重新写一个函数,说明原来的函数不够健壮和灵活。
这时候我们就引入函数的 形式参数(formal parameters)
来帮助我们增强函数的灵活性,使得我们的函数更加强大!所谓的 形式参数就是我们定义函数时,函数名后括号中的变量,简称“形参"
。
形参根据 函数调用的方式
可以分为 仅限位置参数(positional-only)、位置参数或关键字参数(positional-or-keyword)、仅限关键字参数(keyword-only)
。其中 关键字参数
也称 命名参数(named parameter)
。
接下来我们讲解形参的定义和调用方法。
3.1 位置参数
我们使用形参中的 位置参数(positional argument)
来提升 my_Info()
函数的灵活性,修改如下:
# 定义一个函数,实现打印个人信息的功能
def my_Info(name):
print(f'我的名字是:{name}')
my_Info('Jock') # 调用 my_Info 函数
my_Info('Lucy') # 调用 my_Info 函数
输出结果:
我的名字是:Jock
我的名字是:Lucy
对于 my_Info(name)
函数,参数 name
是一个形参,我们在调用 my_Info(nme)
函数时,必须有且只传入一个参数 name
。调用函数时,my_Info('Jock')
中的 'Jock'
被称为实际参数(actual parameter)
,简称“实参”
。实参即实际代入函数的参数值
。这里,采用 my_Info('Jock')
方式调用函数时,name
也是位置参数。
现在如果我们想打印更多的信息,比如:性别、年龄等,那么我们可以改写函数,使其可以接收多个位置参数(name, gender, age),改写代码如下:
# 定义一个函数,实现打印个人信息的功能
def my_Info(name, gender, age):
print(f'我的名字是:{name},性别:{gender},今年 {age} 岁')
my_Info('Jock', '男', 25) # 调用 my_Info 函数
my_Info('Lucy', '女', 26) # 调用 my_Info 函数
结果输出:
我的名字是:Jock,性别:男,今年 25 岁
我的名字是:Lucy,性别:女,今年 26 岁
3.2 默认参数
现在又要求我们同时输出国籍、居住城市等信息。我发现大家都是中国人,现在都住在武汉,那么我就可以在函数定义时,给位置参数指定默认值,这种有默认值的位置参数就叫 默认参数(default argument)
。修改后的代码如下:
# 定义一个函数,实现打印个人信息的功能
def my_Info(name, gender, age, nation='中国', city='武汉'):
print(f'我的名字是:{name},性别:{gender},今年 {age} 岁,来自{nation}, 现居{city}')
my_Info('Jock', '男', 25) # 仅给出必须参数
my_Info('Lucy', '女', 26, city='杭州') # 给出部分可选参数
my_Info('Kobe', '男', 41, '美国', '洛杉矶') # 给出全部可选参数
my_Info('Bob', '男', 71, nation='美国', city='洛杉矶') # 给出全部可选参数
结果输出:
我的名字是:Jock,性别:男,今年 25 岁,来自中国, 现居武汉
我的名字是:Lucy,性别:女,今年 26 岁,来自中国, 现居杭州
我的名字是:Kobe,性别:男,今年 41 岁,来自美国, 现居洛杉矶
我的名字是:Bob,性别:男,今年 71 岁,来自美国, 现居洛杉矶
在这个例子中,nation
和 city
就是默认参数,也可以称为可选参数
。从这个例子我们可以发现,使用默认参数的好处在于:简化函数的调用,降低函数调用的难度
。调用函数的时候,默认参数可以不传,使用使用默认值,或者只传入部分默认参数,或者全部默认参数都传。我们在使用 默认参数
时要注意以下几点:
书写
和调用
时位置参数(也称必须参数(mandatory argument)
)在前,默认参数在后,否则 Python 解释器报错,大家可以思考一下这样设计的好处是什么。后面还会给出例子。- 函数有多个参数时,我们把变化大的参数放在前面,变化小的放在后面,变化小的参数可以设置为默认参数。
默认参数必须指向不可变对象
。
Python 中 默认参数必须指向不可变对象
的原因是 默认参数的值有且仅在函数定义时计算 1 次
,这是 Python 中非常容易踩坑的地方
,我们看下面这例子:
i = 5
def f(arg=i): # 定义函数时,arg 的默认值设为 5
print(arg)
i = 6
f()
输出结果:
5
如果我们把默认参数指向了不可变对象,会出现什么情况呢?看下面这个例子:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
输出结果:
[1]
[1, 2]
[1, 2, 3]
我们发现输出结果并不是 [1]、[2]、[3]
。我们设置了 L
默认参数: L=[]
,可为什么 L
还会存储之前的调用结果呢?这是因为在 Python 中,默认参数值
在函数定义的时候就会被计算出来,并且默认值只会被计算一次
,后面的函数调用都不会再对默认参数重新赋初值。这里默认参数 L
在定义时,值被计算出来,即 []
,因为参数 L
也是一个变量,指向了一个可变对象 []
,每次调用该函数,如果改变了 L
指向对象[]
的值,则下次调用时,默认参数的内容就变了,不再是函数定义时的初值 []
。
所以上面的例子我们改写如下:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
结果输出:
[1]
[2]
[3]
3.3 关键字参数
关键字参数(keyword arguments)
是指调用函数时用 kwarg=value
的形式传入函数参数,其中 kwarg
就是 keyword arguments
的缩写。我们拿前面位置参数的例子来展示关键字参数:
# 定义一个函数,实现打印个人信息的功能
def my_Info(name, gender, age):
print(f'我的名字是:{name},性别:{gender},今年 {age} 岁')
my_Info('Jock', '男', 25) # 通过位置参数调用 my_Info 函数
my_Info(name='Jock', gender='男', age=25) # 通过关键字参数调用 my_Info 函数
输出结果:
我的名字是:Jock,性别:男,今年 25 岁
我的名字是:Jock,性别:男,今年 25 岁
这里我们分别通过 位置参数
和 关键字参数
的方式调用 my_Info()
函数,两种方法都是可以的。
相信大家到这里多少都有点迷惑,位置参数和关键字参数具体怎么区分,什么时候用位置参数,什么时候用关键字参数呢?
实际上区分位置参数和关键字参数取决于 调用函数时,它们传入函数的方式,而不取决于定义函数时的形式
。如果是按照位置传入函数的参数,即 value1, value2, value2
的形式传入函数,比如 my_Info('Jock', '男', 25)
就是位置参数,如果是按关键字参数传入函数,即 kwarg=value
的形式传入函数的参数就是关键字参数,比如 my_Info(name='Jock', gender='男', age=25)
,就是关键字参数传参。
到这里相信大家已经非常清楚怎么区别位置参数和关键字参数了。函数 my_Info(name, gender, age)
中 name, gender, age
既可以是位置参数,也可以是关键字参数,这种情况属于我们说的 Python 3 种参数中的 位置参数或关键字参数(positional-or-keyword)
,即我们定义的函数,可以通过位置参数的方式调用,也可以通过关键字参数的方式调用。
不过需要注意的是,不管是在定义还是在调用时,位置参数都必须在关键字参数前面
。
以下是定义和调用函数的常见错误以及正确的做法:
# 定义函数时错误案例一:定义函数时有位置参数在默认参数后
def my_Info(name, gender='男', age): # Python 解释器报错:SyntaxError: non-default argument follows default argument
print(f'我的名字是:{name},性别:{gender},今年 {age} 岁')
# 正确定义函数
def my_Info(name, gender, age):
print(f'我的名字是:{name},性别:{gender},今年 {age} 岁')
# 错误调用一:调用时有位置参数在默认参数后
my_Info('Jock', gender='男', 25) # SyntaxError: positional argument follows keyword argument
# 正确调用
my_Info('Jock', gender='男', age=25) # 1 个位置参数,两个关键字参数
# 错误调用二:位置参数多次赋值
"""这里 25 作为位置参数赋值给 gender,后面又采用关键词参数的形式 gender='男'
对 gender 赋值,多次赋值,所以报错"""
my_Info('Jock', 25, gender='男') # TypeError: my_Info() got multiple values for argument 'gender'
# 正确调用
my_Info('Jock', '男', age=25) # 2 个位置参数,2 个关键字参数
# 错误调用三:调用未出现的关键字
my_Info('Jock', '男', my_age=25) # TypeError: my_Info() got an unexpected keyword argument 'my_age'
# 正确调用
my_Info(name='Jock', gender='男', age=25) # 3 个关键字参数
3.4 特殊参数
通常,参数可以按位置或通过关键字显式传递给 Python 函数。我们开发时限制传递参数的方式是有意义的,它可以提高代码的可读性和性能,使得开发人员只需查看函数定义即可确定是函数按位置、按位置或关键字、还是按关键字传递参数。
函数定义可能类似于:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
这里:
/
和*
是可选的。/
前面的参数都是仅限位置参数(positional-only parameter)
,即参数只能通过位置参数的形式传入函数,不能通过关键字的形式传入函数。/
和*
之间的是位置或关键字参数(positional-or-keyword parameter)
,即参数可以通过位置参数的形式参入函数,也可以通过关键字的形式传入函数。*
后面的参数是仅限关键字参数(keyword-only parameter)
,即只能通过关键字传入参数。
注意:在 Python 3.7 及之前的 Python 版本都是不能在函数定义中的使用/
符号,否则会报错,而 *
是可以使用的。从 Python 3.8 开始 /
才可以在函数定义时使用了。
下面给出官方文档中的用法举例:
>>> def standard_arg(arg):
... print(arg)
...
>>> def pos_only_arg(arg, /):
... print(arg)
...
>>> def kwd_only_arg(*, arg):
... print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
... print(pos_only, standard, kwd_only)
第一个函数 standard_arg(arg)
是我们定义函数的方式,它对于传入参数的方式没有限制,可以是位置参数,也可以关键字参数形式传入。下面两种方法都正确:
>>> standard_arg(2)
2
>>> standard_arg(arg=2)
2
第二个函数 pos_only_arg(arg, /)
里面出现了 /
符号,所以它前面的参数都是仅限位置参数,只能通过位置参数的形式传入函数,通过关键字参数形式将报错,示例如下:
>>> pos_only_arg(1)
1
>>> pos_only_arg(arg=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got an unexpected keyword argument 'arg'
第三个函数 kwd_only_arg(*, arg)
里面出现了 *
符号,所以它后面的参数都是仅限关键字参数,只能通过关键字参数的形式传入函数,通过位置参数形式传入将报错。示例如下:
>>> kwd_only_arg(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
>>> kwd_only_arg(arg=3)
3
第四个函数 combined_example(pos_only, /, standard, *, kwd_only)
出现了 /
和 *
,所以它组合了 3 种函数调用约定,即仅限位置参数、位置或关键字参数、仅限关键字参数。示例如下:
>>> combined_example(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given
>>> combined_example(1, 2, kwd_only=3)
1 2 3
>>> combined_example(1, standard=2, kwd_only=3)
1 2 3
>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() got an unexpected keyword argument 'pos_only'
这里再提醒一下,函数定义时,仅限位置参数、位置或关键字参数、仅限关键字参数的默认值都是可选的,可以设置默认值,也可以不设置。但是我们组合的时候要注意:
- 如果
/
前的仅限位置参数设置了默认值,依据默认参数必须在位置参数之后的原则
,那么/
和*
之间的位置或关键字参数在函数定义时
只能通过kwarg=value
的形式定义,否则会报错,不过调用时
位置参数和关键字参数的传入方式都可以。但*
之后的仅限关键字参数不受影响,可以不设置默认值,但是调用必须用关键字参数的形式调用。Python 3.8 中测试如下:
# 错误定义:仅限位置参数有默认值,/ 和 * 的位置或关键字参数需用 kwarg=value 定义
# SyntaxError: non-default argument follows default argument
def my_fun(name, age=12, /, nation, *, city):
print(f'我的名字是:{name},今年{age}岁,来自{nation},现居{city}')
# 正确定义一:关键字参数不设置默认值
def my_fun(name, age=12, /, nation='中国', *, city):
print(f'我的名字是:{name},今年{age}岁,来自{nation},现居{city}')
# 正确定义二:关键字参数设置默认值
def my_fun(name, age=12, /, nation='中国', *, city='武汉'):
print(f'我的名字是:{name},今年{age}岁,来自{nation},现居{city}')
# 错误调用一:仅限位置参数不能通过关键字参数形式传入
my_fun('Jock', age=25) # TypeError: my_fun() got some positional-only arguments passed as keyword arguments: 'age'
# 正确调用一
my_fun('Jock', 25) # Correct
# 正确调用二
my_fun('Jock', 25, '中国') # Correct
# 正确调用三
my_fun('Jock', 25, nation='中国') # Correct
# 错误调用二:仅限关键字参数不能通过位置参数的形式传入
my_fun('Jock', 25, '中国', '武汉') # TypeError: my_fun() takes from 1 to 3 positional arguments but 4 were given
# 正确调用四
my_fun('Jock', 25, nation='中国', city='桂林') # Correct
了解了这些,以后我们再也不用担心不会正确的定义函数,也不用担心看不懂函数中的 /
和 *
,可以轻松的调用函数。比如 Python 中的 list
对象的 index
方法,我们可以使用 help(list.index)
查看它的用法如下:
>>> help(list.index)
Help on method_descriptor:
index(self, value, start=0, stop=2147483647, /)
Return first index of value.
Raises ValueError if the value is not present.
我们可以看到 index 函数有 4 个参数, self, value, start, stop, 其中 start 和 stop 有默认值,里面还有一个 /
,所以 value, start, stop 都是仅限位置参数,只能通过位置参数的方式传入函数。这里的 self
比较特殊,后面我们学习类的时候再介绍。示例如下:
>>> list_a = [1, 2, 3, 4, 2, 3]
>>> list_a.index(2, 2) # 从第 3 个位置开始找起
4
>>> list_a.index(2, start=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: index() takes no keyword arguments
好了,学习到这里,我们可以讲解最后一种可变参数(variable argument)
了,在 Python 中可变参数分为两种,一种是 *args
,一种是 **kwargs
。其中:
-
*args
是用来专门存过量的位置参数的元组,如果我们想传入任意个位置参数,我们就可以定义函数为def func(*args)
这时,我们调用函数func(1, 2, 3, 4, 5)
函数会把1, 2, 3, 4, 5
这 5 个参数作为一个元组(tuple)
,传入函数,为什么是元组,不是列表呢?就是因为元组不可变!注意:*args
后面的参数都是仅限关键字参数
,必须通过关键字参数的形式调用。 -
**kwargs
是用来专门存过量的关键字参数的字典。如果我们想传入任意个关键字参数,我们可以定义函数为def func(**kwargs)
,这是我们调用函数func(a=1, b=2, c=3)
,函数会把a=1, b=2, c=3
这 3 对 key-value 作为一个字典(dict),传入函数。
*args
和 **kwargs
可以组合使用,用于接收任意个位置参数和关键字参数,但是 *args
必须在 **kwargs
前面,所以对于任意函数,都可以通过类似 func(*args, **kwargs)
的形式调用它,无论它的参数是如何定义的。
*args
可变位置参数示列:
>>> def concat(*args, sep="/"): # sep 为仅限关键字参数
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".") # sep 为仅限关键字参数
'earth.mars.venus'
>>> concat("earth", "mars", "venus", ".")
此外,如果我们有一个列表 ["earth", "mars", "venus"]
,或者元组 ("earth", "mars", "venus")
,想传入 concat()函数怎么办呢?如果我们直接把列表或者元组传进去都会报错,正确的做法是解包参数列表,即传入 *List
和 *tuple
的形式,*
可以理解为去掉 []
或者 ()
,这非常常用且实用。示例如下:
>>> list_a = ["earth", "mars", "venus"]
>>> concat(list_a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in concat
TypeError: sequence item 0: expected str instance, list found
>>> concat(*list_a)
'earth/mars/venus'
>>> t = ("earth", "mars", "venus")
>>> concat(t)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in concat
TypeError: sequence item 0: expected str instance, tuple found
>>> concat(*t)
'earth/mars/venus'
**kwargs
可变关键字参数示例:
def kwargs_fun(**kwargs):
for key in kwargs:
print(f'{key}:{kwargs[key]}')
kwargs_fun(a=1, b=2, c=3)
输出结果:
a:1
b:2
c:3
*args
和 **kwargs
组合使用示例:
def cheeseshop(kind, *arguments, **keywords):
print("-- Do you have any", kind, "?")
print("-- I'm sorry, we're all out of", kind)
for arg in arguments:
print(arg)
print("-" * 40)
for kw in keywords:
print(kw, ":", keywords[kw])
cheeseshop("Limburger", "It's very runny, sir.",
"It's really very, VERY runny, sir.",
shopkeeper="Michael Palin",
client="John Cleese",
sketch="Cheese Shop Sketch")
输出结果:
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch
此外,字典中也可以用解包,符号是 **dict
,相当于去掉 {}
,测试如下:
>>> def parrot(voltage, state='a stiff', action='voom'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.", end=' ')
... print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d) # 解包字典
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !
这里稍微补充一个测试结果,在 Python 程序中,如果一个函数在调用前重复定义了,那么 Python 解释器会调用哪个呢?答案是:调用最后定义的那个函数。因为函数名也相当于一个变量,重复定义同一个函数,那么后定义的函数会覆盖掉之前定义的函数。测试如下:
"""
时间:
2020年4月2日12:13:41
目的:
测试源代码中定义多个同名函数时,Python 解释器的调用顺序
总结:
后定义的函数会覆盖之前定义的函数,也就是说重复定义时,会调用最后定义的函数。
"""
def fun_1():
print('这是第一次定义fun_1()')
def fun_1():
print('这是第二次定义fun_1()')
if __name__ == '__main__':
fun_1()
"""
在 Python 3.8 中输出结果:
这是第二次定义fun_1()
可以发现,fun_1()
函数在调用前,定义了两次,我们调用的时候执行的是第 2 次定义的内容。
3.5 小结
学到这里,多少都有点迷糊了,我们把 Python 中参数的怎么用,小结一下吧:
- 只能通过
func(value1, value2)
方式调用的是仅限位置参数,只能通过func(kw1=value1, kw2=value2)
方式调用的是仅限关键字参数,两者都可以的是位置或关键字参数。 - 不管是位置参数还是关键字参数都可以设定默认值,这时也可以称为默认参数。
- 函数定义时,变化大的参数放在前面,变化小的参数放在后面,可以把变化小的参数设置为默认参数。
- 不管是调用还是定义,位置参数一定要在默认参数和关键字参数前面!
- 默认参数必须指向不可变对象。否则可能引发 Bug。
- 可变位置参数
*args
必须在可变关键字参数**kwargs
前面。如:func(*args, **kwargs)
*args
前面参数是仅限位置参数,后面参数是仅限关键字参数。/
前是仅限位置参数,/
和*
之间是位置或关键字参数,*
之后是仅限关键字参数。- 各参数的定义顺序:仅限位置参数 --> 默认仅限位置参数 --> 可变位置参数(
*args
) --> 仅限关键字参数(kw=value) --> 默认仅限关键字参数 --> 可变关键字参数(**args
)。如:
def my_Info(name, gender, age=12, *args, nation, city='武汉', **kwargs):
print(f'name: {name}, gender: {gender}, age: {age}, *args: {args}, nation: {nation}, city: {city}, **kwargs: {kwargs}')
my_Info('Jock', '男', 25, nation='中国', city='桂林', 爱好='篮球')
结果输出:
name: Jock, gender: 男, age: 25, *args: (), nation: 中国, city: 桂林, **kwargs: {'爱好': '篮球'}
不要同时使用太多的组合,否则函数接口的可理解性很差
。
Python 3.8 官方文档中总结如下:
格式:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
- 如果你希望形参名称对用户来说不可用,则使用仅位置形参。这适用于形参名称没有实际意义,以及当你希望强制规定调用时的参数顺序,或是需要同时接收一些位置形参和任意关键字形参等情况。
- 当形参名称有实际意义,以及显式指定形参名称可使函数定义更易理解,或者当你想要防止用户过于依赖传入参数的位置时,则使用仅限关键字形参。
- 对于 API 来说,使用仅限位置形参可以防止形参名称在未来被修改时造成破坏性的 API 变动。
这里如果深究会涉及接口设计问题,这部分留作以后再探讨吧。我们目前只要明白基本的函数定义,以及能够看懂别人的函数文档就好了。
4. 函数的返回值
前面我们已经见过了 return
语句,它的功能是函数调用完成一件事情之后,给调用者反馈结果,这个反馈结果就是函数的返回值。
函数是帮我们处理事情的,我们调用函数,当然希望函数处理结束后给我们反馈,告诉我们事情做得怎么样。这是不是非常符合我们现实中的场景?就像老板给你分配了一件任务,你做完之后得想老板报告这件事做得怎么样。接下来我们总结一下 return 语句的常用用法。
4.1 返回 None
如果我们定义的函数没有写 return
语句,那么这个函数默认返回 None
。测试如下:
def my_fun():
pass
print(my_fun())
结果输出:
None
pass 语句之前我们说过,是一个占位符,什么操作也不做,单纯的占位置,使其符合语法规则。 my_fun()
函数中没有 return 语句,所以默认返回 None
,通过 print(my_fun())
打印其返回值,发现是 None。
4.2 返回一个值
现在我们有个函数,它的功能是接收一串数字,然后计算它们的和,返回值是它们的和。代码如下:
def my_sum(*args):
sum = 0
for i in args:
sum += i
return sum # 返回 sum
result = my_sum(1, 2, 3, 4) # 将函数的返回值赋值给 result 变量
print(result)
输出结果:
10
这里 my_fun(*args)
函数包含 return sum
即返回值是所有数之和:sum。
4.3 返回多个值
有时我们希望返回多个值,这个时候我们就可以把多个值放到一个容器中返回,比如元组(tuple),列表(list),字典(dict)都可以。代码如下:
# 计算一个数的平方和立方
def my_cal(x):
return x*x, x*x*x # 以元组的形式返回一个数的平方和立方
n, m = my_cal(4) # 多变量赋值
print(n, m)
输出结果:
16 64
4.4 多个 return 语句
如果一个函数中有多个 return 语句,那么只有有一个 return 语句被执行,那么这个函数的调用就会结束,剩下的代码不会被执行。代码如下:
def query(score):
if score >= 90:
return '优秀!'
elif score >= 60 and score < 90:
return '合格!'
else:
return '很遗憾,您未能通过考试T_T...'
print('这句永远不会被执行!')
print(query(95))
print(query(70))
print(query(55))
输出结果:
优秀!
合格!
很遗憾,您未能通过考试T_T...
可以发现 print('这句永远不会被执行!')
不会被执行。
5. 函数的文档说明
为了提升代码的可读性和易于日后维护,通常我们会在函数的内部注释函数的说明,自己或他人通过特定的方法能够看到这些说明。在你编写的函数中包含函数文档说明是一种很好的做法,所以要养成习惯。
以下是有关文档说明的内容和格式的一些约定。
第一行应该是函数目的的简要概述。为简洁起见,它不应显式声明对象的名称或类型,因为这些可通过其他方式获得(除非名称恰好是描述函数操作的动词)。这一行应以大写字母开头,以句点结尾。
如果文档字符串中有更多行,则第二行应为空白,从而在视觉上将摘要与其余描述分开。后面几行应该是一个或多个段落,描述对象的调用约定,它的副作用等。
示例:
>>> def my_function():
... """Do nothing, but document it.
...
... No, really, it doesn't do anything.
... """
... pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.
No, really, it doesn't do anything.
文档说明的查看方法就是我们前面提到过的 help(函数名)
或者 print(函数名.__doc__)
。
6. 局部变量和全局变量
理解局部变量和全局变量对于我们编程、看懂程序和分析 bug 都很有必要。那么什么是局部变量和全局变量呢?
- 局部变量:函数内定义的变量叫局部变量。只能在定义中的函数使用,函数调用时创建,函数调用结束后被销毁。只作用于当前函数。它设计的目的是一方面是为了避免命名冲突,另一方面用完就释放,节省空间。
- 全局变量:函数外定义的变量叫全局变量。可以被所有的函数使用,在程序启动时创建,直到程序终止才被销毁。可以作用于所有函数。它设计的目的是为了避免重复多次定义相同的使用频率高的变量,从而节省空间。
变量的作用范围:变量作用范围可以理解为变量在代码中的有效范围。有点像同班长(局部变量)只能管理本班(函数)的事务,不能管理其他班(其他函数)的事务,级长(全局变量)可以管理所有班(所有函数)的事务。
示例如下:
time = '我是全局变量-time' # 定义一个全局变量
def my_fun():
n = '我是 my_fun() 的局部变量-n' # 定义一个局部变量
def my_fun_2(): # 函数 my_fun() 内定义一个函数
s = '我是 my_fun_2() 的局部变量-s' # 定义一个局部变量
print(f'我在 my_fun_2() 函数中调用自身的局部变量 s: {s}')
print(f'我在 my_fun_2() 函数中调用外部函数 my_fun()的局部变量n: {n}')
print(f'我在 my_fun_2() 函数中调用全局变量time: {time}')
my_fun_2() # 在函数 my_fun() 内调用函数 my_fun_2()
print(f'我在 my_fun() 函数中的自身的局部变量n: {n}')
print(f'我在 my_fun() 函数中调用全局变量time: {time}')
my_fun()
print(f'我在函数外调用全局变量time: {time}')
结果输出:
我在 my_fun_2() 函数中调用自身的局部变量 s: 我是 my_fun_2() 的局部变量-s
我在 my_fun_2() 函数中调用外部函数 my_fun()的局部变量n: 我是 my_fun() 的局部变量-n
我在 my_fun_2() 函数中调用全局变量time: 我是全局变量-time
我在 my_fun() 函数中的自身的局部变量n: 我是 my_fun() 的局部变量-n
我在 my_fun() 函数中调用全局变量time: 我是全局变量-time
我在函数外调用全局变量time: 我是全局变量-time
接下来就说一说如果局部变量和全局变量同名的问题是怎么处理的?因为 Python 解释器的原因,函数在执行时会引入一个用于函数局部变量的新符号表。更确切地说,函数中所有的变量赋值都将存储在局部符号表中,而变量引用会首先在局部符号表中查找,然后是外层函数的局部符号表,再然后是全局符号表,最后是内置名称的符号表,即 LEGB
原则。 因此,全局变量和外层函数的变量不能在函数内部直接赋值(除非是在 global 语句中定义的全局变量,或者是在 nonlocal 语句中定义的外层函数的变量),但是它们可以被引用。
测试如下:
n = 100 # 定义一个全局变量
def my_fun():
n = 200 # 定义一个局部变量 n,与全局变量同名
print(f'my_fun() 中变量 n 的值是{n}')
def my_fun_2():
print(f'my_fun_2() 中变量 n 的值是{n}')
my_fun()
my_fun_2()
print(f'函数外全局变量的值是{n}')
输出结果:
my_fun() 中变量 n 的值是200
my_fun_2() 中变量 n 的值是100
函数外全局变量的值是100
所以我们记住前面的 LEGB 原则就知道局部变量和全局变量的顺序问题了。
一般我们不会在函数中修改全局变量的值,如果你非要修改,Python 中提供了相关的操作,通过 global
关键字来实现在函数中修改全局变量。如果想在内部函数修改外部函数的局部变量可以使用 nonlocal
关键字,这里我们也不展开了,日后用到,查看官方文档即可。
7. 函数标注
函数标注是我们自定义函数时使用的参数类型和返回值类型的完全可选的元数据信息,详细规则可以参阅PEP 3107 和 PEP 484。日后再总结一篇吧。
函数标注以字典的形式存放在函数的 __annotations__
属性中,并且不会影响函数的任何其他部分。
-
形参标注的定义方式是在形参名称后加上冒号(
:
),后面跟一个表达式,该表达式会被求值为标注的值。 -
返回值标注的定义方式是加上一个组合符号 (
->
),后面跟一个表达式,该标注位于形参列表和表示def
语句结束的冒号之间。 下面的示例有一个位置参数,一个关键字参数以及返回值带有相应标注:
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'
8. 编程风格 PEP8
学习了函数,我们就可以编写更长更加复杂的 Python 代码了,这时候代码风格的重要性也体现出来了,好的代码风格可以增强代码的可读性,利于团队开发和代码维护。
对于 Python,PEP 8 已经成为大多数项目所遵循的风格指南;它促进了一种非常易读且令人赏心悦目的编码风格。每个 Python 开发人员都应该在某个时候阅读它;以下是最重要的几个要点:
- 使用 4 个空格缩进,不要使用制表符。4 个空格是一个在小缩进(允许更大的嵌套深度)和大缩进(更容易阅读)的一种很好的折中方案。制表符会引入混乱,最好不要使用它。
- 换行,使一行不超过 79 个字符。这有助于使用小型显示器的用户,并且可以在较大的显示器上并排放置多个代码文件。
- 使用空行分隔函数和类,以及函数内的较大的代码块。
- 如果可能,把注释放到单独的一行。
- 使用文档字符串。
- 在运算符前后和逗号后使用空格,但不能直接在括号内使用: a = f(1, 2) + g(3, 4)。
- 以一致的规则为你的类和函数命名;按照惯例应使用 UpperCamelCase 来命名类,而以 lowercase_with_underscores 来命名函数和方法。 始终应使用 self 来命名第一个方法参数。
- 如果你的代码旨在用于国际环境,请不要使用花哨的编码。Python 默认的 UTF-8 或者纯 ASCII 在任何情况下都能有最好的表现。
- 同样,哪怕只有很小的可能,遇到说不同语言的人阅读或维护代码,也不要在标识符中使用非 ASCII 字符。
9. 巨人的肩膀
推荐阅读:
- 编程小白安装Python开发环境及PyCharm的基本用法
- 一文了解Python基础知识
- 一文了解Python数据结构
- 一文了解Python流程控制
- 一文了解Python部分高级特性
- 一文了解Python的模块和包
后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技
,扫描下方二维码或者搜索每日学一技
关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!