57_Python函数定义与参数

Python函数定义与参数

文章目录

前言

函数是编程中最基本的抽象单元------它将一段逻辑封装起来,赋予一个有意义的名字,从而实现代码的复用和模块化。Python的函数设计简洁而不失强大,支持位置参数、默认参数、可变参数(*args)、关键字可变参数(**kwargs)等多种参数类型,配合lambda匿名函数和函数式编程风格,能让你用优雅的方式解决复杂问题。

但Python函数也有几个经典陷阱,是所有中级面试几乎必考的内容:默认参数不能使用可变对象(如空列表),否则会产生"记忆"效应;*args**kwargs 的参数顺序有严格规定;lambda虽然方便但可读性有限。本文将全面梳理这些要点,帮你从"会写函数"进阶到"写好函数",让代码兼具灵活性和健壮性。


一、函数的定义与调用

1.1 基本语法

python 复制代码
def 函数名(参数列表):
    """文档字符串------描述函数功能"""
    # 函数体
    return 返回值  # 可省略,默认返回None

最简单的示例

python 复制代码
def greet():
    """打印问候语"""
    print("Hello, World!")

# 调用函数
greet()  # Hello, World!

# 查看文档字符串
print(greet.__doc__)  # 打印问候语

1.2 带返回值的函数

python 复制代码
def add(a, b):
    """返回两个数的和"""
    return a + b

result = add(3, 5)
print(result)  # 8

# 返回多个值(实际上是返回元组)
def min_max(data):
    return min(data), max(data)

mn, mx = min_max([3, 1, 4, 1, 5, 9])
print(mn, mx)  # 1 9

# 没有return语句的函数返回None
def do_nothing():
    pass

print(do_nothing())  # None

二、参数类型详解

Python函数的参数系统非常灵活,支持多种参数类型。

2.1 位置参数(Positional Arguments)

最常见的参数形式,调用时按位置一一对应:

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

describe_person("Alice", 25, "上海")  # 必须按位置传入
# 输出:Alice,25岁,来自上海

2.2 默认参数(Default Arguments)

给参数设置默认值,调用时可以省略:

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

greet("Alice")                        # 你好,Alice!
greet("Bob", greeting="早上好")       # 早上好,Bob!

# 默认参数的陷阱:默认值在函数定义时计算,只计算一次!
def add_item(item, container=[]):
    container.append(item)
    return container

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]  ------ 这可能是意料之外的!
print(add_item(3))  # [1, 2, 3]

# 正确做法:使用None作为哨兵值
def add_item_safe(item, container=None):
    if container is None:
        container = []
    container.append(item)
    return container

print(add_item_safe(1))  # [1]
print(add_item_safe(2))  # [2]  ------ 这才是期望的行为
print(add_item_safe(3))  # [3]

黄金法则:默认参数的值绝不要使用可变对象(列表、字典、集合等),应该用None并在函数体内初始化。

2.3 关键字参数(Keyword Arguments)

调用时通过参数名指定值,无需关心顺序:

python 复制代码
def create_user(name, age, email, phone="N/A"):
    return {
        "name": name,
        "age": age,
        "email": email,
        "phone": phone
    }

# 关键字参数可以打乱顺序
user = create_user(age=25, email="a@test.com", name="Alice")
print(user)  # {'name': 'Alice', 'age': 25, 'email': 'a@test.com', 'phone': 'N/A'}

# 混合使用:位置参数在前,关键字参数在后
user2 = create_user("Bob", age=30, email="b@test.com")

2.4 可变参数(*args)

使用 *args 接收任意数量的位置参数(打包为元组):

python 复制代码
def sum_all(*args):
    """计算任意数量数字的和"""
    total = 0
    for num in args:
        total += num
    return total

print(sum_all(1, 2, 3))            # 6
print(sum_all(1, 2, 3, 4, 5))     # 15
print(sum_all())                   # 0(没有参数时为空元组)

# *args 在参数列表中的位置(必须在位置参数之后,关键字参数之前)
def log(level, *messages):
    for msg in messages:
        print(f"[{level}] {msg}")

log("INFO", "连接成功", "数据加载完成", "服务就绪")

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

使用 **kwargs 接收任意数量的关键字参数(打包为字典):

python 复制代码
def build_html_tag(tag, **attributes):
    """构建HTML标签"""
    attr_str = " ".join(f'{k}="{v}"' for k, v in attributes.items())
    return f"<{tag} {attr_str}>" if attr_str else f"<{tag}>"

print(build_html_tag("input", type="text", id="username", placeholder="请输入用户名"))
# <input type="text" id="username" placeholder="请输入用户名">

