📊 阅读时长:22分钟 | 关键词:Python函数、def、参数传递、return、作用域、命名空间、lambda

引言:为什么要用函数?

你已经学会了变量、数据类型、条件判断、循环——这些足以让你写出能解决简单问题的程序了。但当你开始写更复杂的程序时,你会遇到几个问题:

  1. 代码重复:同一段逻辑在多个地方出现,改一处就要改所有地方
  2. 难以理解:几百行代码揉在一起,很难看清整体逻辑
  3. 难以测试:无法单独验证某个功能是否正确

函数的出现就是为了解决这些问题。它是编程中最重要的抽象手段之一。

函数的核心价值

  • 封装:把一段逻辑打包,起个名字,以后调用名字就行
  • 复用:写一次,到处用
  • 模块化:把大问题拆成小问题,每个函数解决一个小问题

一、函数的定义和调用

1.1 基本语法
def 函数名(参数列表):
    """文档字符串(可选)"""
    函数体
    return 返回值(可选)

一个最简单的函数:

def greet():
    """向用户打招呼"""
    print('你好,欢迎学习 Python!')

# 调用函数
greet()        # 输出:你好,欢迎学习 Python!
greet()        # 可以多次调用

关键语法规则

  • def 关键字开头
  • 函数名后必须有括号冒号
  • 函数体必须缩进
  • 函数定义时不执行,只有调用时才执行
1.2 带参数的函数
def greet(name):
    """向指定用户打招呼"""
    print(f'你好,{name}!欢迎学习 Python!')

greet('小明')   # 你好,小明!欢迎学习 Python!
greet('小红')   # 你好,小红!欢迎学习 Python!

形参 vs 实参

  • 形参(parameter):函数定义时声明的参数,如 name
  • 实参(argument):函数调用时传入的值,如 '小明'
1.3 函数是一等公民:可以赋值给变量

在 Python 中,函数也是对象,可以赋值给变量、作为参数传递:

def plus(num):
    print(num + 1)

# 把函数赋值给变量
f = plus
print(plus)    # <function plus at 0x...>
print(f)       # <function plus at 0x...> —— 同一个函数对象

f(2)           # 3 —— 通过变量调用函数
f(5)           # 6

二、return:函数的输出

2.1 return 的基本用法

return 做两件事:

  1. 把后面的值返回给调用方
  2. 结束函数执行
def add(left, right):
    result = left + right
    return result

sum_value = add(3, 4)
print(sum_value)    # 7
print(add(5, 6))    # 11
2.2 返回多个值
def calc(left, right):
    """返回加法和乘法的结果"""
    return left + right, left * right

# 接收:自动打包成元组
result = calc(3, 4)
print(result)         # (7, 12)

# 或者直接解包
add_result, mul_result = calc(3, 4)
print(add_result)     # 7
print(mul_result)     # 12 Job
2.3 没有 return 的情况
# 情况1:没有 return 语句
def func1():
    print('hello')

print(func1())   # hello \n None —— 返回 None

# 情况2:return 后面什么都不跟
def func2():
    return

print(func2())   # None

# 情况3:只有 pass
def func3():
    pass

print(func3())   # None

规则:函数如果没有遇到 return,或者 return 后面没跟值,默认返回 None

2.4 return 后的代码不会执行
def demo():
    print('这行会执行')
    return '结果'
    print('这行永远不会执行')   # 不可达代码

demo()
# 输出:这行会执行

三、参数传递:可变对象 vs 不可变对象

这是 Python 函数中最容易搞混的概念。先看代码:

# ===== 传不可变对象(int、str、tuple 等)=====
def modify_num(x):
    print(f'函数内(修改前): x={x}, id={id(x)}')
    x = 999
    print(f'函数内(修改后): x={x}, id={id(x)}')

a = 789
print(f'函数外(调用前): a={a}, id={id(a)}')
modify_num(a)
print(f'函数外(调用后): a={a}, id={id(a)}')

