一、背景
上一篇整理了 Python 中的流程控制、循环与迭代器。
这一篇继续整理 Python 中的函数、参数、递归和变量作用域。对应的练习代码主要包括:
40函数简介.py
42函数参数.py
43函数递归.py
44变量的作用域.py
函数是把一段逻辑组织起来,后面可以重复调用。写代码时,如果发现一段逻辑要重复出现,通常就应该考虑把它封装成函数。
二、为什么需要函数
函数可以解决几个问题:
- 减少重复代码
- 让代码结构更清楚
- 把复杂逻辑拆成多个小步骤
- 方便后续维护和复用
例如求列表最大值,可以直接使用 Python 内置函数:
nums = [1, 2, 3]
print(max(nums)) # 3
max()、len()、print() 都是 Python 已经提供好的函数。
我们也可以自己定义函数。
三、函数定义和调用
函数使用 def 定义。
基本格式如下:
def function_name(parameters):
"""函数说明"""
function_body
return result
例如:
def print_message(message):
"""打印传入的字符串"""
print(message)
return 1
result = print_message("hello python")
print(result)
输出:
hello python
1
这里需要注意:
def后面是函数名- 函数名后面是圆括号
- 参数写在圆括号中
- 函数体要缩进
return用于返回结果- 如果没有写
return,默认返回None
例如:
def say_hello(name):
print("hello", name)
result = say_hello("xiaoming")
print(result) # None
四、函数命名
函数命名建议使用小写字母和下划线。
def get_user_name():
pass
def calculate_total_price():
pass
不建议写成:
def getUserName():
pass
Python 中虽然不会禁止这种写法,但是从代码风格上来说,普通函数更推荐使用 snake_case。
五、必须参数
必须参数是调用函数时必须传入的参数。
def print_message(message):
print(message)
print_message("hello")
如果不传参数,会报错:
# print_message()
# TypeError: print_message() missing 1 required positional argument: 'message'
必须参数要按照函数定义中的顺序传入。
def print_user(name, age):
print(name, age)
print_user("xiaoming", 18)
这里 "xiaoming" 会传给 name,18 会传给 age。
六、关键字参数
关键字参数是调用函数时显式指定参数名。
def print_user(name, age):
print("name:", name)
print("age:", age)
print_user(age=18, name="xiaoming")
使用关键字参数时,参数顺序可以调整,因为已经明确指定了参数名。
这类写法在参数较多时更清楚。
七、默认参数
默认参数是在定义函数时给参数设置默认值。
def print_user(name, age=18):
print("name:", name)
print("age:", age)
print_user("xiaoming")
print_user("xiaohong", 20)
输出:
name: xiaoming
age: 18
name: xiaohong
age: 20
如果调用时没有传 age,就使用默认值 18。
如果调用时传了 age,就使用传入的值。
默认参数不要使用可变对象
默认参数有一个很容易踩坑的地方:不要把列表、字典这种可变对象作为默认值。
不推荐:
def add_item(item, data=[]):
data.append(item)
return data
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b']
第二次调用时,之前的列表还在。
推荐写法:
def add_item(item, data=None):
if data is None:
data = []
data.append(item)
return data
print(add_item("a")) # ['a']
print(add_item("b")) # ['b']
这个问题在实际开发中很常见,需要特别注意。
八、不定长参数
有时函数接收的参数个数不固定,可以使用 *args 和 **kwargs。
1. *args
*args 用来接收多个位置参数,函数内部看到的是一个元组。
def print_info(first, *args):
print("first:", first)
print("args:", args)
print_info(10)
print_info(70, 60, 50)
输出:
first: 10
args: ()
first: 70
args: (60, 50)
2. **kwargs
**kwargs 用来接收多个关键字参数,函数内部看到的是一个字典。
def print_user(**kwargs):
print(kwargs)
print_user(name="xiaoming", age=18)
输出:
{'name': 'xiaoming', 'age': 18}
*args 和 **kwargs 经常出现在框架代码、装饰器和通用工具函数中。
九、lambda 匿名函数
lambda 用来创建匿名函数。
基本格式如下:
lambda 参数1, 参数2: 表达式
例如:
add = lambda x, y: x + y
print(add(1, 2)) # 3
不过实际开发中,不建议为了使用 lambda 而使用 lambda。
如果逻辑比较复杂,直接写普通函数更清楚。
适合使用 lambda 的场景通常是作为参数传递一个简单表达式。
users = [
{"name": "xiaoming", "age": 18},
{"name": "xiaohong", "age": 16},
{"name": "xiaogang", "age": 20},
]
users = sorted(users, key=lambda item: item["age"])
print(users)
这里 lambda item: item["age"] 用来告诉 sorted() 按照 age 排序。
十、参数传递
Python 中一切都是对象,变量保存的是对象引用。
所以严格来说,Python 函数参数传递不能简单理解成传统意义上的值传递或引用传递,更准确的说法是:把对象引用传给函数。
但是从表现上看,需要区分可变对象和不可变对象。
十一、不可变对象作为参数
数字、字符串、元组属于不可变对象。
例如:
def change_int(num):
num = 10
value = 2
change_int(value)
print(value) # 2
函数内部把 num 指向了新的整数对象 10,不会影响外部变量 value。
字符串也是一样:
def change_text(text):
text = "python"
name = "hello"
change_text(name)
print(name) # hello
十二、可变对象作为参数
列表、字典、集合属于可变对象。
如果函数内部修改的是对象本身,外部也会看到变化。
def change_list(data):
data.append([1, 2, 3, 4])
print("函数内:", data)
nums = [10, 20, 30]
change_list(nums)
print("函数外:", nums)
输出:
函数内: [10, 20, 30, [1, 2, 3, 4]]
函数外: [10, 20, 30, [1, 2, 3, 4]]
如果不希望函数影响外部列表,可以在函数内部拷贝一份。
def change_list(data):
new_data = data.copy()
new_data.append(100)
return new_data
nums = [1, 2, 3]
new_nums = change_list(nums)
print(nums) # [1, 2, 3]
print(new_nums) # [1, 2, 3, 100]
十三、递归
递归就是函数自己调用自己。
递归必须满足两个条件:
- 函数内部会调用自己
- 必须有终止条件
如果没有终止条件,就会一直递归调用,最后触发递归深度错误。
# def hello():
# hello()
#
# hello()
# RecursionError: maximum recursion depth exceeded
1. 简单递归示例
def say_hello(name, count):
print("hello", name, count)
if count > 1:
say_hello(name, count - 1)
say_hello("xiaoming", 3)
输出:
hello xiaoming 3
hello xiaoming 2
hello xiaoming 1
这个例子中,count 每次减一,当 count 等于 1 时停止继续调用。
2. 阶乘示例
递归更常见的例子是阶乘。
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
执行过程可以理解为:
factorial(5)
5 * factorial(4)
5 * 4 * factorial(3)
5 * 4 * 3 * factorial(2)
5 * 4 * 3 * 2 * factorial(1)
5 * 4 * 3 * 2 * 1
递归代码有时很简洁,但是不一定总是最优选择。数据量比较大时,要注意递归深度和性能问题。
十四、变量作用域
变量作用域指的是变量可以被访问的范围。
Python 中变量查找遵循 LEGB 规则:
| 缩写 | 含义 | 说明 |
|---|---|---|
| L | Local | 局部作用域 |
| E | Enclosing | 外层函数作用域 |
| G | Global | 全局作用域 |
| B | Built-in | 内置作用域 |
查找顺序是:
Local -> Enclosing -> Global -> Built-in
十五、局部变量和全局变量
函数内部定义的变量是局部变量。
global_var = 10
def my_func():
local_var = 20
print(local_var)
print(global_var)
my_func()
local_var 只能在函数内部访问。
# print(local_var) # NameError: name 'local_var' is not defined
定义在函数外部的变量是全局变量,在当前模块中可以访问。
十六、if 和 for 不会创建新的作用域
Python 中,if、for、while 不会创建新的函数作用域。
例如:
if True:
value = 111
print(value) # 111
再看 for:
for x in range(3):
print(x)
print(x) # 2
循环结束后,变量 x 仍然可以访问。
这一点和很多语言不一样,需要注意。
十七、闭包
如果内部函数引用了外部函数中的变量,这个内部函数就可以形成闭包。
示例:
def line_conf():
b = 15
def line(x):
return 2 * x + b
return line
my_line = line_conf()
print(my_line(5)) # 25
这里 line() 使用了外部函数 line_conf() 中的变量 b。
即使 line_conf() 已经执行结束,返回出来的 line() 仍然记住了 b 的值。
这就是闭包。
闭包在装饰器、回调函数、函数式编程中会经常出现。
十八、global 关键字
如果要在函数内部修改全局变量,需要使用 global。
n = 100
def change(value):
global n
n = value
change(11)
print(n) # 11
不使用 global 时,函数内部赋值会创建局部变量。
n = 100
def change():
n = 11
print("函数内:", n)
change()
print("函数外:", n)
输出:
函数内: 11
函数外: 100
实际开发中,全局变量能少用就少用。全局状态过多时,代码会比较难维护。
十九、nonlocal 关键字
nonlocal 用来修改外层函数作用域中的变量。
def outer():
count = 10
def inner():
nonlocal count
count -= 1
print(count)
inner()
outer()
这里 count 不是全局变量,而是 outer() 中的变量。
所以不能用 global,要用 nonlocal。
二十、lambda 的作用域
lambda 本质上也是函数,所以它的变量查找规则也遵循 LEGB。
base = 10
f = lambda x: x + base
print(f(1)) # 11
如果 lambda 中使用了外部变量,仍然会按照局部、闭包、全局、内置的顺序查找。
不过如果逻辑稍微复杂,建议直接改成普通函数,代码会更清楚。
二十一、总结
这一篇主要整理了函数、参数、递归和变量作用域。
需要重点记住:
- 函数使用
def定义,使用函数名加括号调用 return用于返回结果,没有return默认返回None- 参数包括必须参数、关键字参数、默认参数、不定长参数
- 默认参数不要使用列表、字典这种可变对象
- 可变对象作为参数传入函数后,在函数内部修改对象会影响外部
- 递归必须有终止条件
- 变量查找遵循 LEGB 规则
global修改全局变量,nonlocal修改外层函数变量
函数是后面模块、类、异常处理、文件处理中都会继续使用的基础内容。