Python组合数据类型&函数编程
[TOC]
1. 组合数据类型
Python 提供了多种强大的组合数据类型(也称为容器),用于存储和操作一组数据。掌握它们是编写高效 Python 代码的基础。
1.1 列表 (List)
列表是 Python 中最常用的数据类型,它是一个有序且可变的序列。它可以容纳不同类型的元素。
Java 对比:类似于 Java 中的
ArrayList,但 Python 的 List 可以存储任意不同类型的元素。
1.1.1 列表内存结构示意
列表本质上是一个存储对象引用的动态数组。
graph LR
List[List对象] --> |索引0| E1[元素1: 'apple']
List --> |索引1| E2[元素2: 'banana']
List --> |索引2| E3[元素3: 100]
1.1.2 列表常用操作 (增删改查)
| 方法 | 描述 | 示例 |
|---|---|---|
append(x) |
末尾追加 | lst.append(1) |
extend(seq) |
末尾扩展序列 | lst.extend([2,3]) |
insert(i, x) |
指定位置插入 | lst.insert(0, 'a') |
pop(i) |
弹出指定位置元素 | lst.pop() (默认最后) |
remove(x) |
删除第一个值为x的元素 | lst.remove('a') |
clear() |
清空列表 | lst.clear() |
index(x) |
查找元素索引 | lst.index('a') |
count(x) |
统计元素出现次数 | lst.count('a') |
sort() |
排序 (原地修改) | lst.sort(reverse=True) |
reverse() |
反转 (原地修改) | lst.reverse() |
copy() |
浅拷贝列表 | lst.copy() |
# 1. 创建与增加
fruits = ['apple', 'banana']
fruits.append('orange') # ['apple', 'banana', 'orange']
fruits.insert(1, 'grape') # ['apple', 'grape', 'banana', 'orange']
# 2. 删除
x = fruits.pop() # 弹出 'orange'
if 'grape' in fruits: # 安全删除前先检查
fruits.remove('grape') # 删除 'grape'
# 3. 修改
fruits[0] = 'pear' # ['pear', 'banana']
# 4. 查找与统计
nums = [1, 2, 3, 2, 4, 2]
print(f"2出现了 {nums.count(2)} 次") # 统计次数
print(f"3的索引是 {nums.index(3)}") # 查找索引
# 5. 高级排序
students = [('Tom', 80), ('Jerry', 90), ('Spike', 85)]
# 按分数(第二个元素)倒序排序
students.sort(key=lambda x: x[1], reverse=True)
print(students) # [('Jerry', 90), ('Spike', 85), ('Tom', 80)]
1.1.3 列表切片 (Slicing) - 重点
切片是获取列表子集的强大工具。
- 语法:
list[start:end:step](左闭右开区间)
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[1:4]) # [1, 2, 3] (索引1到3)
print(nums[:3]) # [0, 1, 2] (从头开始)
print(nums[7:]) # [7, 8, 9] (直到末尾)
print(nums[::2]) # [0, 2, 4, 6, 8] (步长为2)
print(nums[::-1]) # [9, 8, ..., 0] (列表反转)
print(nums[1:-1]) # [1, ..., 8] (去掉首尾)
1.1.4 列表推导式 (List Comprehension)
列表推导式提供了一种简洁优雅的方式来创建列表,比 for 循环更快。
Java 对比:类似 Java 8 的 Stream API,但语法更简洁。
- Python:
[x*2 for x in list if x > 0]- Java:
list.stream().filter(x -> x > 0).map(x -> x*2).toList()
# 需求:生成 [1, 4, 9, 16, ..., 100]
# 推导式写法 (推荐)
squares = [x**2 for x in range(1, 11)]
# 带条件的推导式 (只计算偶数平方)
even_squares = [x**2 for x in range(1, 11) if x % 2 == 0]
# 嵌套推导式 (生成扑克牌)
colors = ['♠', '♥', '♣', '♦']
ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
deck = [c + r for c in colors for r in ranks]
1.2 元组 (Tuple)
元组与列表类似,但是元组是不可变的。一旦创建,就不能修改。
Java 对比:类似于 Java 中的
Object[]数组(长度不可变),或者 Java 9+ 的List.of()创建的不可变列表。
- 应用场景:保护数据不被篡改、作为字典的 Key、函数返回多个值。
1.2.1 关键陷阱:单元素元组
如果要创建只有一个元素的元组,必须加逗号,否则会被识别为普通括号运算。
t1 = (10) # <class 'int'> (这是整数)
t2 = (10,) # <class 'tuple'> (这是元组)
1.2.2 元组解包 (Unpacking)
# 1. 交换变量
a, b = 10, 20
a, b = b, a # a=20, b=10
# 2. 接收剩余元素 (*)
data = (1, 2, 3, 4, 5)
head, *middle, tail = data
print(middle) # [2, 3, 4]
1.2.3 元组的高级用法
元组因其不可变性,可以作为字典的 Key,而列表不行。
locations = {
(39.9, 116.4): "Beijing",
(31.2, 121.5): "Shanghai"
}
print(locations[(39.9, 116.4)]) # Beijing
1.3 集合 (Set)
集合是一个无序且不重复的元素集。
Java 对比:类似于 Java 中的
HashSet。
- 原理:类似只有 Key 没有 Value 的字典。
- 场景:快速去重、数学集合运算。
1.3.1 创建集合
s1 = {1, 2, 3}
s2 = set([1, 2, 2, 3]) # {1, 2, 3} 自动去重
s3 = set() # 注意:空集合必须用 set(),{} 是空字典
1.3.2 集合实战:列表去重
这是面试中常见的考点。
names = ['Tom', 'Jerry', 'Tom', 'Spike']
unique_names = list(set(names)) # ['Spike', 'Tom', 'Jerry'] (顺序可能打乱)
# 如果需要保持顺序:
unique_names_ordered = sorted(list(set(names)), key=names.index)
1.3.3 集合常用操作
| 方法 | 描述 | 示例 |
|---|---|---|
add(x) |
添加元素 | s.add(4) |
update(seq) |
添加多个元素 | s.update([5, 6]) |
remove(x) |
删除元素 (不存在报错) | s.remove(4) |
discard(x) |
删除元素 (不存在不报错) | s.discard(99) |
pop() |
随机弹出元素 | s.pop() |
clear() |
清空集合 | s.clear() |
1.3.4 集合运算 (交并差)
python_devs = {'Tom', 'Jerry', 'Spike'}
java_devs = {'Tom', 'Tyke'}
print(python_devs & java_devs) # 交集 (都会的人): {'Tom'}
print(python_devs | java_devs) # 并集 (所有人): {'Tom', 'Jerry', 'Spike', 'Tyke'}
print(python_devs - java_devs) # 差集 (只会Python): {'Jerry', 'Spike'}
print(python_devs ^ java_devs) # 对称差集 (只懂一门的): {'Jerry', 'Spike', 'Tyke'}
1.3.4 集合常用判断
| 方法 | 描述 |
|---|---|
isdisjoint() |
是否无交集 |
issubset() |
是否为子集 (<=) |
issuperset() |
是否为父集 (>=) |
1.4 字典 (Dict)
字典是 Python 中唯一的映射类型,采用 Key-Value 存储。
Java 对比:类似于 Java 中的
HashMap(无序) 或LinkedHashMap(有序,Python 3.7+ 字典默认有序)。
- 底层原理:基于 哈希表 (Hash Table) 实现,查找速度为 O(1),极快。
- Key的要求:必须是不可变类型(如字符串、数字、元组),列表不能做 Key。
1.4.1 字典操作详解
# 1. 创建
info = {"name": "Tom", "age": 18}
d2 = dict(name="Jerry", age=20) # 使用工厂函数
# 2. 访问
print(info["name"]) # Tom
print(info.get("sex")) # None (安全访问,不存在不报错)
print(info.get("sex", "男")) # 男 (设置默认值)
# 3. 增改
info["city"] = "Beijing" # 新增
info["age"] = 19 # 修改
# 4. 检查Key
if "age" in info:
print("年龄已存在")
1.4.2 字典常用方法全解
| 方法 | 描述 |
|---|---|
keys() |
获取所有键 (视图对象) |
values() |
获取所有值 (视图对象) |
items() |
获取所有键值对 (返回元组列表) |
update(dict2) |
合并字典 (覆盖已有key) |
pop(key) |
删除并返回指定值 |
popitem() |
删除并返回最后插入的键值对 |
clear() |
清空字典 |
setdefault(key, def) |
获取值,若不存在则设为默认值 |
fromkeys(seq, val) |
快速创建字典,统一初始化值 |
1.4.3 字典推导式
快速构建字典。
# 将两个列表组合成字典
keys = ['name', 'age', 'city']
values = ['Tom', 18, 'Beijing']
info = {k: v for k, v in zip(keys, values)}
# {'name': 'Tom', 'age': 18, 'city': 'Beijing'}
1.4.4 实战:setdefault 分组
这是一个非常实用的功能,常用于数据处理。
data = [("fruit", "apple"), ("fruit", "banana"), ("veg", "carrot")]
group = {}
for category, name in data:
group.setdefault(category, []).append(name)
print(group) # {'fruit': ['apple', 'banana'], 'veg': ['carrot']}
1.4.5 遍历字典
d = {'a': 1, 'b': 2}
# 遍历 Key
for k in d:
print(k)
# 遍历 Value
for v in d.values():
print(v)
# 遍历 Key-Value
for k, v in d.items():
print(f"{k} -> {v}")
1.5 核心难点:深浅拷贝
| 方式 | 描述 | 示意图 |
|---|---|---|
| 赋值 (=) | 引用传递,指向同一对象 | A -> [Obj] <- B |
| 浅拷贝 (copy) | 只拷贝父容器,子元素仍是引用 | A -> [Obj], B -> [Obj] (Obj是同一个) |
| 深拷贝 (deepcopy) | 递归拷贝所有层级,完全独立 | A -> [Obj1], B -> [Obj2] (完全隔离) |
import copy
a = [1, [2, 3]]
b = copy.deepcopy(a)
# 此时修改 a[1] 不会影响 b
1.6 容器对比总结
| 特性 | 列表 (List) | 元组 (Tuple) | 字典 (Dict) | 集合 (Set) |
|---|---|---|---|---|
| 符号 | [] |
() |
{k:v} |
{} |
| 有序性 | 有序 | 有序 | 3.7+有序 | 无序 |
| 可变性 | 可变 | 不可变 | 可变 | 可变 |
| 重复性 | 可重复 | 可重复 | Key不可重复 | 不可重复 |
| 查询效率 | O(n) | O(n) | O(1) | O(1) |
| 主要用途 | 存储序列数据 | 存储常量配置 | 键值映射 | 去重、数学运算 |
2. 函数
函数(function)用于封装一段可重复使用的逻辑:把一段代码“起个名字”,后续需要时直接调用即可。
Java 类比:可以把 Python 函数先类比成“写在类外面的静态方法”,调用时更像直接写
Util.printHello(),而不是必须new一个对象。
2.1 概念
函数(function)是 Python 中用于封装可重复执行代码块的基本单元,它能够接收输入参数、执行特定逻辑,并可选择性地返回结果。
简单的说:函数代表一个独立的可复用的功能。
通过使用函数,程序编写、阅读、测试和修复起来都更加容易。Python中的函数必须先定义后使用,Python提供了许多内建函数,比如print()。也可以自己创建函数,这被叫做用户自定义函数。
2.2 定义与调用
2.2.1 定义格式
Python 定义函数使用 def 关键字,一般格式如下:
Java 类比:
def add(a, b):对应 Java 的方法签名;Python 不要求写参数类型,但可以用类型提示让代码更像 Java(后面会看到)。
def 函数名 (参数列表) :
函数体
return [表达式]
- 格式:
- 函数代码块以
def关键字开头,后接函数名与圆括号(),括号后以冒号:结束 - 函数体使用缩进表示代码块
- 函数代码块以
- 函数名:
- 函数名需要遵循标识符命名规则。Python 工程中通常使用蛇形命名法(snake_case),例如
print_hello、get_user
- 函数名需要遵循标识符命名规则。Python 工程中通常使用蛇形命名法(snake_case),例如
- 参数列表:
- 函数在完成某个功能时,可能需要一些数据,在定义函数时指定函数参数来接收这些数据。例如在屏幕上打印信息,需要把要打印的信息传递给print()函数。如果有多个参数,参数之间使用逗号分隔。函数也可以没有参数,但是小括弧不能省略。
- 函数体:
- 函数体一般包含3个部分:文档说明,功能代码,return [表达式]语句。
- 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。用三个引号括起来,单引号和双引号都可以。
- return [表达式] 结束函数,返回一个值给调用方。不带表达式的return或没有return语句相当于返回 None。
2.2.2 定义与调用示例
- 函数必须先定义再调用;
- 函数定义后不调用是不会执行的;
- 调用一次执行一次;
# -----------------------函数的演示----------------------------
# 定义函数
def print_hello():
"""
打印hello world
"""
print("hello world")
# 调用函数。先定义,再调用。
print_hello()
print("--------")
print_hello()
print()
# -----------------------
# 定义函数,并接收参数
def print_star(line,column):
"""
打印指定行数和列数的星号
"""
for i in range(line):
for j in range(column):
print("*", end=" ")
print()
# 调用函数
print_star(5,10)
print()
# -----------------------
# 定义函数,并返回结果
def add(a, b):
"""
返回两个数的和
"""
return a + b
# 调用函数
print("add(1, 2) = ", add(1, 2))
print("add(2, 3) = ", add(2, 3))
2.3 说明文档(DocString)
Java 类比:DocString ≈ JavaDoc。区别是 DocString 写在函数体第一行的字符串里,运行时可以通过
help()/__doc__读取。
函数说明文档的好处:
- 提高可读性:清晰描述函数用途,别人不用翻实现也能用
- 便于维护:参数与返回值写清楚,后续修改不容易“改坏”
- 支持工具:
help()/ IDE 能直接读取 DocString 作为提示 - 团队协作:减少沟通成本,统一代码风格
# =================函数说明文档的演示========================
# 定义一个函数,可以判断一个数是否是素数
def is_prime(num):
"""
判断num是否是素数
:param num: 待判断的整数
:return: 如果是素数返回True,否则返回False
"""
if num < 2:
return False
import math
for i in range(2, int(math.sqrt(num)+1)):
if num % i == 0:
return False
return True
# 调用函数
print("7是素数吗?", is_prime(7))
print(help(is_prime))
PyCharm中将鼠标悬停在函数名上方也可以看到函数说明文档。

