10 - 函数

10 - 函数

函数是编程里的"积木"------把一段有特定功能的代码打包起来,起个名字,以后需要的时候直接用名字调用。


为什么要用函数

假设你要在程序里算三次圆的面积。不用函数的话:

python 复制代码
r1 = 5
area1 = 3.14159 * r1 ** 2

r2 = 10
area2 = 3.14159 * r2 ** 2

r3 = 7.5
area3 = 3.14159 * r3 ** 2

每次都要写一遍公式,而且如果哪天要把精度改成 3.1415926,得改三处。

用函数:

python 复制代码
def circle_area(radius):
    return 3.14159 * radius ** 2

area1 = circle_area(5)
area2 = circle_area(10)
area3 = circle_area(7.5)

干净多了。改精度只需要改函数里一处。


定义和调用

python 复制代码
def 函数名(参数):
    """文档字符串(可选)"""
    # 函数体
    return 返回值

一个简单的例子:

python 复制代码
def greet(name):
    """打招呼"""
    return f"你好,{name}!"

# 调用
message = greet("小明")
print(message)  # 你好,小明!

几个要点:

  • def 是定义函数的关键字
  • 参数放在括号里,可以有多个
  • return 返回结果。没有 return 的话,函数返回 None
  • 三引号的字符串叫文档字符串(docstring),用来描述函数干嘛的

参数

Python 函数的参数花样挺多的,这也是很多人学不明白的地方。别急,一个个来。

位置参数

最普通的参数,按顺序传:

python 复制代码
def add(a, b):
    return a + b

print(add(3, 5))  # 8

关键字参数

调用的时候指定参数名,顺序就无所谓了:

python 复制代码
def describe(name, age, city):
    print(f"{name},{age}岁,来自{city}")

describe(age=25, name="小明", city="北京")  # 顺序随便写

位置参数和关键字参数可以混用,但位置参数必须在前面

python 复制代码
describe("小明", age=25, city="北京")  # 对的
# describe(name="小明", 25, "北京")    # 报错!位置参数不能在关键字参数后面

默认参数

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

python 复制代码
def greet(name, greeting="你好"):
    return f"{greeting},{name}!"

print(greet("小明"))            # 你好,小明!
print(greet("John", "Hello"))   # Hello,John!

注意一个经典坑:默认参数不要用可变对象!

python 复制代码
# 错误示范
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]?!不是 [2]?
print(add_item(3))  # [1, 2, 3]?!

因为默认参数 [] 在函数定义时只创建了一次,后续调用都用的同一个列表。正确写法:

python 复制代码
def add_item(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

print(add_item(1))  # [1]
print(add_item(2))  # [2],正常了

可变参数(*args)

想传多少个参数都行:

python 复制代码
def total(*numbers):
    result = 0
    for n in numbers:
        result += n
    return result

print(total(1, 2, 3))        # 6
print(total(1, 2, 3, 4, 5))  # 15
print(total())                # 0

*numbers 会把所有传入的参数打包成一个元组。

* 是个约定,numbers 这个名字可以随便起,但大家习惯用 args(arguments 的缩写):

python 复制代码
def total(*args):
    return sum(args)

可变关键字参数(**kwargs)

*args 类似,但接收的是关键字参数,打包成字典:

python 复制代码
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="小明", age=25, city="北京")
# name: 小明
# age: 25
# city: 北京

参数全组合

如果一个函数同时用了这几种参数,顺序必须是

python 复制代码
def func(pos_only, /, normal, *args, keyword_only, **kwargs):
    pass
  1. 仅限位置参数(/ 前面的,不常用)
  2. 普通参数
  3. *args
  4. 仅限关键字参数(*args 后面的)
  5. **kwargs

平时写不会这么复杂,了解顺序就行。最常见的组合:

python 复制代码
def func(a, b, *args, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

func(1, 2, 3, 4, name="小明")
# a=1, b=2
# args=(3, 4)
# kwargs={'name': '小明'}

返回值

返回多个值

Python 函数可以"返回多个值"(其实是返回一个元组):

python 复制代码
def divide(a, b):
    quotient = a // b
    remainder = a % b
    return quotient, remainder

q, r = divide(17, 5)
print(f"商:{q},余数:{r}")  # 商:3,余数:2

没有返回值

没有 returnreturn 后面没东西,函数返回 None

python 复制代码
def say_hello(name):
    print(f"Hello, {name}")

result = say_hello("小明")  # Hello, 小明
print(result)               # None

提前返回

return 可以在函数的任何位置,遇到就立即退出:

python 复制代码
def check_age(age):
    if age < 0:
        return "年龄不能为负数"
    if age < 18:
        return "未成年"
    return "成年人"

变量的作用域

函数里面定义的变量,外面看不到:

python 复制代码
def my_func():
    x = 10  # 局部变量
    print(x)

my_func()
# print(x)  # NameError! x 在外面不存在

如果函数里的变量名跟外面一样呢?

python 复制代码
x = 10  # 全局变量

def my_func():
    x = 20  # 这是一个新的局部变量,跟外面的 x 没关系
    print(f"函数里:{x}")

my_func()       # 函数里:20
print(f"函数外:{x}")  # 函数外:10

如果要在函数里修改全局变量,需要用 global

python 复制代码
count = 0

def increment():
    global count
    count += 1

increment()
increment()
print(count)  # 2

不过说实话,global 能不用就不用。全局变量用多了代码会变得很混乱,很难追踪哪个函数在什么时候改了什么变量。更好的做法是用参数和返回值:

python 复制代码
def increment(count):
    return count + 1

count = 0
count = increment(count)
count = increment(count)
print(count)  # 2

lambda 表达式

lambda 是一种简写函数的方式,只能写一行:

python 复制代码
# 普通函数
def square(x):
    return x ** 2

# 等价的 lambda
square = lambda x: x ** 2

一般不用 lambda 定义变量(那不如直接写 def),它主要用在需要传函数做参数的地方:

python 复制代码
# 排序时自定义规则
students = [("小明", 85), ("小红", 92), ("小刚", 78)]
students.sort(key=lambda s: s[1])  # 按分数排序
print(students)  # [('小刚', 78), ('小明', 85), ('小红', 92)]

# 配合 map 和 filter
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

不过对于 mapfilter,用列表推导式更 Pythonic:

python 复制代码
squares = [x**2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]

闭包(Closure)

闭包是理解装饰器的前提,挺重要的一个概念。

先看个例子:

python 复制代码
def outer():
    x = 10

    def inner():
        print(x)  # inner 引用了 outer 的变量 x

    return inner

f = outer()
f()  # 10

这里 inner 函数"记住了"外层函数 outer 的变量 x,即使 outer 已经执行完了,x 还是可以被 inner 访问。这就是闭包

闭包 = 函数 + 它捕获的外部变量。

闭包的实际用途

工厂函数------生成定制化的函数:

python 复制代码
def make_multiplier(factor):
    def multiplier(x):
        return x * factor  # factor 被捕获了
    return multiplier

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

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

doubletriple 都是闭包,它们各自"记住"了不同的 factor 值。

保持状态(不用全局变量):

python 复制代码
def make_counter():
    count = 0
    def counter():
        nonlocal count  # 声明要修改外层变量
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

nonlocal 关键字告诉 Python "这个变量不是局部的,去外层找"。跟 global 类似,但 nonlocal 是找外层函数(不是全局)。

闭包和装饰器的关系

回头看看装饰器的结构:

python 复制代码
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

这不就是一个闭包吗?wrapper 捕获了外层的 func。所以装饰器本质上就是闭包的一个应用

查看闭包捕获了什么

python 复制代码
def make_adder(n):
    def adder(x):
        return x + n
    return adder

f = make_adder(10)
print(f.__closure__)           # 闭包对象
print(f.__closure__[0].cell_contents)  # 10(捕获的 n 的值)

递归(Recursion)

递归就是函数调用自己。有些问题用递归描述特别自然。

经典例子:阶乘

python 复制代码
def factorial(n):
    if n <= 1:       # 基线条件(什么时候停)
        return 1
    return n * factorial(n - 1)  # 递归调用

print(factorial(5))  # 120(5 × 4 × 3 × 2 × 1)

递归函数必须有两部分:

  1. 基线条件:什么时候停下来(不然就无限递归了)
  2. 递归步骤:把问题缩小,调用自己

经典例子:斐波那契数列

python 复制代码
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55

这个写法很直观,但效率很差(大量重复计算)。后面学了 @lru_cache 就能一行解决性能问题。

实际场景:遍历目录

python 复制代码
import os

def list_all_files(path):
    """递归列出所有文件"""
    for item in os.listdir(path):
        full_path = os.path.join(path, item)
        if os.path.isdir(full_path):
            list_all_files(full_path)  # 目录就递归进去
        else:
            print(full_path)

递归的限制

Python 默认最大递归深度是 1000:

python 复制代码
import sys
print(sys.getrecursionlimit())  # 1000

# 可以修改,但一般不建议
# sys.setrecursionlimit(5000)

如果递归太深会报 RecursionError。大多数情况下,如果你发现需要很深的递归,考虑改成用循环(迭代)来实现。


globals() 和 locals()

这两个函数让你查看当前作用域里的所有变量:

python 复制代码
x = 10
y = "hello"

print(globals())
# {..., 'x': 10, 'y': 'hello', ...}
# 返回一个字典,包含所有全局变量
python 复制代码
def my_func(a, b):
    c = a + b
    print(locals())
    # {'a': 3, 'b': 5, 'c': 8}
    # 返回一个字典,包含所有局部变量

my_func(3, 5)

有什么用?

说实话,日常开发中很少直接用它们。主要用途:

  1. 调试------快速查看当前有哪些变量
  2. 动态操作------根据字符串名字获取变量
python 复制代码
# 根据字符串名字调用函数
def hello():
    print("Hello!")

def goodbye():
    print("Goodbye!")

name = "hello"
globals()[name]()  # Hello!

不过这种动态操作一般有更好的方式(比如用字典做映射),不推荐滥用 globals()


eval() 和 exec()

这两个函数可以动态执行 Python 代码。很强大,也很危险。

eval()------执行表达式,返回结果

python 复制代码
result = eval("2 + 3 * 4")
print(result)  # 14

# 可以访问当前作用域的变量
x = 10
print(eval("x * 2"))  # 20

exec()------执行语句,不返回结果

python 复制代码
exec("y = 100")
print(y)  # 100

exec("""
def hello():
    print("动态定义的函数")
""")
hello()  # 动态定义的函数

安全风险

永远不要用 eval/exec 执行用户输入的内容!

python 复制代码
# 危险示范!
user_input = input("输入表达式:")
# 如果用户输入 __import__('os').system('rm -rf /')
# 你的文件就没了...
result = eval(user_input)

这不是危言耸听,eval() 可以执行任何 Python 代码,包括删除文件、访问网络等。

那什么时候能用?只在你完全控制输入内容的时候。比如从配置文件里读取一个数学表达式,或者做一些 DSL(领域特定语言)的解析。


类型提示(Type Hints)

Python 3.5 开始支持类型提示,不影响运行,但能帮助你和编辑器理解代码:

python 复制代码
def greet(name: str, age: int) -> str:
    return f"你好 {name},你今年 {age} 岁"
  • name: str 表示 name 应该是字符串
  • -> str 表示返回值是字符串

Python 本身不会检查这些类型(写错了也不会报错),但 VS Code、mypy 这类工具会帮你检查:

python 复制代码
# mypy 或编辑器会提示这里类型不对
greet(123, "二十五")  # 参数类型反了

在大型项目中类型提示非常有用。初学阶段不强制写,但建议从一开始就有这个意识。

typing 模块详解

基本的类型提示直接用 intstr 这些就够了,但复杂场景需要 typing 模块:

python 复制代码
from typing import List, Dict, Tuple, Set, Optional, Union

# 列表里装什么类型
def process(items: List[str]) -> List[int]:
    return [len(s) for s in items]

# 字典的键值类型
def get_config() -> Dict[str, int]:
    return {"timeout": 30, "retries": 3}

# 元组每个位置的类型
def get_point() -> Tuple[float, float]:
    return (3.0, 4.0)

# 可能是 None
def find_user(name: str) -> Optional[dict]:
    # Optional[dict] 等价于 dict | None(Python 3.10+)
    ...

# 多种类型之一
def parse_value(s: str) -> Union[int, float, str]:
    # Union[int, float, str] 等价于 int | float | str(Python 3.10+)
    ...

Python 3.10+ 可以用更简洁的语法:

python 复制代码
# 不用 import,直接用 | 语法
def find_user(name: str) -> dict | None:
    ...

def parse_value(s: str) -> int | float | str:
    ...

# 列表等也直接用内置类型
def process(items: list[str]) -> list[int]:
    ...

def get_config() -> dict[str, int]:
    ...

类型别名

类型太复杂的时候,可以起个别名:

python 复制代码
from typing import TypeAlias  # Python 3.10+

# 或者直接用等号(3.10 之前也行)
UserId = int
UserDict = dict[str, str]
Matrix = list[list[float]]

def get_user(user_id: UserId) -> UserDict:
    ...

Callable------函数类型

python 复制代码
from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

# Callable[[int, int], int] 表示:接收两个 int 参数,返回 int 的函数
apply(lambda x, y: x + y, 3, 5)  # 8

docstring 规范

前面简单提过 docstring(文档字符串),这里讲讲规范写法。好的 docstring 能让别人(和未来的你自己)快速理解函数的用途。

Google 风格(推荐)

最流行的 docstring 风格,可读性好:

python 复制代码
def calculate_bmi(weight: float, height: float) -> float:
    """计算 BMI(身体质量指数)。

    Args:
        weight: 体重,单位千克,必须大于 0。
        height: 身高,单位米,必须大于 0。

    Returns:
        BMI 值,保留一位小数。

    Raises:
        ValueError: 当 weight 或 height 小于等于 0 时抛出。

    Examples:
        >>> calculate_bmi(70, 1.75)
        22.9
    """
    if weight <= 0 or height <= 0:
        raise ValueError("体重和身高必须大于 0")
    return round(weight / height ** 2, 1)

关键部分

部分 说明
第一行 简要描述函数做什么(一句话)
Args 每个参数的含义
Returns 返回值的含义
Raises 可能抛出的异常
Examples 使用示例

不是每个部分都要写,简单函数写一行就够了:

python 复制代码
def is_even(n: int) -> bool:
    """判断一个数是否为偶数。"""
    return n % 2 == 0

用 help() 查看文档

写了 docstring 之后,别人可以用 help() 查看:

python 复制代码
help(calculate_bmi)
# 会显示完整的 docstring

VS Code 里鼠标悬停在函数名上也能看到。这也是写好 docstring 的动力之一------方便别人(和自己)使用。


一个综合例子

写一个密码强度检测器:

python 复制代码
def check_password(password: str) -> dict:
    """检查密码强度,返回评估结果"""
    score = 0
    issues = []

    # 长度检查
    if len(password) < 8:
        issues.append("密码太短,至少 8 位")
    else:
        score += 1

    # 大写字母检查
    if not any(c.isupper() for c in password):
        issues.append("建议包含大写字母")
    else:
        score += 1

    # 小写字母检查
    if not any(c.islower() for c in password):
        issues.append("建议包含小写字母")
    else:
        score += 1

    # 数字检查
    if not any(c.isdigit() for c in password):
        issues.append("建议包含数字")
    else:
        score += 1

    # 特殊字符检查
    special = "!@#$%^&*"
    if not any(c in special for c in password):
        issues.append("建议包含特殊字符")
    else:
        score += 1

    # 评级
    levels = {5: "很强", 4: "较强", 3: "一般", 2: "较弱", 1: "很弱", 0: "极弱"}
    
    return {
        "score": score,
        "level": levels.get(score, "极弱"),
        "issues": issues
    }


# 测试
result = check_password("Hello123!")
print(f"得分:{result['score']}/5")
print(f"强度:{result['level']}")
if result["issues"]:
    print("建议:")
    for issue in result["issues"]:
        print(f"  - {issue}")

这个例子用到了函数的参数、返回值(字典)、类型提示,还有 any() 这个内置函数(后面会讲)。


本章小结

  • def 定义函数,return 返回值,没有 return 返回 None
  • 参数类型:位置参数、关键字参数、默认参数、*args**kwargs
  • 默认参数不要用可变对象(用 None 代替)
  • 函数内的变量是局部的,改全局变量需要 global(但尽量避免)
  • lambda 是一行函数的简写,主要用在传参数的场景
  • 闭包 = 函数 + 捕获的外部变量,装饰器的底层原理
  • 递归 = 函数调用自己,需要基线条件防止无限递归
  • globals()locals() 查看当前作用域的变量
  • eval() / exec() 动态执行代码,千万不要对用户输入使用
  • 类型提示用 typing 模块处理复杂类型(ListOptionalUnion 等)
  • docstring 用 Google 风格写,help() 可以查看

面试题

Q1:*args**kwargs 分别是什么?
点击查看答案

  • *args:收集所有多余的位置参数,打包成元组
  • **kwargs:收集所有多余的关键字参数,打包成字典
python 复制代码
def func(*args, **kwargs):
    print(args)    # 元组
    print(kwargs)  # 字典

func(1, 2, 3, name="小明")
# (1, 2, 3)
# {'name': '小明'}

*** 也可以用于解包:

python 复制代码
args = [1, 2, 3]
func(*args)  # 把列表解包成位置参数

Q2:为什么默认参数不能用可变对象?
点击查看答案

因为默认参数在函数定义时只创建一次,不是每次调用都创建。如果默认参数是可变对象(列表、字典等),所有调用共享同一个对象,上一次调用的修改会影响下一次。

python 复制代码
def f(lst=[]):
    lst.append(1)
    return lst
f()  # [1]
f()  # [1, 1],不是 [1]!

正确做法:默认值设为 None,函数内创建新对象。

python 复制代码
def f(lst=None):
    if lst is None:
        lst = []
    lst.append(1)
    return lst

Q3:Python 中函数是一等公民(first-class citizen),这是什么意思?
点击查看答案

一等公民意味着函数可以像普通变量一样被操作:

  1. 赋值给变量f = print
  2. 作为参数传递sorted(items, key=len)
  3. 作为返回值:函数可以返回另一个函数
  4. 存储在数据结构中[func1, func2, func3]

这是函数式编程的基础,也是装饰器、回调函数等高级特性的前提。

Q4:lambda 和普通函数有什么区别?什么时候用 lambda?
点击查看答案

lambda def
表达式数量 只能有一个表达式 可以有多条语句
返回值 自动返回表达式的值 需要 return
名字 匿名(没有名字) 有名字
文档 不能有 docstring 可以有
可读性 简单场景更简洁 复杂逻辑更清晰

用 lambda 的场景:需要一个简单的函数做参数(如 sort(key=...)mapfilter),且逻辑简单到一行就能写完。

其他情况一律用 def。不要用 lambda 赋值给变量(square = lambda x: x**2),这种写法没有好处。


相关推荐
APIshop1 小时前
Python 获取 1688 商品采集 API 接口 | 工厂货源自动化对接商品信息 | 无需选品
运维·python·自动化
z落落1 小时前
C#String字符串
开发语言·c#·php
猫头虎-前端技术2 小时前
JS 作用域与闭包:从变量提升到闭包陷阱的超详细解析
开发语言·javascript·云计算·bootstrap·ecmascript·openstack·perl
charlee442 小时前
《GIS基础原理与技术实践》配套案例(Python版)
python·conda·numpy·gis·环境配置
枫叶林FYL2 小时前
项目十:事件溯源仓储管理系统(WMS)仿真实现
开发语言·python
繁华落尽,倾城殇?2 小时前
[C++11] : atomic,nullptr,default/delete,enum class
开发语言·c++·c++11·nullptr·atomic·enum class·default/delete
01_ice3 小时前
C语言数据在内存中的存储
c语言·开发语言
代码村新手3 小时前
C++-二叉搜索树
开发语言·c++
渣渣xiong4 小时前
从零开始:前端转型AI agent直到就业第五十七天-第五十八天
前端·人工智能·python