五:函数——定义、参数、返回值

一、回顾与本篇目标

前四篇我们学了变量、数据类型、条件判断、循环、四种容器。你已经能用 Python 写出有一定逻辑的小程序了——比如猜数字游戏、学生成绩管理系统。

但你可能注意到一个问题:随着程序越写越长,有些代码出现了重复。比如成绩管理系统里,打印学生信息的那段代码在多个地方重复出现。如果要改格式,就得改好几处。

函数就是解决这个问题的。它把一段代码打包,起个名字,以后需要的时候直接喊名字就行。函数是编程中最核心的抽象工具——它能让你把复杂的问题拆成小块,逐个击破。

本篇的目标:

  1. 学会用 def 定义函数
  2. 理解位置参数、默认参数、关键字参数的区别
  3. 掌握函数的返回值
  4. 学会 lambda 匿名函数
  5. 理解 Python 函数的参数传递机制(和 JS 的异同)

二、为什么需要函数

先看一个没有函数的反面例子:

# 没有函数:重复代码
student1 = '张三'
score1 = 85
print(f'学生:{student1},成绩:{score1}分,等级:{"优秀" if score1 >= 90 else "良好" if score1 >= 80 else "及格"}')

student2 = '李四'
score2 = 72
print(f'学生:{student2},成绩:{score2}分,等级:{"优秀" if score2 >= 90 else "良好" if score2 >= 80 else "及格"}')

student3 = '王五'
score3 = 95
print(f'学生:{student3},成绩:{score3}分,等级:{"优秀" if score3 >= 90 else "良好" if score3 >= 80 else "及格"}')

同样的逻辑写了三遍。如果要加一个“不及格”的判断,得改三个地方。如果有 100 个学生呢?

用函数重写:

# 有函数:一次定义,反复使用
def print_student_report(name, score):
    if score >= 90:
        grade = '优秀'
    elif score >= 80:
        grade = '良好'
    elif score >= 60:
        grade = '及格'
    else:
        grade = '不及格'
    print(f'学生:{name},成绩:{score}分,等级:{grade}')

# 使用函数
print_student_report('张三', 85)
print_student_report('李四', 72)
print_student_report('王五', 95)

逻辑只写了一次,修 bug、改格式都只需要改一处。函数就是代码的“打包盒”,把一段逻辑封装起来,起个名字,随时调用。

三、定义和调用函数

基本语法

def 函数名(参数1, 参数2, ...):
    """文档字符串:说明这个函数是干什么的(可选)"""
    # 函数体:要执行的代码
    return 返回值  # 可选,没有 return 则返回 None

最简单的函数

def greet():
    print('你好!')
    print('欢迎学习 Python')

# 调用函数(写多少次就执行多少次)
greet()
greet()
greet()

和 JavaScript 的对比:

# Python
def greet():
    print('你好')

// JavaScript
function greet() {
  console.log('你好');
}

核心区别:

  • Python 用 def,JS 用 function
  • Python 函数体靠缩进,JS 靠花括号
  • Python 函数名后面的括号和冒号是必须的。

带参数的函数

参数是函数接收外部数据的入口。调用时把具体的值传进去:

def greet(name):
    print(f'你好,{name}!')

greet('张三')  # 你好,张三!
greet('李四')  # 你好,李四!

多个参数用逗号分隔:

def introduce(name, age, city):
    print(f'我叫{name},今年{age}岁,来自{city}')

introduce('张三', 28, '上海')
introduce('李四', 22, '北京')

形参和实参

这两个术语在面试和文档中经常出现,用一张表说清楚:

术语 是什么 在哪里出现 示例
形参 定义函数时括号里的变量名,用来占位 函数定义中 def foo(name): 中的 name
实参 调用函数时传入的具体值 函数调用中 foo('张三') 中的 '张三'
def add(a, b):       # a 和 b 是形参(占位符)
    return a + b

result = add(3, 5)   # 3 和 5 是实参(实际传入的值)
print(result)         # 8

四、参数的五种类型

Python 函数的参数比 JavaScript 更灵活,有五种不同的传参方式。

1. 位置参数:按顺序传值

这是最常见的传参方式——按参数定义的位置顺序传入值:

def describe_pet(animal_type, pet_name):
    print(f'我有一只{animal_type},名叫{pet_name}')

describe_pet('狗', '旺财')   # 我有一只狗,名叫旺财
describe_pet('猫', '咪咪')   # 我有一只猫,名叫咪咪

顺序不能乱——describe_pet('旺财', '狗') 会输出“我有一只旺财,名叫狗”,完全不对。

2. 关键字参数:按名字传值

调用函数时,可以明确指定形参的名字来传值,顺序就可以任意了:

def describe_pet(animal_type, pet_name):
    print(f'我有一只{animal_type},名叫{pet_name}')

# 用关键字参数,顺序任意
describe_pet(animal_type='狗', pet_name='旺财')
describe_pet(pet_name='咪咪', animal_type='猫')  # 顺序反过来也行

位置参数和关键字参数可以混用,但位置参数必须在关键字参数前面:

describe_pet('狗', pet_name='旺财')   # 正确
# describe_pet(animal_type='狗', '旺财')  # 报错!位置参数不能跟在关键字参数后面

3. 默认参数:参数有备选值

给形参设置一个默认值,调用时如果不传这个参数,就使用默认值:

def describe_pet(pet_name, animal_type='狗'):
    print(f'我有一只{animal_type},名叫{pet_name}')

describe_pet('旺财')                    # 不传 animal_type,默认是“狗”
describe_pet('咪咪', animal_type='猫')  # 传了就用传的值

有默认值的参数必须放在没有默认值的参数后面:

# 正确:默认参数在后面
def func(a, b=10):
    pass

# 错误:默认参数不能在前
# def func(a=10, b):  # SyntaxError
#     pass

默认参数的一个坑:可变默认值

这是一道经典的 Python 面试题,也是实际开发中的常见 bug:

# 有问题的写法:默认值是可变对象(列表)
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item('苹果'))   # ['苹果']
print(add_item('香蕉'))   # ['苹果', '香蕉'] —— 预期是 ['香蕉']!
print(add_item('橘子'))   # ['苹果', '香蕉', '橘子'] —— 越来越多了!

原因:Python 的默认参数只在函数定义时计算一次,而不是每次调用时都重新创建。所以三次调用实际上操作的是同一个列表对象

正确写法

def add_item(item, items=None):
    if items is None:
        items = []    # 每次调用时新建一个空列表
    items.append(item)
    return items

print(add_item('苹果'))   # ['苹果']
print(add_item('香蕉'))   # ['香蕉'] —— 正确了!

4. 可变参数:接收任意数量的位置参数

在形参前面加一个星号 *args,表示这个参数可以接收任意数量的位置参数,它们会被打包成一个元组

def make_pizza(*toppings):
    print('你点的披萨配料有:')
    for topping in toppings:
        print(f'  - {topping}')

make_pizza('蘑菇')
make_pizza('蘑菇', '香肠', '芝士', '青椒')

*args 这个名字是约定俗成的(arguments 的缩写),重点在那个星号。你可以起任何名字,但大家都用 *args

5. 关键字可变参数:接收任意数量的关键字参数

在形参前面加两个星号 **kwargs,表示这个参数可以接收任意数量的关键字参数,它们会被打包成一个字典

def build_profile(**user_info):
    for key, value in user_info.items():
        print(f'{key}: {value}')

build_profile(name='张三', age=28, city='上海', job='程序员')

**kwargs 同样是约定俗成的名字(keyword arguments 的缩写),重点是两个星号。

五种参数的组合顺序

如果把五种参数都写在一个函数里,它们的顺序必须是:

def func(位置参数, *args, 默认参数, **kwargs):
    pass

实际上很少会五种全用到。最常见的是位置参数、默认参数,偶尔用到 *args**kwargs

五、返回值:函数交出的结果

函数不只是做事情,还能交回结果。这就是 return 的作用。

返回单个值

def add(a, b):
    return a + b

result = add(3, 5)
print(result)       # 8
print(add(10, 20))  # 30

返回多个值

Python 函数可以“返回多个值”——实际上返回的是一个元组

def get_user_info():
    name = '张三'
    age = 28
    city = '上海'
    return name, age, city    # 实际返回的是 (name, age, city) 元组

