Python 函数

1. 函数基础:定义与抽象

函数不是什么玄乎的东西——它就是一段封装好、可重复调用、解决单一问题的代码块。从程序员的视角看,函数最核心的价值是抽象:比如不用每次都写“从1加到100的循环”,可以直接有sum();不用每次想三角函数,直接掏math.sin()就行。

1.1 标准定义语法

Python 用 def 关键字起手定义函数,现在的工业级代码都推荐加文档字符串(Docstring)类型提示(Type Hints)——前者给调用者看“这函数干嘛”,后者给编辑器和静态检查工具看“传啥、返啥”,提升可维护性。

import math

def area_of_circle(radius: float) -> float:
    """计算给定半径的圆的面积。
    
    参数:
        radius: 圆的半径,必须是正数
        
    返回:
        计算出的圆的面积,保留无限精度的float
    """
    return math.pi * radius ** 2

1.2 关于返回值的细节

  • 单一返回:用 return 直接扔结果就行。
  • 多个返回:写 return x, y 就够——Python 会自动把它们包成元组(tuple),调用时可以用多变量接收或者索引。
  • 空占位:暂时没想好逻辑?用 pass 占坑;没写 return 或者 return 后面空着?Python 默认返回 None

2. 函数调用:让代码块“动起来”

定义完函数,它只是躺在内存里的“静态模板”,必须通过调用才能执行。

2.1 三种基础调用姿势

# 姿势1:直接传字面量
print(area_of_circle(5.0)) 

# 姿势2:传变量,可读性更高
r = 10.5
s = area_of_circle(r)
print(f"半径为{r}的面积是: {s:.2f}")

# 姿势3:直接嵌在表达式里
total_area = area_of_circle(2) + area_of_circle(3)

2.2 重要的参数传递机制

别光记参数名字,搞懂Python怎么把值丢给函数很关键:

  1. 调用方式分两种
    • 位置调用f(a, b),严格按函数定义的顺序传值。
    • 关键字调用area_of_circle(radius=5.0),清晰明了,参数多的时候还能乱序。
  2. 内存层面的本质:Python 是「对象引用传递」,不是纯「值传递」或纯「地址传递」:
    • 不可变对象(数字、字符串、元组):函数内部的赋值/修改只会创建新对象,不会碰外部变量。
    • 可变对象(列表、字典、集合):函数内部的原地修改(比如 .append().update())会同步影响外部变量!

2.3 快速动态调用(参数解包)

如果你的参数已经存在列表/元组、字典里,不用一个个拆,用 *** 就行:

# 解包列表/元组为位置参数
params = [5.0]
print(area_of_circle(*params))

# 解包字典为关键字参数
def greet(name, age):
    print(f"你好,我叫{name},今年{age}岁")
user_info = {"name": "Alice", "age": 25}
greet(**user_info)

3. 深入理解参数系统:规则让你更自由

Python 的参数系统非常灵活,但定义顺序必须严格遵守,不然会报错。下面按优先级整理成表格:

参数类型示例语法核心特点
位置参数def f(a, b)调用时必选,严格按顺序传。
默认参数def f(a, b=2)必须指向不可变对象(比如 None、整数、字符串),不然会有“默认值复用”的坑!
可变参数def f(*args)接收任意个位置参数,自动打包成元组。
命名关键字参数def f(*, city)*, 切断位置参数,强制必须用 city='北京' 这种键值对传,避免混淆。
关键字参数def f(**kw)接收任意个关键字参数,自动打包成字典,常用于扩展功能。

4. 递归函数:简化逻辑,但要注意边界

递归就是函数自己调用自己,核心思想是把复杂问题拆解成「结构完全相同但规模更小」的子问题

4.1 经典入门案例:阶乘

def factorial(n):
    # 终止条件!没有它会死循环到系统栈溢出
    if n == 1:
        return 1
    # 自己调用自己,每次n减1,向终止条件靠近
    return n * factorial(n - 1)

4.2 递归的三个黄金守则

  1. 必须有明确的终止条件:比如上面的 n == 1,不然Python默认的1000层递归限制(sys.getrecursionlimit())会触发。
  2. 每次递归都要向终止条件靠近:比如每次把n减1,不能越算越大。
  3. 合理权衡性能:递归代码虽然简洁,但每次调用都会消耗函数调用栈的空间,对性能敏感的场景(比如计算超级大的斐波那契数),建议用迭代(循环)代替。

4.3 现代Python优化:记忆化缓存

很多递归(比如斐波那契)会重复计算相同的子问题,Python 3.9+ 提供了超级简单的解决方案——直接加装饰器就行!

from functools import cache  # 3.9+专用,比旧版lru_cache()更简洁

@cache  # 自动把计算过的(n, 结果)存起来,下次直接拿
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 测试:不加缓存算fib(40)会卡,加了瞬间出结果
print(fibonacci(40))

5. 工业级最佳实践与规范

写好函数不仅要能跑,还要好读、好改、好复用

  1. 单一职责原则:一个函数只解决一个问题,比如不能同时“计算面积并打印并保存到文件”。
  2. 命名规范:全小写加下划线,动词开头(比如 calculate_taxvalidate_input)。
  3. 参数验证:在函数开头用 isinstance() 检查类型,用条件语句检查范围,不对就抛明确的异常(比如 TypeErrorValueError)。
  4. 尽量避免副作用:别在函数内部修改全局变量,别原地修改传入的可变对象——如果必须改,要么文档里写清楚,要么返回一个新的修改后的对象。

6. 综合实战:一元二次方程求解器

把前面的知识点串起来,写一个符合规范的工具函数:

import math
from typing import Tuple, Union

def solve_quadratic(
    a: Union[int, float],
    b: Union[int, float],
    c: Union[int, float]
) -> Tuple[float, float]:
    """求解标准形式的一元二次方程(仅实数解)。
    
    参数:
        a: 二次项系数,不能为0
        b: 一次项系数
        c: 常数项
        
    返回:
        两个实数解组成的元组,按从小到大排序
    """
    # 参数验证:检查类型
    if not all(isinstance(x, (int, float)) for x in [a, b, c]):
        raise TypeError("所有参数必须是整数或浮点数")
    # 参数验证:检查二次项系数
    if a == 0:
        raise ValueError("二次项系数a不能为0,否则不是一元二次方程")
    # 计算判别式
    discriminant = b**2 - 4*a*c
    # 参数验证:检查是否有实数解
    if discriminant < 0:
        raise ValueError("方程没有实数解")
    # 计算根
    sqrt_d = math.sqrt(discriminant)
    root1 = (-b + sqrt_d) / (2*a)
    root2 = (-b - sqrt_d) / (2*a)
    # 按从小到大排序返回
    return (root2, root1) if root2 < root1 else (root1, root2)

# 调用测试
try:
    print(f"方程2x²+3x+1=0的解为: {solve_quadratic(2, 3, 1)}")
except Exception as e:
    print(f"出错了: {e}")

7. 总结

函数是Python编程的核心基石:基础的定义调用能让代码不重复,灵活的参数系统能适配各种场景,递归能简化复杂逻辑但要注意边界,记忆化缓存能提升重复计算的效率。最后别忘了遵循最佳实践——这样你的代码才能从“能跑”变成“好用”。