print(build_html_tag("br"))
# <br>

2.6 参数顺序规则

Python函数参数的完整顺序是:

复制代码
def func(位置参数, *args, 关键字限定位置参数, 默认关键字参数, **kwargs)

示例

python 复制代码
def complex_func(a, b, *args, c=10, d=20, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"c={c}, d={d}")
    print(f"kwargs={kwargs}")

complex_func(1, 2, 3, 4, 5, c=100, e=30, f=40)
# a=1, b=2
# args=(3, 4, 5)
# c=100, d=20
# kwargs={'e': 30, 'f': 40}

三、强制关键字参数(Python 3.8+)

* 之后的参数只能通过关键字传入:

python 复制代码
def configure(host, port, *, use_ssl=False, timeout=30):
    """*后面的参数必须使用关键字传入"""
    return {
        "host": host,
        "port": port,
        "use_ssl": use_ssl,
        "timeout": timeout
    }

# 正确的调用
config = configure("localhost", 8000, use_ssl=True, timeout=60)
# configure("localhost", 8000, True, 60)  # TypeError! 不能按位置传入

四、参数解包

在调用函数时,可以使用 *** 将序列和字典解包为参数:

python 复制代码
def multiply(a, b, c):
    return a * b * c

# 列表/元组解包为位置参数
nums = [2, 3, 4]
print(multiply(*nums))  # 24(等价于 multiply(2, 3, 4))

# 字典解包为关键字参数
params = {"a": 2, "b": 3, "c": 5}
print(multiply(**params))  # 30

# 混合使用
def build_url(base, **params):
    query = "&".join(f"{k}={v}" for k, v in params.items())
    return f"{base}?{query}"

defaults = {"page": 1, "size": 20}
url = build_url("https://api.example.com/items", **defaults)
print(url)  # https://api.example.com/items?page=1&size=20

五、lambda匿名函数

lambda 是创建小型匿名函数的方式,语法为 lambda 参数: 表达式

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

# lambda等价形式
square_lambda = lambda x: x ** 2

print(square(5))         # 25
print(square_lambda(5))  # 25

5.1 典型使用场景

python 复制代码
# 场景1:排序的key参数
students = [
    ("Alice", 88),
    ("Bob", 92),
    ("Charlie", 79)
]
students.sort(key=lambda s: s[1], reverse=True)
print(students)  # [('Bob', 92), ('Alice', 88), ('Charlie', 79)]

# 场景2:map/filter
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)  # [2, 4, 6, 8, 10]

squared = list(map(lambda x: x**2, nums))
print(squared)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# 场景3:作为回调函数
import tkinter as tk
# root = tk.Tk()
# button = tk.Button(root, text="点击", command=lambda: print("被点击了"))

5.2 lambda的局限性

lambda只能包含单个表达式,不能包含语句或复杂逻辑。很多初学者试图在lambda中写多行逻辑或赋值语句,这是不可行的:

python 复制代码
# lambda不宜用于复杂逻辑
# bad_lambda = lambda x: if x > 0: ...  # 语法错误

# 正确做法:简单用lambda,复杂用def
def classify_number(n):
    if n > 0:
        return "正数"
    elif n < 0:
        return "负数"
    else:
        return "零"

在实际开发中,lambda最合适的用途是作为 sorted()key 参数和 filter()/map() 的短小回调。如果一行写不下,或者逻辑超过两个条件分支,直接用 def 定义具名函数------可读性远重要于少写两行代码。另一个需要注意的陷阱是:在循环中创建lambda时,变量捕获的是引用而非值 ,会导致所有lambda共享最终迭代值。遇到这种情况,可以用 functools.partial 或者给lambda加默认参数来"固定"当前值。


六、函数注解(Type Hints)