# 用多个变量接收
name, age, city = get_user_info()
print(name, age, city)  # 张三 28 上海

# 用一个变量接收(会得到一个元组)
info = get_user_info()
print(info)             # ('张三', 28, '上海')
print(type(info))       # 

这和 JavaScript 的解构赋值非常像,但 Python 的写法更自然。

没有 return 语句

如果函数没有 return,或者 return 后面什么都没有,函数返回 None

def say_hello():
    print('你好')

result = say_hello()
print(result)  # None

None 是 Python 的“空值”,相当于 JavaScript 的 nullundefined 的结合体。

return 会立即终止函数

函数体内 return 后面的代码不会被执行

def check_age(age):
    if age < 18:
        return '未成年'
    # 如果上面 return 了,下面的代码不会执行
    return '已成年'

print(check_age(15))  # '未成年'
print(check_age(25))  # '已成年'

六、变量的作用域

函数内部定义的变量和函数外部定义的变量,它们的“可见范围”是不同的。

局部变量

函数内部定义的变量是局部变量,只能在函数内部使用,函数外部访问不到:

def my_function():
    message = '我是局部变量'
    print(message)      # 函数内部可以访问

my_function()
# print(message)        # 报错!NameError: name 'message' is not defined

全局变量

在所有函数外部定义的变量是全局变量,整个程序都可以访问:

global_message = '我是全局变量'

def read_global():
    print(global_message)   # 可以读取全局变量

read_global()  # '我是全局变量'

修改全局变量需要用 global 关键字

如果要在函数内部修改全局变量的值,必须用 global 关键字声明:

counter = 0

def increment():
    global counter       # 声明要修改全局变量
    counter += 1

increment()
increment()
print(counter)  # 2

如果不写 global countercounter += 1 会被 Python 认为是在创建一个新的局部变量 counter,从而导致错误(因为局部变量 counter 还没赋值就被读取了)。

实际开发中,尽量少用全局变量。通过参数把值传进函数,通过 return 把结果传出来,这样代码更好维护。

七、lambda 匿名函数

lambda 是一种快速定义单行函数的方式。它没有函数名,所以叫“匿名函数”。

基本语法

lambda 参数: 表达式

它等价于:

def 函数名(参数):
    return 表达式

简单示例

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

# lambda 写法
add_lambda = lambda a, b: a + b

print(add(3, 5))         # 8
print(add_lambda(3, 5))  # 8

lambda 最常见的用途:作为排序的 key

# 按第二个元素排序
pairs = [(1, 'one'), (4, 'four'), (3, 'three'), (2, 'two')]
pairs.sort(key=lambda pair: pair[1])  # 按英文单词排序
print(pairs)
# [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

lambda 也可以和 map、filter 一起使用

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

# map:对每个元素应用同一个函数
squared = list(map(lambda x: x ** 2, nums))
print(squared)  # [1, 4, 9, 16, 25]

# filter:筛选出满足条件的元素
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)   # [2, 4]

不过,Python 社区更推荐用列表推导式代替 map/filter(后面会讲),所以 lambda 在实际代码中最常见的还是用作 sortkey 参数。

lambda 和 JavaScript 箭头函数的对比

# Python
square = lambda x: x ** 2

// JavaScript
const square = (x) => x ** 2;

两者功能相似,但 Python 的 lambda 只能写一个表达式,不能包含多行语句。如果需要复杂逻辑,就用 def 定义命名函数。

八、参数传递机制:可变对象与不可变对象

这是一个理解 Python 函数行为的关键概念,也是面试高频题。

Python 的参数传递是“传递对象引用”——不是传值,也不是传引用,而是把实参对象的引用(内存地址)传给了形参。具体表现取决于传入的对象是可变的还是不可变的:

# 不可变对象作为参数(数字、字符串、元组)
def change_value(x):
    x = 100          # 这行让 x 指向了一个新的对象,不影响外部的变量
    print(f'函数内部:{x}')

a = 10
change_value(a)
print(f'函数外部:{a}')  # 10 —— 没变!

# 可变对象作为参数(列表、字典)
def change_list(lst):
    lst.append(4)     # 修改了 lst 指向的对象本身,外部也会受影响
    print(f'函数内部:{lst}')

