1. 为什么我们需要函数?因为我“懒”

兄弟们,如果你发现自己一直在复制、粘贴同一段代码,只是改了改里面的几个数字,那你就得小心了。这在编程里叫 “Code Smell”(代码坏味道)

函数(Function)的本质就是:把一段逻辑打包,给它起个好听的名字,以后想用的时候直接喊名字就行了。

一个优秀的架构师,不是看他写了多少行代码,而是看他把复杂的逻辑拆分成了多少个清爽、可复用的函数。今天咱们就来拆解 Python 函数的那些硬核玩法。


2. 基础:写一个合格的函数

2.1 别再写“三无”函数了

一个好的函数应该有:清晰的名字、明确的参数、确定的返回值。

def calculate_tax(salary: float) -> float:
    """
    计算个人所得税(这是一个简单的模拟逻辑)
    """
    if salary <= 5000:
        return 0.0
    return (salary - 5000) * 0.2

# 调用
my_tax = calculate_tax(8000)
print(f"我要交的税:¥{my_tax}")

老司机建议:看到上面的 : float-> float 没?这叫 Type Hints(类型提示)。虽然 Python 不强制要求,但在团队开发里,这能让你的同事少掉一半头发,因为他们一眼就能看出该传什么、会返回什么。

2.2 函数作为一等公民的实现原理

在 Python 中,函数是一等公民(First-Class Citizen)。这意味着函数可以像普通变量一样被传递、赋值、存储在数据结构中,甚至作为其他函数的返回值。

实现原理:Python 中的一切都是对象,函数也不例外。当你用 def 定义一个函数时,Python 实际上创建了一个函数对象(function object),并将其绑定到函数名上。

def greet(name):
    return f"Hello, {name}!"

# 函数可以赋值给变量
say_hello = greet
print(say_hello("张三"))  # Hello, 张三!

# 函数可以存储在数据结构中
func_list = [greet, str.upper, len]
print(func_list[0]("李四"))  # Hello, 李四!

# 查看函数对象的类型
print(type(greet))  # <class 'function'>
print(greet.__name__)  # greet

这种设计让 Python 具备了强大的函数式编程能力。你可以把函数当作"数据"来操作,实现高度抽象的代码逻辑。


3. 进阶:灵活的参数传递

有时候你不知道用户会传多少个参数进来,这时候 Python 的“黑魔法”就派上用场了。

3.1 *args:打包成元组

如果你要写一个求和函数,用户可能传 2 个数,也可能传 100 个数:

def super_sum(*args):
    # args 此时是一个元组 (1, 2, 3, ...)
    return sum(args)

print(super_sum(1, 2, 3, 4, 5))

3.2 **kwargs:打包成字典

这在配置系统里超级好用,它能接收任何“键值对”:

def save_user_profile(**kwargs):
    # kwargs 此时是一个字典 {"name": "张三", "age": 18}
    for key, value in kwargs.items():
        print(f"正在保存 - {key}: {value}")

save_user_profile(name="张三", age=18, city="北京")

4. 装饰器 (Decorator):给代码穿上“外挂”

这是 Python 最优雅、也是新手最头疼的特性。
直白解释:你想给几个函数都加个功能(比如记录运行时间),但你又不想改动这些函数的源码。装饰器就是那个“外挂”。

4.1 实战:写个性能监控外挂

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time() # 记录开始时间
        result = func(*args, **kwargs) # 执行原函数
        end_time = time.time() # 记录结束时间
        print(f"--- 函数【{func.__name__}】运行耗时: {end_time - start_time:.4f}s ---")
        return result
    return wrapper

@timer_decorator
def complex_logic():
    print("正在执行复杂的逻辑...")
    time.sleep(1.5)

complex_logic()

看到那个 @timer_decorator 没?这就叫**“无侵入式编程”**。你不需要动 complex_logic 里的任何一行代码,就让它拥有了计时功能。

4.2 装饰器的执行时机详解

很多人误以为装饰器是在被装饰函数运行时才执行的。实际上,装饰器在**模块导入时(定义时)**就已经执行了!

def decorator(func):
    print(f"装饰器执行了!被装饰的函数是: {func.__name__}")
    return func

@decorator
def my_func():
    print("函数运行了")

# 输出:装饰器执行了!被装饰的函数是: my_func