Python 3.5+ 支持类型注解,提高代码可读性和IDE支持:

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

    Args:
        weight: 体重(千克)
        height: 身高(米)

    Returns:
        BMI值
    """
    return weight / (height ** 2)

bmi = calculate_bmi(70, 1.75)
print(f"BMI: {bmi:.1f}")  # BMI: 22.9

# 复杂类型注解
from typing import List, Dict, Tuple, Optional, Union

def process_scores(
    scores: List[float],
    weights: Optional[Dict[str, float]] = None
) -> Tuple[float, float, float]:
    """处理成绩,返回平均分、最高分、最低分"""
    if scores:
        return sum(scores) / len(scores), max(scores), min(scores)
    return 0.0, 0.0, 0.0

Python的类型注解不会在运行时强制检查(需配合mypy等静态检查工具),但它对代码可维护性很有帮助。


七、函数作为一等公民

在Python中,函数是一等公民------可以赋值给变量、作为参数传递、作为返回值:

python 复制代码
# 函数赋值给变量
def greet_chinese(name):
    return f"你好,{name}"

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

# 函数字典(策略模式)
greeters = {
    "zh": greet_chinese,
    "en": greet_english
}

lang = "zh"
print(greeters[lang]("Alice"))  # 你好,Alice

# 函数作为参数(高阶函数)
def apply_twice(func, value):
    return func(func(value))

def inc(x):
    return x + 1

print(apply_twice(inc, 5))  # 7(先5+1=6,再6+1=7)

# 函数作为返回值(工厂函数)
def make_multiplier(factor):
    def multiplier(x):
        return x * factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))  # 10
print(triple(5))  # 15

八、综合实战:命令行参数解析器

python 复制代码
import argparse
from typing import Callable, Dict, Any

def create_cli():
    """创建一个简单的命令行工具框架"""

    commands: Dict[str, Callable] = {}

    def register(name: str):
        """装饰器工厂:注册命令处理函数"""
        def decorator(func: Callable):
            commands[name] = func
            return func
        return decorator

    def execute(command_name: str, **kwargs: Any):
        """执行指定的命令"""
        if command_name not in commands:
            print(f"未知命令:{command_name}")
            print(f"可用命令:{', '.join(commands.keys())}")
            return

        handler = commands[command_name]
        result = handler(**kwargs)
        return result

    def list_commands():
        return list(commands.keys())

    return register, execute, list_commands

# 使用框架
register_cmd, execute_cmd, list_cmds = create_cli()

@register_cmd("greet")
def handle_greet(name: str = "World", repeat: int = 1) -> str:
    """问候命令"""
    for _ in range(repeat):
        print(f"Hello, {name}!")
    return f"已问候{repeat}次"

@register_cmd("calc")
def handle_calc(a: float, b: float, op: str = "+") -> float:
    """计算命令"""
    ops = {"+": a + b, "-": a - b, "*": a * b, "/": a / b}
    result = ops.get(op, None)
    if result is None:
        print(f"不支持的运算符:{op}")
        return 0.0
    print(f"{a} {op} {b} = {result}")
    return result

# 执行命令
print(f"可用命令:{list_cmds()}")
execute_cmd("greet", name="Alice", repeat=3)
execute_cmd("calc", a=10, b=5, op="*")

总结

Python的函数机制博大精深,本文涵盖了核心要点:

  1. def定义:函数名、参数、文档字符串、返回值的基本结构
  2. 位置参数:按顺序传入,最基础的参数传递方式
  3. 默认参数 :提供可选值,但绝不能使用可变对象作为默认值
  4. *args:接收任意数量的位置参数,打包为元组
  5. **kwargs:接收任意数量的关键字参数,打包为字典
  6. 强制关键字参数*后参数必须通过关键字传入,提高可读性
  7. 参数解包***调用时展开序列和字典
  8. lambda:简洁的匿名函数,适用于排序键、map/filter等场景
  9. 类型注解:提高代码可读性,配合mypy做静态检查
  10. 函数是一等公民:可赋值、传参、返回,支撑高阶函数和装饰器模式

函数参数设计是一门艺术------好的参数设计让调用者直观易懂,避免了"这个参数是干嘛的"的困惑。下一篇文章我们将深入学习装饰器,探索Python最具魔力的语法特性之一。

✅ 亮点总结

  • 默认参数避坑:绝不能使用可变对象(\[\]、{})作为默认值,理解"定义时求值"规则
  • *args 与 **kwargs:接收任意数量参数,构建灵活通用的函数接口
  • 强制关键字参数* 后的参数必须通过关键字传入,大幅提升调用可读性
  • lambda 匿名函数:一行定义简单函数,配合 sorted/map/filter 使用简洁优雅
  • 函数是一等公民:可赋值给变量、作为参数传递、作为返回值,支撑高阶函数和装饰器模式
  • 类型注解def add(a: int, b: int) -> int: 提升代码可读性,配合 mypy 做静态检查

适用场景

  • 开发通用工具函数库:利用 *args/**kwargs 设计灵活API,适配多种调用方式
  • 回调函数与事件处理:lambda 和函数引用作为参数传递给GUI按钮点击、定时任务等
  • API包装函数设计:封装外部接口时,用类型注解明确参数和返回值含义

扩展方向

  • 学习 functools 模块:partial(偏函数)、reduce、lru_cache 等实用工具
  • 掌握生成器函数(yield)与迭代器协议,实现惰性数据流处理
  • 推荐继续阅读下一篇:Python函数进阶之装饰器,探索Python最具魔力的语法特性