# 输出:
# 函数外(调用前): a=789, id=4378419440
# 函数内(修改前): x=789, id=4378419440  ← 同一个对象!
# 函数内(修改后): x=999, id=4378518832  ← 新对象!
# 函数外(调用后): a=789, id=4378419440  ← 不受影响!
# ===== 传可变对象(list、dict、set 等)=====
def modify_list(lst):
    print(f'函数内(修改前): lst={lst}, id={id(lst)}')
    lst.append(4)
    print(f'函数内(修改后): lst={lst}, id={id(lst)}')

a = [1, 2, 3]
print(f'函数外(调用前): a={a}, id={id(a)}')
modify_list(a)
print(f'函数外(调用后): a={a}, id={id(a)}')

# 输出:
# 函数外(调用前): a=[1, 2, 3], id=4378524352
# 函数内(修改前): lst=[1, 2, 3], id=4378524352  ← 同一个对象!
# 函数内(修改后): lst=[1, 2, 3, 4], id=4378524352  ← 还是同一个!
# 函数外(调用后): a=[1, 2, 3, 4], id=4378524352  ← 被改了!

在这里插入图片描述

核心结论

传入类型 函数内修改行为 外部是否受影响
不可变对象(int/str/tuple) 赋值 = 创建新对象 ❌ 不受影响
可变对象(list/dict/set) 原地修改(append 等) ✅ 受影响
可变对象 赋值 = 创建新对象 ❌ 不受影响
# 可变对象但用赋值(创建新对象),外部不受影响
def reassign(lst):
    lst = [4, 5, 6]    # 创建新列表,不是原地修改

a = [1, 2, 3]
reassign(a)
print(a)    # [1, 2, 3] —— 不受影响

四、参数类型详解

Python 函数的参数非常灵活,支持多种形式。

4.1 必需参数

最普通的参数——调用时必须传入,不多不少:

def func(a, b):
    print(a - b)

func(3, 4)     # -1
# func(3)      # TypeError! 少了参数
# func(3,4,5)  # TypeError! 多了参数
4.2 位置参数 vs 关键字参数
def func(a, b):
    print(f'a={a}, b={b}')

# 位置参数:按顺序传递
func(3, 4)         # a=3, b=4
func(4, 3)         # a=4, b=3

# 关键字参数:按名称传递,顺序无关
func(a=3, b=4)     # a=3, b=4
func(b=4, a=3)     # a=3, b=4

# 混合使用:位置参数必须在关键字参数前面
func(3, b=4)       # a=3, b=4  ✓
# func(a=3, 4)     # SyntaxError! 位置参数不能跟在关键字参数后面
4.3 默认参数

给参数设置默认值,调用时可以不传:

def func(a, b=4):
    print(a - b)

func(3)        # -1 —— b 使用默认值 4
func(3, 5)     # -2 —— b 使用传入的值 5
func(a=3)      # -1
func(3, b=5)   # -2

⚠️ 默认参数的陷阱:默认值在函数定义时就被创建,而不是每次调用时创建!

# 危险的写法
def add_item(item, lst=[]):
    lst.append(item)
    return lst

print(add_item(1))   # [1]
print(add_item(2))   # [1, 2]  —— 预期是 [2]!
print(add_item(3))   # [1, 2, 3]  —— 越积累越多!

# 原因:lst=[] 只在定义时创建一次,之后每次调用共享同一个列表

# 正确的写法
def add_item(item, lst=None):
    if lst is None:
        lst = []      # 每次调用时创建新列表
    lst.append(item)
    return lst

print(add_item(1))   # [1]
print(add_item(2))   # [2]  ✓

在这里插入图片描述

4.4 不定长参数:*args 和 **kwargs

当你不知道调用者会传多少个参数时,用不定长参数:

# *args:接收任意数量的位置参数,打包成元组
def func(*args):
    print(f'接收到了 {len(args)} 个参数:{args}')

func()                # 接收到了 0 个参数:()
func(3, 1, 4, 6)     # 接收到了 4 个参数:(3, 1, 4, 6)

# **kwargs:接收任意数量的关键字参数,打包成字典
def func(**kwargs):
    print(f'接收到了:{kwargs}')

