Python 学习笔记 6:函数、参数、递归与变量作用域


一、背景

上一篇整理了 Python 中的流程控制、循环与迭代器。

这一篇继续整理 Python 中的函数、参数、递归和变量作用域。对应的练习代码主要包括:

40函数简介.py
42函数参数.py
43函数递归.py
44变量的作用域.py

函数是把一段逻辑组织起来,后面可以重复调用。写代码时,如果发现一段逻辑要重复出现,通常就应该考虑把它封装成函数。

二、为什么需要函数

函数可以解决几个问题:

  1. 减少重复代码
  2. 让代码结构更清楚
  3. 把复杂逻辑拆成多个小步骤
  4. 方便后续维护和复用

例如求列表最大值,可以直接使用 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

这里需要注意:

  1. def 后面是函数名
  2. 函数名后面是圆括号
  3. 参数写在圆括号中
  4. 函数体要缩进
  5. return 用于返回结果
  6. 如果没有写 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" 会传给 name18 会传给 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]

十三、递归

递归就是函数自己调用自己。

递归必须满足两个条件:

  1. 函数内部会调用自己
  2. 必须有终止条件

如果没有终止条件,就会一直递归调用,最后触发递归深度错误。

# 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 中,ifforwhile 不会创建新的函数作用域。

例如:

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 中使用了外部变量,仍然会按照局部、闭包、全局、内置的顺序查找。

不过如果逻辑稍微复杂,建议直接改成普通函数,代码会更清楚。

二十一、总结

这一篇主要整理了函数、参数、递归和变量作用域。

需要重点记住:

  1. 函数使用 def 定义,使用函数名加括号调用
  2. return 用于返回结果,没有 return 默认返回 None
  3. 参数包括必须参数、关键字参数、默认参数、不定长参数
  4. 默认参数不要使用列表、字典这种可变对象
  5. 可变对象作为参数传入函数后,在函数内部修改对象会影响外部
  6. 递归必须有终止条件
  7. 变量查找遵循 LEGB 规则
  8. global 修改全局变量,nonlocal 修改外层函数变量

函数是后面模块、类、异常处理、文件处理中都会继续使用的基础内容。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录