# 调用函数时,装饰器不会再执行
my_func()  # 只输出:函数运行了

执行流程解析

  1. Python 解释器读到 @decorator 时,会立即执行 decorator(my_func)
  2. 装饰器函数接收被装饰函数作为参数,并返回一个(通常是包装后的)函数
  3. 被装饰的函数名 my_func 被重新绑定到装饰器返回的函数上

4.3 装饰器的闭包机制

装饰器的核心在于闭包(Closure)。闭包是指一个函数记住了它被定义时的环境(即外部函数的局部变量),即使外部函数已经执行完毕。

def outer_decorator(msg):
    # 这是外部函数的局部变量
    prefix = f"[{msg}]"
  
    def decorator(func):
        # wrapper 函数形成了闭包,记住了 prefix
        def wrapper(*args, **kwargs):
            print(f"{prefix} 开始执行 {func.__name__}")
            result = func(*args, **kwargs)
            print(f"{prefix} 结束执行 {func.__name__}")
            return result
        return wrapper
    return decorator

@outer_decorator("INFO")
def say_hi():
    print("Hi!")

say_hi()
# 输出:
# [INFO] 开始执行 say_hi
# Hi!
# [INFO] 结束执行 say_hi

闭包的关键要素

  • 必须有一个嵌套函数(内部函数引用外部函数的变量)
  • 内部函数必须被返回(或赋值给外部变量)
  • 外部函数的局部变量被内部函数"记住",形成了闭包

4.4 使用 functools.wraps 保留元数据