func()                        # 接收到了:{}
func(a=3, b=2, c=4)          # 接收到了:{'a': 3, 'b': 2, 'c': 4}

# 两者结合:*args 必须在 **kwargs 前面
def func(*args, **kwargs):
    print(f'位置参数:{args}')
    print(f'关键字参数:{kwargs}')

func(1, 2, 3, name='小明', age=25)
# 位置参数:(1, 2, 3)
# 关键字参数:{'name': '小明', 'age': 25}

命名约定argskwargs 只是约定俗成的名字,你可以用其他名字。但建议遵循惯例,因为这是 Python 社区的标准。

4.5 特殊参数:/*

Python 3.8+ 支持用 /* 来限制参数的传递方式:

def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    """
    pos1, pos2:仅限位置参数(在 / 之前)
    pos_or_kwd:位置或关键字都可以
    kwd1, kwd2:仅限关键字参数(在 * 之后)
    """
    print(pos1, pos2, pos_or_kwd, kwd1, kwd2)

# ✓ 正确
func(1, 2, 3, kwd1=4, kwd2=5)
func(1, 2, pos_or_kwd=3, kwd1=4, kwd2=5)

# ✗ 错误
# func(pos1=1, pos2=2, pos_or_kwd=3, kwd1=4, kwd2=5)  # pos1/pos2 不能用关键字传
# func(1, 2, 3, 4, 5)  # kwd1/kwd2 不能用位置传

这个特性主要用于库函数的 API 设计,日常开发中用得不多,但阅读源码时会遇到。

五、函数的返回值:不只是 return

5.1 返回函数

由于函数是一等公民,你可以从函数中返回另一个函数:

def make_multiplier(n):
    """返回一个把输入乘以 n 的函数"""
    def multiplier(x):
        return x * n
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

这就是闭包的雏形——内部函数记住了外部函数的变量 n。我们会在后续文章中深入讲解。

六、动手练习

练习 1:编写计算器函数

def calculator(a, b, operator):
    """
    根据 operator 执行对应运算
    operator: '+', '-', '*', '/'
    """
    if operator == '+':
        return a + b
    elif operator == '-':
        return a - b
    elif operator == '*':
        return a * b
    elif operator == '/':
        return a / b if b != 0 else '除数不能为0'
    else:
        return '不支持的运算符'

print(calculator(10, 5, '+'))   # 15
print(calculator(10, 5, '/'))   # 2.0
print(calculator(10, 0, '/'))   # 除数不能为0

练习 2:验证默认参数陷阱

# 预测以下代码的输出,然后运行验证
def mystery(x, data=[]):
    data.append(x)
    return sum(data)

print(mystery(5))     # ?
print(mystery(10))    # ?
print(mystery(15))    # ?

练习 3:编写统计函数

def stats(*numbers):
    """接收任意数量的数字,返回 (最小值, 最大值, 平均值)"""
    if not numbers:
        return None
    return min(numbers), max(numbers), sum(numbers) / len(numbers)

print(stats(1, 2, 3, 4, 5))   # (1, 5, 3.0)
print(stats(10, 20))           # (10, 20, 15.0)

小结

函数是 Python 编程的基石。本篇覆盖了函数的基础知识:

| 知识点 | 关键内容 |
|:—|:—|:—|
| 定义与调用 | def 函数名(参数):,定义时不执行,调用时才执行 |
| return | 返回值 + 结束函数;没有 return 默认返回 None |
| 参数传递 | 不可变对象传值(不受影响),可变对象传引用(受影响) |
| 参数类型 | 必需参数、位置/关键字参数、默认参数、*args、**kwargs |
| 默认参数陷阱 | 默认值在定义时创建,可变默认值要用 None |
| 函数是一等公民 | 可以赋值给变量、作为参数、作为返回值 |

下一篇文章,我们将继续深入函数的高级话题——匿名函数(lambda)、封包与解包、命名空间与作用域、global 和 nonlocal——这些是理解 Python 函数进阶特性的关键。


本文是「Python从入门到数据分析」系列的第 7 篇,共 24 篇。关注我,不错过后续更新。

更多推荐