CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Python 语言基础/课时/函数
RawBlame
课时目录12 节

Python 语言基础

  1. 01Python 是什么
  2. 02语法与缩进
  3. 03类型与变量
  4. 04条件与循环
  5. 05函数
  6. 06列表字典与集合
  7. 07字符串处理
  8. 08文件读写
  9. 09模块与 pip
  10. 10类与对象入门
  11. 11异常处理
  12. 12综合练习与下一步

函数

def、参数默认值、*args/**kwargs 入门。

35 分钟

函数

这一课你将能做到

  1. 用 def 定义带参数、带返回值、带默认值的函数,并区分位置参数与关键字参数
  2. 读懂并会写 *args / **kwargs,理解「可变参数」在什么场景用
  3. 说清局部变量与全局变量的规则,避免「函数里改了变量外面没生效」的困惑

为什么要学

写到第三课你一定发现了很多重复代码:算两次折扣要抄两遍、打印不同用户信息要复制粘贴。函数就是「给一段代码起名字、留几个口子」——写一次,到处调。而且 Python 标准库、第三方包、面试题里全是函数思维:不理解函数,后面类与对象、异常、模块全都悬空。

人话:函数像自动售货机——投币(传参数)、按键(选函数)、出货(拿返回值)。机器内部怎么转的你不用管,接口清楚就能用。自己写函数,就是造一台自己的售货机。

先建立图像

def greet(name):            ← 定义:造机器(此时不执行内部代码)
    return f"你好, {name}"   ← return:出货口

greet("小明")               ← 调用:投币启动
        │
        ▼
┌──────────────────┐
│  name = "小明"    │   ← 参数在函数内部就是普通变量
│  return "你好, 小明" │
└──────────────────┘
        │
        ▼
   '你好, 小明'          ← 返回值交还给调用处

与 Java 方法对照:

// Java:类型写死在签名里
static int add(int a, int b) { return a + b; }
# Python:不声明类型,参数即变量
def add(a, b):
    return a + b

跟着做

步骤 1:定义与调用最简单的函数

新建 func_demo.py:

def say_hi():                 # 无参数
    print("hi")

def add(a, b):                # 两个参数
    return a + b

say_hi()
result = add(3, 5)            # 返回值接住
print(result)                 # 8

# 没写 return 的函数,返回值是 None
def no_return(x):
    x * 2                     # 忘了 return,计算结果被丢弃

print(no_return(21))          # None  ← 常见 bug 来源

输出:

hi
8
None

步骤 2:默认参数与关键字参数

def make_coffee(size="中杯", sugar=0, milk=False):
    parts = f"{size}杯"
    if sugar:
        parts += f",{sugar} 份糖"
    if milk:
        parts += ",加奶"
    return parts

# 位置参数:按顺序对应
print(make_coffee("大杯", 2))

# 关键字参数:指名道姓,顺序随便
print(make_coffee(milk=True, size="小杯"))

# 混用:位置在前,关键字在后
print(make_coffee("大杯", milk=True))

输出:

大杯杯,2 份糖
小杯杯,加奶
大杯杯,加奶

默认参数让调用方「只传必要的」;关键字参数让调用处自文档化——make_coffee(milk=True) 一眼知道在干嘛。

步骤 3:可变默认参数的世纪大坑

def add_item(item, items=[]):        # ← 错误示范:默认值是可变对象
    items.append(item)
    return items

print(add_item(1))    # [1]
print(add_item(2))    # [1, 2]  ← 惊!默认列表被跨调用共享了

输出:

[1]
[1, 2]

默认值在函数定义时只创建一次,不是每次调用都新建。正确写法:

def add_item_ok(item, items=None):   # 标准解法:用 None 哨兵
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item_ok(1))    # [1]
print(add_item_ok(2))    # [2]   每次都是新列表

输出:

[1]
[2]

步骤 4:*args 与 **kwargs

def demo(*args, **kwargs):
    print("args:", args)          # 元组,收住所有多余位置参数
    print("kwargs:", kwargs)      # 字典,收住所有多余关键字参数

demo(1, 2, 3, name="小明", age=18)

输出:

args: (1, 2, 3)
kwargs: {'name': '小明', 'age': 18}

实战:写一个「把任意多个数加起来」的函数:

def total(*nums):
    result = 0
    for n in nums:                # nums 在函数内是元组
        result += n
    return result

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

# 反过来:* 还能把序列「拆开」传给函数
def power(base, exp):
    return base ** exp

pair = (2, 10)
print(power(*pair))               # 1024,等价于 power(2, 10)

输出:

3
15
1024

步骤 5:返回多个值与文档字符串

def min_max(nums):
    """返回列表的最小值和最大值。

    参数 nums: 数字列表
    """
    return min(nums), max(nums)   # 实际返回一个元组

low, high = min_max([3, 1, 4, 1, 5])   # 解包接收
print(low, high)                       # 1 5

print(min_max.__doc__)                 # 文档字符串会被存下来

输出:

1 5
返回列表的最小值和最大值。

    参数 nums: 数字列表

「返回多个值」是假象——Python 只是把元组打包返回、调用处解包。这是 Python 函数特别顺手的一点。

步骤 6:作用域——函数内外是两个世界

count = 0                # 全局变量

def increase():
    n = 100              # 局部变量:只在函数内存在
    print("函数内 n =", n)

def try_read():
    print("函数内读全局 count =", count)   # 读全局:可以

increase()
try_read()
# print(n)               # NameError: name 'n' is not defined

counter = 0
def bump():
    global counter       # 想在函数内修改全局变量,必须声明 global
    counter += 1

bump()
print(counter)           # 1

输出:

函数内 n = 100
函数内读全局 count = 0
1

规则:函数内可以读全局变量,不能直接改(改会创建同名局部变量);要改就 global 声明——但更好的做法是把值当参数传进去、用 return 传出来,global 尽量少用。

讲清楚要点

  1. def 是「定义」不是「执行」:函数体在调用时才跑。定义时参数名拼错了都不报错,调用那一刻才炸——所以函数写完必须实际调用测一次。
  2. 参数四种形态,顺序固定:位置参数 → 默认参数 → *args → **kwargs。写成 def f(a, b=1, *args, **kwargs) 是标准结构。
  3. 默认参数只求值一次:默认值在 def 时创建。可变默认值(list/dict/set)会跨调用共享,一律用 None 哨兵代替。
  4. return 立刻结束函数:return 后面的代码不执行;没有 return 或光写 return,返回 None。函数要么「返回值」要么「产生副作用」,两者混着写是可读性大坑。
  5. 传参传的是「对象引用」:把列表传进函数并在函数内 append,外面的列表也会变(可变对象共享);把 int 传进去做加法,外面不变(不可变对象)。这不是「值传递 vs 引用传递」二选一,而是「引用的值传递」。
  6. 变量查找顺序 LEGB:Local(函数内)→ Enclosing(外层函数)→ Global(模块级)→ Built-in(内置)。局部遮蔽全局,全局遮蔽内置。
  7. lambda 是一行式小函数:sorted(pairs, key=lambda p: p[1]) 里的 key 回调是它的主战场。复杂逻辑别硬塞 lambda,老实用 def 起名。

你可能会懵的地方

  • 参数传「值」还是传「引用」? 都不是,是「传对象引用」。行为上:不可变对象(int/str/tuple)在函数内「改不动」外面的;可变对象(list/dict)在函数内原地修改会影响外面的。根源是第 3 课说的「变量是名字标签」。

  • 什么时候用 global? 几乎可以永远不用。脚本级小程序里改一个配置标志可以接受;正式代码用参数传入、返回值带出,或者干脆封装成类。global 让函数行为依赖外部状态,测试和排错都变难。

  • * 到底几种意思? 三种:定义时 *args 收集多余位置参数;调用时 f(*mylist) 拆开序列逐个传;单独出现 def f(a, *, b) 表示 b 必须用关键字传(强制关键字参数)。本课掌握前两种即可。

  • 函数名能当变量用吗? 能。函数是对象,f = add 之后 f(1, 2) 照样跑;函数还能当参数传给别的函数(如 sorted 的 key)、当返回值返回。这是第 10 课之前你能接触到的「一切皆对象」最直观的例子。

  • 为什么我的函数改了列表,外面真的变了?这和 global 冲突吗? 不冲突。global 管的是「名字绑定」(counter += 1 是重新绑定名字);而 lst.append(x) 是「修改对象内容」,名字没变、对象变了,所以不需要 global。记住:重新绑定才要 global,原地修改不要。

错法对照

错法问题正解
def f(items=[])可变默认值跨调用共享def f(items=None) + 函数内判 None
函数里算了结果忘了 return调用方拿到 None明确 return 结果
def f(a=1, b)默认参数后不能跟必选参数,SyntaxError必选在前:def f(b, a=1)
函数内直接给全局变量赋值生成同名局部变量,外面不变参数传入 + return 带出;确需修改用 global
函数太长干五件事不可测不可复用一个函数一件事,超过 30 行考虑拆分
用 print 代替 return结果没法参与后续运算函数返回值,print 留给调用方

卡点急救

  1. 调用报 TypeError: missing 1 required positional argument:必选参数没传。对照 def 签名逐个检查,默认参数可以不传,必选不行。
  2. 函数返回 None:忘了 return,或只写了 return;也可能是调用了「原地修改型」方法(如 .sort())当作返回值。检查函数体最后一行。
  3. 改了全局变量外面不生效:函数内赋值只动局部。要么 return 出去,要么 global 声明(慎用)。
  4. 默认列表/字典里「攒」了上次的数据:可变默认参数坑,改成 None 哨兵写法。
  5. *args 打印出来是元组不是我要的数:*args 把多个参数收成元组是正常行为;遍历它或用 sum(args) 处理,别直接当单个值用。

面试常问

  • ***args 和 kwargs 的区别与用途? *args 把多余位置参数收成元组,**kwargs 把多余关键字参数收成字典;常用于写包装函数、透传参数(def wrapper(*args, **kwargs): return real(*args, **kwargs))。调用侧的 * / ** 则是反向解包。

  • Python 函数传参机制? 传对象引用(call by object reference)。函数拿到的是实参对象的引用;对可变对象原地修改会影响外部,对参数重新赋值(绑定新对象)不影响外部。可结合 list.append 与 int 赋值举例。

  • 可变默认参数为什么危险? 默认值在函数定义时求值一次,存进 __defaults__,所有调用共享同一个对象;对它的原地修改会跨调用累积。解法是 None 哨兵。能用 f.__defaults__ 亲眼验证是加分项。

  • lambda 与 def 的区别? lambda 是匿名单表达式函数,语法上只能放一个表达式(不能有语句、多行逻辑),返回表达式的值;def 可以有完整函数体和文档字符串。lambda 典型用途是 sorted/max/min 的 key、map/filter 回调。

  • 什么是 LEGB 作用域规则? 变量查找顺序 Local → Enclosing → Global → Built-in。函数内赋值默认创建局部变量;读写全局需注意遮蔽;修改全局要 global、修改外层函数变量要 nonlocal。

自测三问

  1. def f(a, b=2, *args, **kwargs),调用 f(1, 3, 4, x=9) 时 a、b、args、kwargs 分别是什么?
  2. 为什么默认参数永远不该写成 =[]?标准替代写法是什么?
  3. 函数内 lst.append(1) 能影响外部列表,count += 1 却影响不了外部整数,原因是什么?

小结一句话

函数 = 起名字的代码块:def 定义、参数开洞、return 出货;记住可变默认参数用 None、*args/**kwargs 收杂货、改全局要么 return 要么 global,就能放心造积木了。

延伸

  • 官方教程 — 函数定义
  • PEP 8 函数命名建议
  • 下一课预告:列表字典与集合——Python 四大容器一次讲透,增删改查、去重、排序全在这里。
加载本节练习…
← 上一课条件与循环下一课 →列表字典与集合