使用装饰器后,原函数的元数据(如函数名、文档字符串)会丢失:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """这是 wrapper 的文档"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """这是 greet 的文档"""
    print("Hello")

print(greet.__name__)  # wrapper(原函数名丢失了!)
print(greet.__doc__)   # 这是 wrapper 的文档(原文档丢失了!)

解决方案:使用 functools.wraps

from functools import wraps

def my_decorator(func):
    @wraps(func)  # 关键:保留原函数的元数据
    def wrapper(*args, **kwargs):
        """这是 wrapper 的文档"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """这是 greet 的文档"""
    print("Hello")

print(greet.__name__)  # greet(正确!)
print(greet.__doc__)   # 这是 greet 的文档(正确!)

5. 作用域与闭包:谁才是"话事人"?

Python 遵循 LEGB 原则(Local -> Enclosing -> Global -> Built-in)。理解这个规则,你就能明白为什么有时候变量"找不到",或者为什么修改全局变量会报错。

5.0 LEGB规则详解

LEGB 是 Python 查找变量名的搜索顺序

层级 英文 中文 说明
L Local 局部作用域 当前函数内部定义的变量
E Enclosing 嵌套作用域 外层嵌套函数的局部变量
G Global 全局作用域 模块级别定义的变量
B Built-in 内置作用域 Python 内置的变量(如 len, print

搜索流程:当 Python 遇到一个变量名时,会按照 L -> E -> G -> B 的顺序查找,找到第一个匹配的即停止。

# 演示 LEGB 规则
x = "global"  # G: 全局作用域

def outer():
    x = "enclosing"  # E: 嵌套作用域
  
    def inner():
        x = "local"  # L: 局部作用域
        print(f"inner 中的 x: {x}")  # 找到 L 层的 "local"
  
    inner()
    print(f"outer 中的 x: {x}")  # 找到 E 层的 "enclosing"

outer()
print(f"全局中的 x: {x}")  # 找到 G 层的 "global"

# 输出:
# inner 中的 x: local
# outer 中的 x: enclosing
# 全局中的 x: global

重要规则

  • 读取变量:Python 会自动按 LEGB 顺序查找
  • 修改变量:默认只能修改 L(局部)层的变量
  • 要修改 E 层用 nonlocal,修改 G 层用 global

5.1 别乱碰全局变量!

count = 10

def my_func():
    global count # 必须声明,否则你改不了全局的 count
    count += 1
    print(f"函数内:{count}")

my_func()

老司机的忠告:在函数里用 global 是非常危险的行为。它会让你的程序变得难以预测。尽量通过参数传进去、通过返回值传出来,这才是“函数式编程”的精髓。

5.2 闭包 (Closure):让变量“长生不老”

有时候你希望一个函数运行完后,它里面的变量不要被销毁。

def make_counter():
    count = 0
    def counter():
        nonlocal count # 修改外部函数的局部变量
        count += 1
        return count
    return counter

c1 = make_counter()
print(c1()) # 1
print(c1()) # 2

这就是闭包。它让函数拥有了"记忆",这在写一些状态保持的工具时非常有用。

5.3 偏函数(Partial Function)

偏函数是指固定一个函数的部分参数,生成一个参数更少的新函数。这在需要反复调用某个函数、且部分参数固定不变时非常有用。

from functools import partial

# 原始函数:计算幂次
def power(base, exponent):
    return base ** exponent

# 创建偏函数:固定 exponent=2,专门用于平方
square = partial(power, exponent=2)
print(square(5))  # 25
print(square(10))  # 100

# 创建偏函数:固定 base=2,专门用于计算2的幂
# 注意:固定位置参数后,调用时需要使用关键字参数
power_of_2 = partial(power, 2)  # 固定第一个参数 base=2
print(power_of_2(3))  # 8 (2的3次方)
print(power_of_2(10))  # 1024 (2的10次方)

实际应用场景

from functools import partial

# 场景:创建专用的比较函数
# 创建偏函数:固定 min 的默认值
at_least_10 = partial(max, 10)  # 确保结果至少为10

# 批量处理数据,确保所有值不小于10
values = [5, 15, 8, 20, 3]
adjusted = [at_least_10(v) for v in values]
print(adjusted)  # [10, 15, 10, 20, 10]

5.4 柯里化(Currying)

柯里化是把一个接受多个参数的函数,转换成一系列只接受单个参数的函数。每个函数返回下一个接收单个参数的函数,直到所有参数都被收集完毕。

# 普通函数
def add(a, b, c):
    return a + b + c

# 柯里化版本
def curried_add(a):
    def inner_b(b):
        def inner_c(c):
            return a + b + c
        return inner_c
    return inner_b

# 使用柯里化函数
result = curried_add(1)(2)(3)
print(result)  # 6

# 也可以分步调用
add_1 = curried_add(1)      # 固定第一个参数
add_1_2 = add_1(2)          # 固定第二个参数
result = add_1_2(3)         # 传入第三个参数
print(result)  # 6

柯里化的优势

  • 延迟计算:可以分步传递参数,在最后一步才执行计算
  • 参数复用:可以预先固定某些参数,生成专用函数
  • 函数组合:便于构建复杂的函数管道

使用装饰器实现自动柯里化

from functools import partial

def curry(func):
    """简单的柯里化装饰器"""
    def curried(*args):
        if len(args) >= func.__code__.co_argcount:
            return func(*args)
        return partial(curried, *args)
    return curried

@curry
def multiply(a, b, c):
    return a * b * c

# 现在可以像使用柯里化函数一样调用
print(multiply(2, 3, 4))    # 24
print(multiply(2)(3)(4))    # 24

# 或者分步调用
double = multiply(2)
triple = double(3)
print(triple(4))  # 24

6. 递归:自己调用自己,是艺术也是坑

递归就像是《盗梦空间》,你进入一层梦境,处理完再出来。

def factorial(n):
    """ 计算阶乘:n! """
    if n == 1: # 递归出口,没这个就死循环了
        return 1
    return n * factorial(n - 1)

print(factorial(5)) # 120

避坑指南:Python 的递归深度是有限制的(默认 1000 层)。如果你递归太深,会报 RecursionError。所以,能用循环解决的,尽量别用递归。


6.5 函数式编程范式详解

**函数式编程(Functional Programming)**是一种编程范式,它将计算视为数学函数的求值,避免使用可变状态和循环。Python 虽然不是纯函数式语言,但提供了丰富的函数式编程工具。

6.5.1 纯函数(Pure Function)

纯函数是指相同的输入永远产生相同的输出,且没有副作用(不修改外部状态)的函数。

# 纯函数:没有副作用,输出只依赖输入
def pure_add(a, b):
    return a + b

# 非纯函数:有副作用(修改了外部变量)
total = 0
def impure_add(a):
    global total
    total += a  # 副作用:修改了外部状态
    return total

# 非纯函数:输出依赖外部状态
import random
def impure_random():
    return random.random()  # 每次调用结果不同

纯函数的优势

  • 可测试性:给定输入,输出确定,易于单元测试
  • 可缓存性:结果可以被缓存(Memoization),提高性能
  • 并行安全:没有共享状态,可以安全地在多线程/多进程中运行

6.5.2 不可变性(Immutability)

函数式编程鼓励使用不可变数据,避免修改原有数据,而是创建新的数据。

# 命令式风格(修改原数据)
def add_item_imperative(items, item):
    items.append(item)  # 修改了传入的列表
    return items

# 函数式风格(创建新数据)
def add_item_functional(items, item):
    return items + [item]  # 创建新列表,原列表不变

# 测试
original = [1, 2, 3]

# 命令式:原数据被修改
result1 = add_item_imperative(original.copy(), 4)
print(result1)  # [1, 2, 3, 4]

# 函数式:原数据保持不变
original = [1, 2, 3]
result2 = add_item_functional(original, 4)
print(original)  # [1, 2, 3] - 原数据未被修改
print(result2)   # [1, 2, 3, 4]

6.5.3 高阶函数(Higher-Order Function)

高阶函数是指接收函数作为参数返回函数作为结果的函数。

# 接收函数作为参数
def apply_operation(numbers, operation):
    """对列表中的每个元素应用指定的操作"""
    return [operation(n) for n in numbers]

# 定义一些操作函数
def square(x):
    return x ** 2

def double(x):
    return x * 2

numbers = [1, 2, 3, 4, 5]

# 传递不同的函数,实现不同的行为
print(apply_operation(numbers, square))   # [1, 4, 9, 16, 25]
print(apply_operation(numbers, double))   # [2, 4, 6, 8, 10]

Python 内置的高阶函数:map(), filter(), reduce(), sorted() 等。

6.5.4 函数组合(Function Composition)

函数组合是将多个简单的函数组合成一个复杂的函数,一个函数的输出作为下一个函数的输入。

from functools import reduce

def compose(*functions):
    """组合多个函数,从右向左执行"""
    def composed(arg):
        # reduce 从右向左应用函数
        return reduce(lambda v, f: f(v), reversed(functions), arg)
    return composed

# 定义简单的处理函数
def add_one(x):
    return x + 1

def double(x):
    return x * 2

def to_string(x):
    return f"结果是: {x}"

# 组合函数:先加1,再翻倍,最后转字符串
process = compose(to_string, double, add_one)

print(process(5))  # 结果是: 12 ((5+1)*2 = 12)

6.5.5 惰性求值与生成器

惰性求值是指只在需要时才计算值,这在处理大数据集时非常有用。

# 惰性求值:生成器函数
def fibonacci():
    """生成无限斐波那契数列"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 只取前10个,不会无限计算
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]
print(first_10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# 惰性管道处理大数据
def read_large_file(file_path):
    """惰性读取大文件,一次只读一行"""
    with open(file_path, 'r') as f:
        for line in f:
            yield line.strip()

def filter_lines(lines, keyword):
    """惰性过滤包含关键字的行"""
    for line in lines:
        if keyword in line:
            yield line

def count_words(lines):
    """惰性计算每行的单词数"""
    for line in lines:
        yield len(line.split())

# 构建惰性处理管道(不会立即执行)
# lines = read_large_file('huge_file.txt')
# filtered = filter_lines(lines, 'Python')
# word_counts = count_words(filtered)
# 
# 只在遍历时才真正处理数据
# for count in word_counts:
#     print(count)

6.5.6 记忆化(Memoization)

记忆化是一种优化技术,缓存函数的返回结果,避免重复计算。

from functools import lru_cache

# 使用内置装饰器实现记忆化
@lru_cache(maxsize=128)
def fibonacci(n):
    """计算斐波那契数列,带缓存"""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 第一次计算较慢,后续从缓存读取
print(fibonacci(100))  # 瞬间完成,因为缓存了中间结果

# 查看缓存信息
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

6.5.7 Python 函数式编程工具总结

工具 作用 示例
map() 对每个元素应用函数 map(func, iterable)
filter() 按条件过滤元素 filter(pred, iterable)
reduce() 累积计算 reduce(func, iterable)
lambda 匿名函数 lambda x: x**2
sorted() 排序 sorted(iterable, key=func)
zip() 合并多个序列 zip(list1, list2)
enumerate() 带索引遍历 enumerate(iterable)
any() / all() 逻辑判断 any(pred(x) for x in iterable)
functools.partial 偏函数 partial(func, arg=val)
functools.lru_cache 缓存 @lru_cache()

7. 模块化:别把所有鸡蛋放在一个篮子里

当你的项目超过 200 行时,就该考虑分文件了。

6.1 if __name__ == "__main__": 是干啥的?

你肯定在很多代码底部见过这行。它的意思是:“只有当你直接运行这个文件时,才执行下面的代码;如果你是作为模块被别人 import 的,那就别乱动。”

6.2 目录结构建议

my_project/
├── main.py          # 程序入口
├── utils/           # 工具函数包
│   ├── __init__.py  # 声明这是一个包
│   ├── logger.py    # 日志工具
│   └── calc.py      # 计算工具
└── .venv/           # 虚拟环境

7. 避坑小贴士(老司机的叮嘱)

  1. 函数别写太长:一个函数如果超过 50 行,大概率说明你写得太复杂了,该拆分了。“一个函数只干一件事”,这是金科玉律。
  2. Lambda 表达式别滥用:一行小逻辑可以用 Lambda,逻辑稍微复杂点,求你正经写个 def。代码是写给人看的,不是写给机器看的。
  3. 闭包的坑:闭包虽然高级,但容易造成内存泄漏或者逻辑混乱。新手建议先玩明白装饰器。

8. 实战演练:巩固你的内功

纸上得来终觉浅,绝知此事要躬行。这里有几个实战小练习,看看你能搞定几个?

题目 1:编写一个“带重试机制”的装饰器

需求
写一个装饰器 retry,它可以接收一个参数 max_retries(最大重试次数)。
如果被装饰的函数抛出异常,就自动重试,直到超过最大次数再抛出异常。

点击查看参考答案
import time
import random

def retry(max_retries=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {i+1} 次尝试失败:{e},正在重试...")
                    time.sleep(1) # 稍微歇会儿
            # 最后一次还没成功,那就真报错吧
            print("重试次数耗尽,任务失败。")
            raise Exception("Function failed after max retries")
        return wrapper
    return decorator

@retry(max_retries=3)
def unstable_network_request():
    # 模拟 70% 概率失败
    if random.random() < 0.7:
        raise ValueError("网络连接超时")
    return "数据下载成功!"

# 测试
try:
    print(unstable_network_request())
except Exception as e:
    print(f"最终结果:{e}")

题目 2:闭包实战——简易存钱罐

需求
编写一个函数 piggy_bank,它返回一个内部函数。
每次调用这个内部函数存钱,它都会累加金额,并打印当前余额。
如果不传金额,默认返回当前余额。

点击查看参考答案
def piggy_bank():
    balance = 0 # 自由变量,藏在闭包里
  
    def operation(amount=0):
        nonlocal balance
        if amount > 0:
            balance += amount
            print(f"存入 {amount} 元,当前余额:{balance} 元")
        elif amount < 0:
            print("只准存不准取!")
        else:
            print(f"当前余额:{balance} 元")
        return balance
      
    return operation

# 测试
my_bank = piggy_bank()
my_bank(100) # 存入 100
my_bank(50)  # 存入 50
my_bank()    # 查询余额

题目 3:函数式编程——筛选质数

需求
使用 filter 函数和 lambda 表达式(或者自定义函数),从一个列表中筛选出所有的质数。
给定列表:numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]

点击查看参考答案
def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]

# 使用 filter
# filter(函数, 可迭代对象):保留函数返回 True 的元素
primes = list(filter(is_prime, numbers))
print(f"质数列表:{primes}")

# 或者用列表推导式(更 Pythonic)
primes_v2 = [x for x in numbers if is_prime(x)]
print(f"推导式结果:{primes_v2}")

系列索引


写在最后
函数是 Python 进阶的分水岭。如果你能玩转装饰器和模块化,那你已经具备了开发中型项目的能力。
别光看不练,试着把上一讲的学生管理系统,重构成“函数版本”:增加、删除、查询都写成独立的函数。
觉得有收获的话,点赞、收藏 走一波!咱们下一讲聊聊 Python 里的“降龙十八掌”——面向对象。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