小朋友们,你们好!欢迎来到Python魔法课堂。今天我们要学习一个超级厉害的工具——函数!
你有没有遇到过这样的情况:写代码时,有一段相同的操作要重复很多次?比如,你要计算好几个长方形的面积,每次都要写 长 * 宽,是不是又累又容易写错?要是能有一个“小机器人”,你只要告诉它长和宽,它就能自动算出面积,那该多好呀!
在Python里,这个“小机器人”就叫函数。函数可以把一段代码打包起来,给它起个名字,然后随时喊它帮忙。学会了函数,你的代码就会变得像搭积木一样,又简单又有趣!
准备好了吗?让我们打开今天的魔法宝箱吧!
一、第一个魔法盒:定义和调用函数
想象你有一个魔法盒,你只要对它念一句咒语,它就会自动帮你做一件事。在Python里,这个魔法盒就是函数。
1. 怎么造一个魔法盒?
造魔法盒的咒语是 def(define 的缩写)。格式如下:
比如,我们造一个会打招呼的魔法盒:
def say_hello(): print("你好呀,朋友!")
这个魔法盒的名字叫 say_hello,它里面只放了一句话:打印“你好呀,朋友!”。
2. 怎么使用魔法盒?
魔法盒造好之后,不会自己动,你需要“叫醒”它。叫醒它的方法就是写出它的名字,后面加上括号 ()。
运行后,屏幕上就会显示:
你可以叫它很多次,它每次都会乖乖执行里面的代码。
试试看:定义一个函数 print_favorite_food(),里面打印一句“我最喜欢吃披萨!”,然后调用它三次。
二、给魔法盒喂原料:函数的参数
上面的魔法盒每次只会做同样的事,有点死板。如果能让它根据我们给的不同原料做出不同反应,那就更棒了!这些“原料”就是参数。
1. 必填参数:一个都不能少
我们可以在定义函数时,在括号里写上参数的名字。调用时,必须按顺序提供对应数量的原料。
def greet(name): print(f"你好,{name}!")greet("小明") # 输出:你好,小明!greet("小红") # 输出:你好,小红!# greet() # 报错,因为缺少参数
如果函数需要多个原料,就写多个参数,用逗号隔开。
def add(a, b): result = a + b print(f"{a} + {b} = {result}")add(3, 5) # 输出:3 + 5 = 8
注意:参数的顺序很重要。add(5, 3) 会把5给a,3给b,结果还是8,但如果两个参数作用不同,顺序就不能乱。
试试看:定义一个函数 multiply(x, y),打印 x 和 y 的乘积,然后调用它计算 4×7。
2. 默认参数:有备无患
有时候,某些原料大部分时候都一样,只有偶尔不同。我们可以给参数设置一个默认值。这样,如果调用时没给这个原料,就会自动使用默认值。
def greet(name, greeting="你好"): print(f"{greeting},{name}!")greet("小明") # 输出:你好,小明!greet("小红", "哈喽") # 输出:哈喽,小红!
注意:默认参数必须放在必填参数的后面,否则Python会糊涂。
# 错误示例def greet(greeting="你好", name): # 这样不行! ...
试试看:定义一个函数 introduce(name, age=10),打印“我叫xx,今年xx岁”。如果不给年龄,默认10岁。
3. 关键字参数:打乱顺序也不怕
调用函数时,你可以用“参数名=值”的形式指定参数,这叫关键字参数。这样,你可以不按照定义时的顺序传递原料,非常自由。
def introduce(name, age, city): print(f"我叫{name},今年{age}岁,住在{city}。")# 以下三种调用方式都正确introduce("小明", 10, "北京")introduce(city="北京", name="小明", age=10)introduce("小明", city="北京", age=10)
注意:关键字参数必须写在普通参数的后面。
# 错误示例introduce(city="北京", "小明", age=10) # 语法错误
试试看:调用上面的 introduce 函数,用关键字参数把顺序打乱,看看结果。
4. 不定长参数:我不知道会有多少原料
有时候,你希望函数能接收任意多个原料,比如计算一堆数字的总和。这时可以用 *args。
*args 会把所有额外的位置参数打包成一个元组(你可以把它想象成一个列表)。
def sum_all(*numbers): total = 0 for n in numbers: total += n print(f"总和是:{total}")sum_all(1, 2, 3) # 输出:总和是:6sum_all(10, 20, 30, 40) # 输出:总和是:100
如果你还需要接收任意多个关键字参数,可以用 **kwargs。它会把额外的关键字参数打包成一个字典。
def show_info(**info): for key, value in info.items(): print(f"{key} -> {value}")show_info(name="小明", age=10, hobby="编程")
输出:
name -> 小明age -> 10hobby -> 编程
试试看:定义一个函数 average(*scores),计算传入的所有分数的平均值,并打印。
三、魔法盒的产出:返回值
我们的魔法盒不仅能做事,还能返回结果给你。比如,你给魔法盒两个数字,它算好和之后,把结果交到你手里。这就要用到 return 语句。
def add(a, b): return a + bresult = add(5, 3)print(result) # 输出 8
重要规则:
def say_hi(): print("嗨") return print("这行永远不会执行") # 被忽略x = say_hi()print(x) # 输出 None
试试看:写一个函数 is_even(n),如果 n 是偶数返回 True,否则返回 False。
四、变量的地盘:作用域
每个变量都有自己的“地盘”,有的只能在函数内部使用,有的可以在整个程序中使用。这叫做作用域。
1. 局部变量:只在函数内部生效
在函数内部定义的变量,就是局部变量,外面无法访问。
def test(): x = 10 # 局部变量 print(x)test()print(x) # 报错!x 在外面不可见
为什么要分地盘?
这样可以避免函数内部的变量不小心影响到程序的其他部分,就像每个小朋友有自己的玩具箱,不会混在一起。
练习1:下面的代码会输出什么?试着运行一下。
def func(): a = 5 print(a)func()print(a) # 这里会怎样?
练习2:写一个函数 my_secret(),在里面定义一个局部变量 secret = "我喜欢编程",然后在函数内部打印它。在函数外面也尝试打印这个变量,看看会发生什么。
2. 全局变量:到处都能用
在函数外部定义的变量,就是全局变量。在函数内部,你可以读取它。
y = 20 # 全局变量def show(): print(y) # 可以读取show() # 输出 20
但是,如果想在函数内部修改全局变量,需要先声明 global。
y = 20def change(): global y # 告诉Python:我要修改全局变量y y = 100change()print(y) # 输出 100
如果不加 global,在函数内部给同名变量赋值,会创建一个新的局部变量,不会影响全局变量。
z = 30def try_change(): z = 50 # 这是局部变量,不是全局的 print("函数内:", z)try_change() # 输出 函数内: 50print("全局:", z) # 输出 30
练习3:写一个全局变量 score = 100,定义一个函数 add_score(),它让 score 增加10,然后打印新分数。注意要用 global。
练习4:下面的代码输出什么?为什么?
x = 10def test(): x = 20 print(x)test()print(x)
练习5:猜一猜输出。
a = 5def func(): global a a = a + 1 print(a)func()print(a)
练习6:写一个程序,有一个全局变量 total = 0。定义两个函数:add() 让 total 增加任意指定的数值;show() 打印 total 的当前值。然后依次调用 add(5), add(3), show(),观察结果。
3. 嵌套函数中的 nonlocal
当函数里面再定义一个函数时,内层函数如果想修改外层函数的变量,需要用 nonlocal。
def outer(): x = 10 def inner(): nonlocal x x += 1 print(x) inner()outer() # 输出 11
如果不写 nonlocal,内层函数只能读取,不能修改外层变量(会创建一个新的局部变量)。
练习7:写一个函数 counter(),里面定义一个变量 count = 0,再定义一个内部函数 increment(),每次调用 increment() 就让 count 加1并打印。最后让 counter() 返回 increment。然后测试:c = counter(),然后多次调用 c()。
练习8:下面代码会输出什么?为什么?
def outer(): msg = "Hello" def inner(): msg = "Hi" print(msg) inner() print(msg)outer()
五、函数自己叫自己:递归
有一种特殊的函数,它会在自己的肚子里调用自己。这就像镜子里的镜子,一层套一层。这种函数叫递归函数。
递归必须有一个结束条件(也叫基线条件),否则会无限循环下去,直到程序崩溃。
1. 递归的经典例子:计算阶乘
阶乘:n! = n × (n-1) × (n-2) × ... × 1,并且规定 0! = 1。
def factorial(n): if n == 0 or n == 1: # 结束条件 return 1 else: return n * factorial(n-1)print(factorial(5)) # 输出 120(5×4×3×2×1)
它是怎么工作的?
我们可以画出“调用栈”:
factorial(5) 想要结果,就去问 factorial(4)
factorial(4) 去问 factorial(3)
factorial(3) 去问 factorial(2)
factorial(2) 去问 factorial(1)
factorial(1) 直接返回 1
然后一层层返回:1 → 2 → 6 → 24 → 120
练习9:手动模拟 factorial(4) 的计算过程,写出每一步的返回值。
2. 另一个经典例子:斐波那契数列
斐波那契数列:1, 1, 2, 3, 5, 8, 13, ... 其中第一个和第二个都是1,后面的每个数都是前两个数之和。
def fib(n): if n == 1 or n == 2: return 1 else: return fib(n-1) + fib(n-2)print(fib(6)) # 输出 8(数列:1,1,2,3,5,8)
注意:这种写法虽然简单,但效率很低,因为会重复计算很多次。比如 fib(5) 会计算 fib(4) 和 fib(3),而 fib(4) 又会计算 fib(3) 和 fib(2),fib(3) 被算了两次。我们后面会学优化方法(比如记忆化)。
练习10:用递归函数计算斐波那契数列的第10项,然后自己验证是否正确。
3. 递归画图:谢尔宾斯基三角形(选读)
递归不只是用来算数字,还可以用来画图。比如画一个“谢尔宾斯基三角形”——一个大三角形里面挖掉一个倒着的小三角形,剩下的三个小三角形再重复这个过程。
import turtledef sierpinski(t, length, depth): if depth == 0: for _ in range(3): t.forward(length) t.left(120) else: sierpinski(t, length/2, depth-1) t.forward(length/2) sierpinski(t, length/2, depth-1) t.backward(length/2) t.left(60) t.forward(length/2) t.right(60) sierpinski(t, length/2, depth-1) t.left(60) t.backward(length/2) t.right(60)t = turtle.Turtle()t.speed(0)sierpinski(t, 200, 3)turtle.done()
这个例子比较难,可以先跳过,只是让你感受递归的威力。
4. 递归的注意事项
必须有结束条件,否则会无限递归,导致 RecursionError。
Python默认递归深度大约1000,所以深度太大的递归(比如 factorial(2000))会报错。你可以用 sys.setrecursionlimit(3000) 临时增加深度,但不推荐。
有些问题用循环(迭代)解决更高效,比如阶乘也可以用循环写。
练习11:用递归写一个函数 power(base, exp),计算 base 的 exp 次方(exp是自然数)。例如 power(2, 3) 返回 8。
练习12:用递归写一个函数 sum_range(start, end),返回从 start 到 end 的所有整数的和(包括两端)。例如 sum_range(1, 5) 返回 15。
练习13:用递归写一个函数 reverse_string(s),返回字符串 s 的反转。例如 reverse_string("hello") 返回 "olleh"。提示:如果字符串长度为0或1,直接返回;否则返回 最后一个字符 + reverse_string(除了最后一个字符的字符串)。
练习14:用递归判断一个字符串是否是回文(正反一样)。例如 is_palindrome("level") 返回 True。提示:比较第一个和最后一个字符,然后递归判断去掉两端后的子串。
练习15:写一个递归函数 draw_line(n),打印 n 个星号。例如 draw_line(5) 打印 *****。然后再写一个递归函数 draw_triangle(n),打印一个由星号组成的直角三角形,行数为 n。例如 draw_triangle(4) 输出:
练习16:汉诺塔问题。有三根柱子 A、B、C,A 上有 n 个盘子(从小到大叠放)。要把所有盘子从 A 移到 C,每次只能移动一个盘子,且大盘子不能压在小盘子上。写一个递归函数 hanoi(n, start, end, helper),打印每一步的移动步骤。例如 hanoi(3, "A", "C", "B") 会输出 7 步。这是递归的经典难题,试试看!
六、一句话函数:lambda
有些函数特别简单,只有一行表达式。Python允许我们用一种更简洁的方式定义它们,叫做lambda(匿名函数)。
1. lambda 的基本写法
语法:
lambda 参数1, 参数2, ... : 表达式
例如,一个求两数之和的函数:
add = lambda a, b: a + bprint(add(3, 5)) # 输出 8
注意:
2. lambda 的常用场景
lambda 通常用在需要临时小函数的地方,比如排序、过滤、映射等。
2.1 与 sorted() 结合
按年龄对学生列表排序:
students = [("小明", 10), ("小红", 9), ("小刚", 11)]students.sort(key=lambda s: s[1]) # 按年龄排序print(students) # [('小红', 9), ('小明', 10), ('小刚', 11)]
按名字长度排序:
words = ["apple", "pear", "banana", "kiwi"]words.sort(key=lambda w: len(w))print(words) # ['pear', 'kiwi', 'apple', 'banana']
2.2 与 filter() 结合(选读)
filter() 用来筛选列表中的元素。它需要两个参数:一个判断函数(返回 True/False),一个可迭代对象。
numbers = [1, 2, 3, 4, 5, 6]even_numbers = list(filter(lambda x: x % 2 == 0, numbers))print(even_numbers) # [2, 4, 6]
2.3 与 map() 结合
map() 用来对列表中的每个元素应用一个函数,返回新列表。
numbers = [1, 2, 3, 4]squares = list(map(lambda x: x**2, numbers))print(squares) # [1, 4, 9, 16]
2.4 在条件表达式中使用
lambda 可以结合 if-else 条件表达式,但注意这是表达式,不是语句。
max = lambda a, b: a if a > b else bprint(max(5, 8)) # 输出 8
3. lambda 的局限性
不能包含循环(如 for, while)。
不能包含赋值语句(如 x = 5)。
不能包含多行代码。
没有自己的函数名,调试时不太方便。
如果逻辑超过一行,最好还是用 def 定义普通函数。
练习17:用 lambda 写一个函数 is_positive(x),返回 x 是否大于0。
练习18:有一个列表 [3, 8, 1, 6, 5],用 sorted() 和 lambda 按数字的个位数排序。
练习19:用 filter() 和 lambda 筛选出列表中所有长度大于3的字符串。列表:["cat", "elephant", "dog", "bird"]。
练习20:用 map() 和 lambda 把列表 [1,2,3,4] 的每个元素变成它的立方。
练习21:写一个 lambda 表达式 my_max,接收任意多个参数(用 *args),返回其中的最大值。提示:*args 在 lambda 中也可以使用。
七、高手进阶:冷门但有趣的知识
当你对函数越来越熟悉后,会发现它们还有很多隐藏的魔法。下面我们详细讲解这些“高手技巧”。
1. 函数也是对象
在Python里,函数和其他数据(数字、字符串)一样,可以被赋值给变量、作为参数传递、作为返回值。
def hello(): print("Hello")f = hello # 把函数赋值给变量ff() # 调用f,输出 Hello
你甚至可以把函数放在列表里:
def add(a, b): return a + bdef sub(a, b): return a - bops = [add, sub]print(ops[0](5, 3)) # 输出 8
练习22:写一个函数 apply(func, x, y),它接收一个函数 func 和两个数字,然后返回 func(x, y)。然后调用 apply(lambda a,b: a*b, 4, 5),看看结果。
2. 文档字符串(docstring)
在函数的第一行用三个引号写一段文字,就可以用 help() 查看。这叫文档字符串,是给使用者看的说明书。
def multiply(a, b): """返回 a 和 b 的乘积""" return a * bhelp(multiply) # 会显示函数名和说明
多行文档字符串示例:
def complex_func(x, y): """ 这是一个复杂函数的说明。 参数: x: 第一个数字 y: 第二个数字 返回: 两个数字的某种运算结果 """ return x ** y
练习23:给你之前写的某个函数加上文档字符串,然后调用 help 查看效果。
3. 类型注解(Type Hints)—— 给变量贴标签
Python 是动态类型语言,变量的类型可以随时改变。但为了代码更清晰、更容易发现错误,Python 3.5 引入了类型注解。你可以给参数和返回值加上类型提示,但这些提示不会强制检查,只是给程序员看的(也可以被一些工具如 mypy 用来做静态检查)。
3.1 基本语法
def 函数名(参数1: 类型, 参数2: 类型) -> 返回值类型: ...
例子:
def add(a: int, b: int) -> int: return a + bresult = add(3, 5)print(result)
这里的 : int 表示 a 和 b 应该是整数,-> int 表示返回值应该是整数。即使你传入字符串,Python 也不会报错,但阅读代码的人就知道你的意图。
3.2 常见的类型
int:整数
float:浮点数
str:字符串
bool:布尔值
list:列表(可以指定元素类型,如 list[int])
tuple:元组
dict:字典(如 dict[str, int] 表示键是字符串,值是整数)
Optional:表示可以是该类型或 None(如 Optional[int])
Any:任意类型
3.3 对列表、字典等容器指定内部类型
需要从 typing 模块导入 List, Dict, Tuple, Optional 等。
from typing import List, Dict, Tupledef sum_list(numbers: List[int]) -> int: return sum(numbers)def get_student() -> Dict[str, int]: return {"age": 10, "score": 95}def divide(a: int, b: int) -> Tuple[int, int]: return (a // b, a % b)
3.4 默认参数的类型注解
默认参数也可以加类型,写法与普通参数一样。
def greet(name: str, greeting: str = "你好") -> None: print(f"{greeting},{name}!")
注意:返回值是 None 时写 -> None。
3.5 类型别名
如果某个类型很复杂,可以给它起个外号。
from typing import ListStudent = List[str] # Student 是字符串列表的别名def get_names() -> Student: return ["小明", "小红"]
3.6 为什么需要类型注解?
练习24:给下面这个函数加上类型注解。
def multiply(a, b): return a * b
练习25:写一个函数 make_person(name, age, city),返回一个字典,包含这三项信息。给参数和返回值加上类型注解。
练习26:写一个函数 filter_positive(numbers),接收一个整数列表,返回一个新列表,只包含正数。加上类型注解。
4. 嵌套函数(再探)与闭包
函数内部可以再定义函数。内部函数可以访问外部函数的变量,并且可以返回内部函数,这样就形成了闭包。闭包可以“记住”外部函数的变量值。
def outer(x): def inner(y): return x + y return inneradd5 = outer(5)print(add5(3)) # 输出 8
这里 outer 返回了 inner,并且 inner 记住了 x=5。即使 outer 已经执行完毕,x 的值仍然保留在 add5 中。
练习27:写一个函数 make_multiplier(n),返回一个函数,这个函数接收一个参数 x,返回 x * n。例如 double = make_multiplier(2),然后 double(5) 应返回 10。
5. 小心默认参数的“陷阱”
如果默认参数是可变对象(比如列表、字典),多次调用可能会累积修改,因为默认值只在函数定义时计算一次,之后每次调用都使用同一个对象。
def add_item(item, lst=[]): lst.append(item) return lstprint(add_item(1)) # [1]print(add_item(2)) # [1, 2] —— 不是 [2]!
解决方法:用 None 作为默认值,在函数内部创建新列表。
def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst
练习28:找出下面代码的问题并修复。
def add_student(name, class_list=[]): class_list.append(name) return class_list
6. 解包参数:用 * 和 ** 拆散列表和字典
调用函数时,可以用 * 把一个列表拆成多个位置参数,用 ** 把一个字典拆成多个关键字参数。
def add(a, b, c): return a + b + cnums = [1, 2, 3]print(add(*nums)) # 相当于 add(1,2,3)info = {"a": 10, "b": 20, "c": 30}print(add(**info)) # 相当于 add(a=10,b=20,c=30)
练习29:写一个函数 print_info(name, age, city),然后用一个字典 {"name":"小明", "age":10, "city":"北京"} 通过 ** 解包调用它。
7. 函数可以返回多个值(其实是返回一个元组)
def get_name_and_age(): return "小明", 10name, age = get_name_and_age()print(name, age) # 小明 10
实际上返回的是一个元组 ("小明", 10),然后通过元组解包赋值给两个变量。
8. nonlocal 关键字(再巩固)
在嵌套函数中,如果想修改外层函数的变量,需要用 nonlocal。如果不写,Python会认为你在创建一个新的局部变量。
def outer(): x = 10 def inner(): nonlocal x x += 1 print(x) inner()outer() # 输出 11
练习30:写一个函数 create_counter(),它返回一个内部函数,每次调用内部函数时,计数器加1并返回当前计数值。要求使用 nonlocal。
9. 函数属性
函数也可以有自己的属性,就像变量一样。你可以给函数添加自定义属性。
def say_hello(): print("Hello")say_hello.times_called = 0 # 给函数添加一个属性say_hello.times_called += 1print(say_hello.times_called) # 输出 1
这个技巧可以用来统计函数被调用的次数,或者缓存一些信息。
八、综合大挑战
现在,你已经掌握了函数的各种魔法。来挑战几个小任务吧!
挑战1:计算器(带类型注解)
编写一个函数 calculator(a: float, b: float, op: str) -> float,根据 op (+, -, *, /) 返回计算结果。如果 op 不是这些符号,返回 None。如果除数为0,返回 None。
挑战2:斐波那契数列(记忆化递归)
普通的递归 fib 很慢,因为重复计算太多。请用“记忆化”(用一个字典保存已经计算过的值)来优化它。写一个函数 fib_memo(n, memo={})。
挑战3:任意多数字的平均值
写一个函数 average(*nums: float) -> float,返回所有数字的平均值。如果没传任何数字,返回 0.0。
挑战4:密码生成器(带类型注解和默认参数)
写一个函数 generate_password(length: int = 8, use_digits: bool = True, use_punctuation: bool = False) -> str,随机生成一个指定长度的密码。可以包含字母(大小写)、数字、标点符号(根据参数)。提示:使用 random 模块,string.ascii_letters, string.digits, string.punctuation。
挑战5:递归打印目录树(高级)
如果你学过了 os 模块,可以尝试写一个递归函数 list_files(path, indent=0),打印出指定文件夹下的所有文件和子文件夹,并用缩进表示层级。
挑战6:装饰器入门
写一个装饰器 timer,它接收一个函数,返回一个新函数。新函数在执行原函数之前记录开始时间,执行后记录结束时间,并打印耗时。(提示:使用 time.time() 和 *args, **kwargs)
九、总结
今天我们开启了一场函数大冒险,学到了:
定义和调用:用 def 创建魔法盒,用 函数名() 使用它。
参数:必填参数、默认参数、关键字参数、不定长参数 *args 和 **kwargs。
返回值:用 return 把结果交给调用者。
变量作用域:局部变量、全局变量(global)、嵌套函数中的 nonlocal。做了大量练习。
递归:函数自己调用自己,必须有结束条件。通过阶乘、斐波那契、画图等例子深入理解,并完成了多个练习。
匿名函数:lambda 一行小函数,常用于排序、过滤、映射。详细讲解了用法和局限性。
类型注解:给参数和返回值添加类型提示,提高代码可读性,包括基本类型、容器类型、默认参数等。
冷门技巧:函数是对象、文档字符串、嵌套函数与闭包、默认参数陷阱、解包参数、函数属性等。
函数让我们的代码变得像搭积木一样,每个小积木(函数)可以独立测试、反复使用。从今天起,试着把你常用的代码段改写成函数吧!
最后的最后:别忘了多动手练习,把每个练习都在电脑上敲一遍,观察结果。遇到错误不要怕,那是学习的最好机会!