my_list = [1, 2, 3]
change_list(my_list)
print(f'函数外部:{my_list}')  # [1, 2, 3, 4] —— 变了!

核心规律

  • 如果传入的是不可变对象(数字、字符串、元组),在函数内部重新赋值不会影响外部变量
  • 如果传入的是可变对象(列表、字典),在函数内部修改对象的内容会影响外部变量。

这和 JavaScript 的行为类似——基本类型传值,对象类型传引用。但 Python 的不可变类型更多(元组、字符串等),理解这个机制能避免很多奇怪的 bug。

九、综合演示:一个简单的工具库

下面这段代码把本篇学到的函数知识整合起来,写几个实用的小工具函数:

# ========== 工具函数库 ==========
import math

# 1. 计算圆的面积和周长
def circle_info(radius):
    area = math.pi * radius ** 2
    circumference = 2 * math.pi * radius
    return area, circumference

# 2. 判断一个年份是不是闰年
def is_leap_year(year):
    return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)

# 3. 计算列表的平均值
def average(*numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)

# 4. 生成指定范围内的素数列表
def get_primes(start, end):
    primes = []
    for num in range(start, end + 1):
        if num < 2:
            continue
        is_prime = True
        for i in range(2, int(math.sqrt(num)) + 1):
            if num % i == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return primes

# ========== 测试 ==========
area, circumference = circle_info(5)
print(f'半径 5 的圆:面积={area:.2f},周长={circumference:.2f}')

print(f'2000 年是闰年吗?{is_leap_year(2000)}')
print(f'2023 年是闰年吗?{is_leap_year(2023)}')

print(f'1 到 10 的平均值:{average(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)}')
print(f'没有参数:{average()}')

print(f'1 到 30 之间的素数:{get_primes(1, 30)}')

十、本篇动手练习

练习 1:计算器函数

新建 practice5-1.py,写一个函数 calculator(a, b, operator),operator 可以是 '+''-''*''/',返回对应的计算结果。如果 operator 不在这四种里,返回 None

练习 2:字符串处理函数

新建 practice5-2.py,写一个函数 process_text(text, operation),operation 可以是 'upper'(全大写)、'lower'(全小写)、'reverse'(反转)、'count_words'(统计单词数)。

练习 3:斐波那契数列

新建 practice5-3.py,写一个函数 fibonacci(n),返回包含前 n 个斐波那契数的列表。斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13, …(每个数等于前两个数之和)。

练习 4:密码强度检测

新建 practice5-4.py,写一个函数 check_password_strength(password),根据密码长度、是否包含数字、是否包含大小写字母、是否包含特殊字符来打分。返回 ‘弱’、’中’、’强’ 三个等级。

十一、本篇小结

这一篇你系统学习了 Python 函数的核心知识:

  • 函数的定义和调用def 关键字、缩进表示函数体、调用用 函数名()
  • 五种参数:位置参数(按顺序)、关键字参数(按名字)、默认参数(有备选值)、*args(接收任意数量的位置参数,打包成元组)、**kwargs(接收任意数量的关键字参数,打包成字典)。
  • 返回值return 返回结果,可以返回多个值(实际返回元组)。没有 return 则返回 Nonereturn 会立即终止函数。
  • 变量作用域:局部变量只能在函数内使用,全局变量可以全局访问,修改全局变量需要用 global 关键字。
  • lambda 匿名函数:单行函数的快捷写法,常用于排序的 key 参数。
  • 参数传递机制:传递对象引用。不可变对象修改不影响外部,可变对象修改会影响外部。
  • 默认参数的坑:默认值如果是可变对象(如列表),每次调用会共用同一个对象。用 None 作为默认值并在函数内部初始化。

函数是编程中最核心的抽象工具。把常用的逻辑封装成函数,你的代码会越来越像搭积木——调用现成的函数,组合出更复杂的功能。

下一篇预告

下一篇——《模块与包——组织你的代码》:当你的函数越来越多,一个文件会变得很臃肿。我们需要学会把函数拆分到不同的文件里,然后用 import 来引用。还会介绍 pip 安装第三方库、创建虚拟环境管理项目依赖。

Python 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容