2.3.1 函数注释(了解)
Java 类比:这里更像“类型提示 + 一点点描述信息”,不是 Java 的
@Annotation。它主要给人和工具看(IDE/静态检查),不影响 Python 运行。
Python 3.x 引入了函数注释,以增强函数的注释功能。
可以使用:对参数逐个进行注释,注释内容可以是任何形式,比如参数的类型、作用、取值范围等等,返回值使用->标注,所有的注释都会保存到函数的__annotations__属性中。另外,使用函数注释并不影响默认参数的使用。
# 普通的自定义函数:
def dog(name, species, age):
print(f"{name} 的品种是:{species},年龄是:{age}")
# 调用函数
dog("旺财", "泰迪", 5)
# 添加了注释的自定义函数:
def dog(name: str, species: "狗狗的品种", age: (1, 99) = 1) -> "返回None":
print(f"{name} 的品种是:{ species},年龄是:{age}")
# 获取函数的注释信息
print(dog.__annotations__)
# 调用函数
dog("旺财", "泰迪")
2.4 返回值
Java 类比:Java 的
void表示“没有返回值”。Python 没写return时会自动返回None(你打印出来就能看到)。
2.4.1 没有 return
# =================函数返回值的演示========================
# 定义函数,函数体没有return语句,表示没有返回值
def print_hello():
"""
打印hello
"""
print("hello")
# 调用函数
print_hello() # print_hello函数没有返回值,所以不用接收返回值
# 在Python中函数没有返回值,也可以打印它的结果,但结果是None
print("结果:", print_hello()) # print_hello函数没有返回值,所以结果是None
result = print_hello() # 在Python中函数没有返回值,也可以用变量接收返回值,但结果是None
print("result=",result)
2.4.2 只有 return
# 定义函数,函数体有return语句,但return后面没有值,表示没有返回值
def print_star(line,column):
"""
打印指定行数和列数的星号
"""
if line <= 0 or column <= 0:
return # 作用是提前结束函数的执行
for i in range(line):
for j in range(column):
print("*", end=" ")
print()
# 调用函数
print_star(5,10) # print_star函数没有返回值,所以不用接收返回值
print("结果是", print_star(3,5)) # 结果是None
result = print_star(5,10) # 结果是None
print("result=",result)
2.4.3 return 值
# 定义函数
def add(a, b):
"""
返回两个数的和
"""
return a + b # 返回结果
# 调用函数
add(1, 2) # add函数有返回值,不接收返回值也是可以的,返回值就丢失了。因为add函数中没有打印语句,看起来像没有执行一样
print("add(1, 2) = ", add(1, 2)) # 直接输出返回值
result = add(1, 2) # 可以用变量接收返回值
print("result=",result)
2.5 参数
Java 类比:
- 位置参数:就是普通方法调用传参
- 关键字参数:更像“命名参数”,Java 语法本身不支持(通常靠 Builder/Map/重载实现)
*args:≈ Java 的T... args**kwargs:≈ 传一个Map<String, Object>
2.5.1 形参与实参
-
在定义函数时,指定的参数称为形式参数,简称为形参(函数的提供者);在调用函数时,给函数传递的参数称为实际参数,简称为实参(函数的调用者)
-
在定义函数时,形参没有分配存储空间,也没有值,相当于一个占位符;在调用函数时, 会在栈区中给函数分配存储空间, 然后给形参/局部变量分配存储空间,传递的是实际的数据。当函数执行结束,函数所占的栈空间会被释放,函数的形参/局部变量也会被释放
2.5.2 常见参数形式
2.5.2.1 位置参数
实参的顺序、个数必须与形参完全一致
# 1. 位置参数
# 定义函数
def print_info(name, age):
print(f"{name} 的年龄是:{age}")
# 调用函数
print_info("张三", 18)
2.5.2.2 默认值参数
定义默认值参数时要求:含默认值的形参必须在所有不含默认值的形参后面
含默认值的形参,可以不用指定实参
# 2. 默认参数,默认参数必须放在非默认参数后面
# 定义函数
def print_info(name, age=18):
print(f"{name} 的年龄是:{age}")
# 调用函数
print_info("张三", 20) # 可以正常给age形参赋值
print_info("张三") # 也可以不给age形参赋值,此时age就是默认值
2.5.2.3 关键字参数
调用方法时,实参可以使用形参名作为关键字来指定给哪个形参赋值,此时实参的顺序就不用与形参完全一致了
# 3. 关键字参数
# 定义函数
def print_info(name, age, gender='男'):
print(f"{name} 的年龄是:{age},性别是:{gender}")
# 调用函数
# 可以通过关键字参数给形参赋值, 此时name和age,gender的顺序可以任意
print_info(age=18, name="张三")
print_info(gender='女',name="张三", age=18)
2.5.2.4 可变参数
*args: 接收任意数量的位置参数,存储为tuple类型**kwargs: 接收任意数量的关键字参数,存储为dict类型- 如果一个函数包含可变参数和普通参数,那么建议普通参数在前,可变参数在后
- 一个函数中最多只能有一个
*args和**kwargs的可变参数 *args形式的可变参数后面,可以有**kwargs形式的可变参数。虽然*args后面也可以有普通参数,但是此时普通参数必须通过关键字参数的形参赋值,所以一般不推荐在*args后面定义普通参数。**kwargs形式的可变参数后面不允许再有任何参数。
- 一个函数中最多只能有一个
# 定义函数,,*hobby表示可变参数,*hobby会变成一个元组。
def print_info( *hobby):
print(f"爱好是:{hobby}")
# 调用函数
print_info("跑步", "看电影", "看小说")
# 定义函数,习惯上普通参数在前,可变参数一般放在形参列表的最后
def print_info(name, *hobby):
print(f"{name} 的爱好是:{hobby}")
# 调用函数
print_info("张三", "跑步", "看电影", "看小说")
# 定义函数,如果可变参数后面还有普通参数,那么普通参数必须使用关键字参数给普通参数赋值
def print_info(name, *hobby, age):
print(f"{name} 的爱好是:{hobby},年龄:{age}")
# 调用函数
print_info( "张三","跑步", "看电影", "看小说", age = 18)
# 定义函数,**contact_person表示可变参数,**contact_person会变成一个字典。
def print_info(name, **contact_person):
print(f"{name} 的联系人有:{contact_person}")
# 调用函数
print_info("张三", mon = "12345678901", dad = "12345678902")
# 定义函数,**contact_person可变参数必须在最后
def print_info(name, *hobby, **contact_person):
print(f"{name} 的爱好是:{hobby},联系人有:{contact_person}")
# 调用函数
print_info("张三","跑步", "看电影", "看小说", mon = "12345678901", dad = "12345678902")
2.5.2.5 解包传参
- 若函数的形参是定长参数,可以通过 * 和 ** 对列表、元组、字典等解包传参。
# 5. 解包传参
# 定义函数
def print_info(name, age):
print(f"{name} 的年龄是:{age}")
# 调用函数
print_info(*["张三", 18]) # 列表解包,要求列表元素个数与参数个数一致
print_info(*("张三", 18)) # 元组解包,要求元组元素个数与参数个数一致
print_info(*{"张三", 18}) # 不推荐用集合,因为集合是无序的
print_info(**{"name": "张三", "age": 18}) # 字典解包,要求字典的key与参数名一致
2.5.2.6 强制位置/关键字
- 强制使用位置参数或关键字参数:/ 前的参数必须使用位置传参,* 后的参数必须用关键字传参。
# 6. 强制使用位置参数或关键字参数:/ 前的参数必须使用位置传参,* 后的参数必须用关键字传参。
# 定义函数
def print_info(name, age, /, gender, hobby, *, city):
print(f"{name} 的年龄是:{age},性别是:{gender},爱好是:{hobby},城市是:{city}")
# 调用函数
print_info("张三", 18, "男", "看电影", city="上海")
print_info("张三", 18, gender="男", hobby="看电影", city="上海")
2.5.2.7 deepcopy 防修改
- 根据Python的参数传递机制的原理,如果参数是可变类型(例如:列表、集合、字典等),那么在函数中对可变参数的内容做修改,会影响实参。如果此时希望对形参的修改不影响实参,那么可以通过
copy.deepcopy()来实现。本质上相当于是复制了一个实参对象给形参。
# 7、防止列表等参数被修改
# 定义函数
def change_list(la):
print(f"{la} 的id是:{id(la)}")
la[0] = 100
print(f"{la} 的id是:{id(la)}")
la.append(6)
print(f"{la} 的id是:{id(la)}")
# 调用函数
import copy
listDemo = [1,2]
print(f"函数调用前:{listDemo},listDemo.id = {id(listDemo)}")
change_list(copy.deepcopy(listDemo))
print(f"函数调用后:{listDemo},listDemo.id = {id(listDemo)}")
2.5.3 参数传递机制
Java 核心对比:
Python 的参数传递机制与 Java 的 引用传递 (Call by Object Reference) 完全一致(针对对象)。
- 基础原则:变量保存的是对象的引用(地址),函数传参传的也是这个引用。
- 效果区别:
- 不可变对象(int, str, tuple):类似 Java 的 String。函数内修改(如
a = a + 1)是让变量指向新对象,不影响外面的变量。- 可变对象(list, dict):类似 Java 的 ArrayList。函数内修改内容(如
lst.append(1)),外面会受影响。但如果让变量指向新列表(lst = []),则不影响外面。
2.5.3.1 案例演示
1. 不可变对象 (类似 Java String)
def change_str(s):
print(f"函数内修改前: {s}, id={id(s)}")
s += "b" # 创建新对象 "aab",s 指向新地址
print(f"函数内修改后: {s}, id={id(s)}")
x = "aa"
print(f"调用前: {x}, id={id(x)}")
change_str(x)
print(f"调用后: {x}, id={id(x)}") # x 仍然是 "aa"
2. 可变对象 (类似 Java ArrayList)
def change_list(lst):
# 修改内容 -> 影响外部
lst.append(100)
# 重新赋值 -> 不影响外部
lst = [9, 9, 9]
my_list = [1, 2]
change_list(my_list)
print(my_list) # 输出 [1, 2, 100]
2.5.3.7 两种运算符区别
对于可变类型来说,还有一个小细节,la *= 2 和 la = la * 2 有差别:
- la *= 2 使用原地址
- la = la * 2 返回新地址
类似的还有:+=。
# ==============2种列表乘法的区别=================
def multiply_list_One(la) :
print(f"{la} 的id是:{id(la)}")
# la *= 2 # 使用原地址
la += [1,2]
print(f"{la} 的id是:{id(la)}")
list1 = [1,2]
multiply_list_One(list1)
print(list1) # [1, 2, 1, 2]
def multiply_list_two(la) :
print(f"{la} 的id是:{id(la)}")
# la = la * 2 # 使用新地址
la = la + [1,2]
print(f"{la} 的id是:{id(la)}")
list2 = [1,2]
multiply_list_two(list2)
print(list2) # [1, 2]
2.6 嵌套调用
- 在一个函数中调用另一个函数,当内层函数执行完之后才会继续执行外层函数的其他语句。
# ================函数嵌套定义======================
def function_a():
print("\t函数 A 开始执行")
print("\t函数 A 执行中...")
print("\t函数 A 结束执行")
def function_b():
print("函数 B 开始执行")
print("函数 B 执行中...")
function_a()
print("函数 B 执行中...")
print("函数 B 结束执行")
function_b()
执行结果:
函数 B 开始执行
函数 B 执行中...
函数 A 开始执行
函数 A 执行中...
函数 A 结束执行
函数 B 执行中...
函数 B 结束执行
2.7 作用域
Java 类比:Java 的局部变量作用域主要由
{}控制;Python 的作用域主要由“函数/类/模块”决定,if/for/while这类代码块并不会新开作用域。
2.7.1 LEGB 四种作用域
Python中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。变量的作用域决定了哪一部分程序可以访问哪个变量,Python的作用域一共有4种,分别是:
- L (Local) 局部作用域
- 函数内部定义的变量
- 只在当前函数内有效
- E (Enclosing)嵌套作用域
- 闭包中外层函数的作用域
- 在嵌套函数中可以访问外层函数的变量
- G (Global) 全局作用域
- 模块级别的变量
- 在整个文件中都可以访问
- B (Built-in) 内建作用域
- Python内置的变量和函数
- 如 len, print, int 等内置函数
以 L –> E –> G –>B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,其外部也可以访问。
# =================变量作用域====================
# if-else语句块中定义变量,不会开启新的变量作用域
import random
a= random.randint(1,10) # 全局变量
if a>5:
result = "big" # 全局变量,因为当前if不在函数,闭包中
print("a的值是:",a)
else:
result = "small"
print("result的值是:",result) # if-else语句块外,仍然可以访问result
# =================变量作用域====================
# 在函数中定义变量,会开启新的变量作用域
# G (Global): 模块级别变量
x = "global" # 全局变量
def outer():
# E (Enclosing): 闭包
y = "enclosing" # 外层函数变量,在整个outer函数中都可以访问
# 在外部函数体中定义内部函数
def inner():
# L (Local): 函数内部变量
z = "local" # 局部变量,仅限于inner函数内部访问
print(f"\t在inner中访问局部变量z的值是:{z}") # 访问局部变量
print(f"\t在inner中访问嵌套变量y的值是:{y}") # 访问外层函数变量
print(f"\t在inner中访问全局变量x的值是:{x}") # 访问全局变量
print(f"\t在inner中访问内建函数的值是:{len}") # 访问内建函数
# 在外部函数体中调用内部函数
print("在outer中调用inner函数")
inner()
# print(f"在outer中访问内层函数变量z的值是:{z}") # 报错
print(f"在outer中访问外层函数变量y的值是:{y}")
print(f"在outer中访问全局变量x的值是:{x}")
print(f"在outer中访问内建函数的值是:{len}")
# 调用外部函数函数
print("调用outer函数")
outer()
# print(f"在main中访问内层函数变量z的值是:{z}") # 报错
# print(f"在main中访问外层函数变量y的值是:{y}") # 报错
print(f"在main中访问全局变量x的值是:{x}")
print(f"在main中访问内建函数的值是:{len}")
2.7.2 global
- 在函数中不使用 global 声明全局变量时不能修改全局变量的指向,即不能让全局变量指向新地址。
- 可变类型:在函数中修改可变类型的全局变量的内容,可以不加global关键字。因为此时全局变量引用的地址并未改变。
- 不可变类型:在函数中修改不可变类型的全局变量值,必须加global关键字。因为不可类型的变量,一旦修改就意味着指向新地址。
2.7.2.1 可变类型
# =========可变类型在局部修改可以不使用global关键字===========
listDemo = [1,2,3] # 可变类型,全局变量
# 定义函数
def change_list():
listDemo.append(4)
print("change_list()函数中,listDemo的值是:",listDemo) # listDemo= [1,2,3,4]
# listDemo = [4,5,6] # 错误,因为Python 默认会把这个变量当作局部变量
# 调用函数
print("调用change_list()函数之前,listDemo的值是:",listDemo) # listDemo= [1,2,3]
change_list()
print("调用change_list()函数之后,listDemo的值是:",listDemo) # listDemo= [1,2,3,4]

2.7.2.2 不可变类型
# =============未使用global关键字无法修改不可变类型全局变量的值=============
x = "global"
# 定义函数
def change_x_one():
# x += "local" # 错误,因为没有global关键字,无法修改全局变量
x = "local" # 没有global关键字,给x赋值,Python 默认会把这个变量当作局部变量
print("change_x_one()函数中,x的值是:",x) # x= local
# 调用函数
print("调用change_x_one()函数之前,x的值是:",x) # x= global
change_x_one()
print("调用change_x_one()函数之后,x的值是:",x) # x= global
print()

# ===============使用global关键字才可以修改不可变类型全局变量的值=================
# global关键字,可以修改全局变量的值
x = "global"
# 定义函数
def change_x_two():
global x # 添加global关键字,修改全局变量的值
x = "local"
print("change_x_two()函数中,x的值是:",x) # x= local
# 调用函数
print("调用change_x_two()函数之前,x的值是:",x) # x= global
change_x_two()
print("调用change_x_two()函数之后,x的值是:",x) # x= local
print()

2.7.3 nonlocal
nonlocal 也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域而是嵌套作用域。
# ========使用nonlocal关键字在内部作用域修改外部作用域不可变类型变量的值==============
def function_outer():
str_demo = "outer"
print("function_outer()函数中,str_demo的值是:",str_demo)
def function_inner():
nonlocal str_demo
str_demo = "inner"
# 调用内部函数
print("调用内部函数function_inner()后")
function_inner()
print("function_outer()函数中,str_demo的值是:",str_demo)
# 调用外部函数
function_outer()
2.8 函数是一等公民
Java 类比:在 Java 里你通常用“函数式接口 + Lambda”来传递行为;在 Python 里函数本身就是对象,直接传就行。
在Python中,函数是一等公民,函数也是一种数据类型。
# 函数是一种数据类型
# 定义函数
def my_function():
print("我是函数体")
# 打印函数的类型
print(type(my_function))
所以,函数也是一个对象,它可以:
- 函数可以被赋值给变量
- 函数可以作为参数传递给其他函数
- 函数可以作为函数的返回值
- 函数可以存储在数据结构中
这种特性使得Python支持高阶函数、闭包、装饰器等高级编程技术。
2.8.1 赋值给变量
# 可以使用函数可以给变量赋值
def greet():
print("Hello!")
say_hello = greet # 注意greet后面没有()
say_hello()
注意:
- say_hello = greet # 赋值给say_hello变量的是一个函数
- say_hello = greet() # 赋值给say_hello变量的是函数的返回结果,这里是None,因为greet()函数没有返回值
2.8.2 作为参数传递
# 函数可以作为参数传递
def say_hi():
print("Hi!")
def call_function(func):
func() # 这里要求传给func的实参是一个函数。
call_function(say_hi) # say_hi是一个函数,它作为参数被传给了call_function函数
2.8.3 作为返回值
# 函数可以作为返回值
def get_function():
def inner_function():
print("Inner function")
return inner_function
#调用get_function函数
func = get_function() # 得到的是一个函数
func() # 调用func()函数
2.8.4 存进数据结构
# 函数可以被存储到数据结构中
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
# 将函数存储在列表中,同样也可以被存储在元组、字典等容器中
operations = [add, subtract, multiply, divide]
for operation in operations:
result = operation(5, 3)
print(operation.__name__ , ": ", result)
2.9 匿名函数(lambda)
Java 类比:lambda 的核心用途很像 Java 的
x -> x + 1,通常用在“短小的一次性逻辑”里;复杂逻辑更建议写成具名函数。
2.9.1 Lambda 表达式
Python使用 lambda 来定义匿名函数,所谓匿名,指其不用 def 的标准形式定义函数。
lambda 参数列表: 表达式
- 特点:
- 简洁:适合写“很短的小函数”。
- 限制:主体只能是一条表达式(不能写多行、赋值、if/for块)。
- 闭包:可以访问上下文变量。
Java 对比:
- Java: Lambda 必须依赖“函数式接口”(Functional Interface),如
Function<T, R>,Predicate<T>。- Python: Lambda 本质是创建一个函数对象,可以直接赋值给变量,或作为参数传递。
2.9.2 实战:结合内置函数 (Map/Filter/Sorted)
这是 Lambda 最常见的应用场景,相当于 Java 的 Stream API 操作。
| Python 操作 | Java Stream 对比 | 说明 |
|---|---|---|
map(func, list) |
stream().map(func) |
映射:对每个元素做处理 |
filter(func, list) |
stream().filter(func) |
过滤:保留符合条件的元素 |
sorted(list, key=func) |
stream().sorted(Comparator) |
排序:自定义排序规则 |
reduce(func, list) |
stream().reduce(func) |
归约:聚合结果 (需导入 functools) |
data = [1, 2, 3, 4, 5]
# 1. map: 每个元素平方
# Java: list.stream().map(x -> x*x).toList()
res_map = list(map(lambda x: x*x, data))
print(res_map) # [1, 4, 9, 16, 25]
# 2. filter: 保留偶数
# Java: list.stream().filter(x -> x%2==0).toList()
res_filter = list(filter(lambda x: x % 2 == 0, data))
print(res_filter) # [2, 4]
# 3. sorted: 按绝对值排序
# Java: list.stream().sorted(Comparator.comparingInt(Math::abs)).toList()
nums = [-5, 2, -10, 1]
res_sorted = sorted(nums, key=lambda x: abs(x))
print(res_sorted) # [1, 2, -5, -10]
3. 综合案例
本章节用到前面已经讲过的内容(list / dict / set / tuple、函数、lambda 排序、setdefault 分组等),尽量做到短小、清晰、可直接运行。
graph TD
A["准备数据结构 list/dict/set/tuple"] --> B["封装函数:add/update/report"]
B --> C["用推导式/循环统计"]
C --> D["用 sorted + lambda 排序"]
C --> E["用 setdefault 分组"]
D --> F["打印/返回结果"]
E --> F
3.1 成绩小助手(列表+字典+排序+分组)
需求:
- 保存学生成绩
- 修改成绩
- 统计:平均分、及格名单、Top3、分组(及格/不及格)
# 学生列表:每个元素是一个字典,保存 name / score
students = [
{"name": "Tom", "score": 80},
{"name": "Jerry", "score": 59},
{"name": "Spike", "score": 100},
]
def add_student(students, name, score):
# 新增一条学生数据(列表是可变对象,函数内修改会影响外部)
students.append({"name": name, "score": score})
def update_score(students, name, new_score):
# 按姓名遍历查找,找到后修改分数并返回 True;找不到返回 False
for s in students:
if s["name"] == name:
s["score"] = new_score
return True
return False
def report(students):
# 约定:空列表时直接返回“空报表”
if not students:
return {"avg": 0, "pass_names": [], "top3": [], "group": {}}
# 平均分:sum + 生成器表达式
avg = sum(s["score"] for s in students) / len(students)
# 及格名单:列表推导式
pass_names = [s["name"] for s in students if s["score"] >= 60]
# Top3:sorted + lambda,按 score 倒序取前三
top3 = sorted(students, key=lambda s: s["score"], reverse=True)[:3]
# 分组:setdefault 经典用法
group = {}
for s in students:
key = "pass" if s["score"] >= 60 else "fail"
group.setdefault(key, []).append(s["name"])
return {"avg": round(avg, 2), "pass_names": pass_names, "top3": top3, "group": group}
# 测试:新增一个学生
add_student(students, "Tyke", 70)
# 测试:把 Jerry 的分数从 59 改成 61(刚好及格)
update_score(students, "Jerry", 61)
result = report(students)
print("avg:", result["avg"])
print("pass:", result["pass_names"])
print("top3:", [(s["name"], s["score"]) for s in result["top3"]])
print("group:", result["group"])
3.2 购物清单结算(列表+元组+字典+集合)
需求:
- 价格表用字典保存
- 购物清单用列表保存(元素用元组表示)
- 结算总价,顺便找出“清单里有没有价格表里不存在的商品”
# price:商品 -> 单价
price = {"apple": 3.0, "banana": 2.5, "milk": 6.0}
# cart:每一项是 (商品名, 数量)
cart = [("apple", 2), ("banana", 3), ("milk", 1), ("cola", 1)]
def calc_total(price, cart):
total = 0.0
for name, qty in cart:
if name in price:
# 命中价格表:参与结算
total += price[name] * qty
# 未命中价格表:跳过(也可以选择抛异常/记录错误,这里保持简单)
return round(total, 2)
def missing_items(price, cart):
# 用集合差集找出“购物车里有、价格表里没有”的商品
names_in_cart = {name for name, _ in cart}
names_in_price = set(price.keys())
return sorted(list(names_in_cart - names_in_price))
print("total:", calc_total(price, cart))
print("missing:", missing_items(price, cart))
Java 对比:
list[dict]的结构在 Java 里通常对应List<Map<String, Object>>或者更推荐用List<Student>(用类建模);sorted(..., key=...)的思想类似 Java 的Comparator.comparing(...);setdefault的分组写法,对应 Java 常见的Map.computeIfAbsent(key, k -> new ArrayList<>());- 集合差集
A - B类似 Java 里对Set做removeAll(注意会修改原集合),或用新集合计算差集。
评论