一、函数、参数与返回值
函数可以先理解成:把一段可重复使用的功能封装起来,需要时再调用。
函数的基本定义
最小语法骨架:
def 函数名(参数1, 参数2, ...):
函数体
return 返回值
def用来定义函数- 参数用于接收外部传入的数据
return用于把结果返回给调用方
例如:
def greet(name, greeting="hello"):
return f"{greeting}, {name}"
message = greet("alex")
print(message)
函数按参数和返回值分类
1. 无参数、无返回值
def greet() -> None:
print("您好")
greet()
2. 有参数、无返回值
def greet(name: str) -> None:
print(name + ",您好")
greet("张老师")
greet("赵老师")
3. 有参数、有返回值
def greet(name: str) -> str:
return name + ",您好"
result = greet("张老师")
print(result)
4. 多返回值
def parse_user() -> tuple[str, int]:
return "alex", 18
name, age = parse_user()
print(name, age) # alex 18
二、函数签名里的类型提示
学函数时,最适合顺手把类型提示一起掌握,因为它最常写在函数签名上。
def greet(name: str) -> str:
return f"Hello, {name}"
def find_user(user_id: int) -> str | None:
if user_id == 1:
return "alex"
return None
def log_message(message: str) -> None:
print(message)
这里最重要的是:
- 参数类型写在
:后面 - 返回值类型写在
->后面 - 可能为空就写
str | None - 没有返回值就写
-> None
容器类型怎么写
如果函数参数或返回值是容器,常见写法是:
scores: list[int] = [90, 85, 92]
user: dict[str, int] = {"id": 1, "age": 18}
def parse_user() -> tuple[str, int]:
return "alex", 18
也就是把“容器本身的类型”和“里面元素的类型”一起写出来。
写到什么程度就够了
入门阶段优先保证两件事:
- 给函数参数和返回值加注解
- 在类型不明显时,再给变量补充注解
如果局部变量一眼就能看懂,通常没必要每个都写:
name = "alex"
total = 100
另外,Any 表示“任意类型”,但它会明显削弱检查效果,能不用就尽量不用。
配合 mypy 做静态检查
类型提示真正的价值,不只是“看起来更清楚”,还在于能配合工具提前发现明显错误。
pip install mypy
mypy script.py
例如下面这段代码,mypy 会在运行前提示参数类型不对:
def add(a: int, b: int) -> int:
return a + b
add("hello", "world")
三、全局变量和局部变量
变量的作用域,指的是变量在程序里“哪些地方能用、哪些地方不能用”。
- 在函数外定义的变量,通常叫全局变量
- 在函数内定义的变量,通常叫局部变量
num = 10
name = "Python"
def fun() -> None:
local_num = 100
num = 200
print("函数内部访问全局变量 name:", name)
print("函数内部访问局部变量 local_num:", local_num)
print("函数内部同名局部变量 num:", num)
fun()
print("函数外部访问全局变量 num:", num)
# print(local_num) # 会报错,函数外部不能直接访问局部变量
这个例子同时说明了三件事:
- 函数内部通常可以读取外部的全局变量,比如
name - 函数内部定义的局部变量,只能在函数内部使用,比如
local_num - 如果局部变量和全局变量同名,比如这里的
num,那么函数内部优先使用局部变量,函数外部的全局变量不受影响
如果你确实要在函数内部修改全局变量,需要显式使用 global:
count = 0
def increase() -> None:
global count
count += 1
increase()
print(count) #1
位置传参与关键字传参
1. 位置参数
def user_info(name: str, age: int, address: str) -> None:
print(f"我的名字{name},今年{age}岁了,家里住在{address}")
user_info("Tom", 23, "美国纽约")
位置参数要求:实参顺序要和形参顺序保持一致。
2. 关键字参数
def user_info(name: str, age: int, address: str) -> None:
print(f"我的名字{name},今年{age}岁了,家里住在{address}")
user_info(name="Tom", age=23, address="美国纽约")
关键字参数的特点是可读性更强,调用时不容易把顺序写错。
默认参数
默认参数指的是:在定义函数时,先给某个参数准备一个默认值。调用时如果不传,就使用默认值。
def user_info(name: str, age: int, gender: str = "男") -> None:
print(f"我的名字{name},今年{age}岁了,我的性别为{gender}")
user_info("李林", 25)
user_info("姗儿", 18, "女")
注意: 默认参数一定要写在普通参数后面
可变参数
可变参数也常被叫作“不定长参数”:
- 在函数定义处,
*args/**kwargs用来收集不固定数量的参数 - 在函数调用处,
*/**用来把已有对象拆开后再传给函数
适合的种场景:
- 调用函数时,参数个数不固定
- 你事先不知道会传几个位置参数
- 或者事先不知道会传哪些关键字参数
1. 定义处的 *args 和 **kwargs
*args用来接收额外的位置参数,接收后是元组**kwargs用来接收额外的关键字参数,接收后是字典
def collect_scores(*scores: int, **metadata: str) -> None:
print("scores:", scores)
print("metadata:", metadata)
collect_scores(90, 95, 88, student="tom", course="python")
#scores: (90, 95, 88)
#metadata: {'student': 'tom', 'course': 'python'}
2. 调用处的 * 和 **
当星号出现在函数调用处时,含义就变了:
*会把列表、元组等可迭代对象拆成位置参数**会把字典拆成关键字参数
def show_args(*args: object, **kwargs: object) -> None:
print(args)
print(kwargs)
values = (1, 2, 4)
items = [11, 15, 23]
data = {"a": 1, "b": 12}
show_args(values, data)
show_args(*values, **data)
show_args(items, data)
show_args(*items, **data)
输出:
((1, 2, 4), {'a': 1, 'b': 12})
{}
(1, 2, 4)
{'a': 1, 'b': 12}
([11, 15, 23], {'a': 1, 'b': 12})
{}
(11, 15, 23)
{'a': 1, 'b': 12}
关键区别是:
show_args(values, data)
这里是把 values 和 data 当作两个普通参数传入。
show_args(*values, **data)
这里才是先“拆开”,再传给函数。
3. 几个容易踩的点
args 不能直接修改
因为 args 本质上是元组,而元组不可变。
def update_first(*args: int) -> None:
args[0] = 5
update_first(1, 2, 3)
#报错:TypeError: 'tuple' object does not support item assignment
即使你传进去的是列表:
numbers = [1, 2, 3]
update_first(*numbers)
进入函数以后,它依然会被收集成元组。
*dict 传的是字典的键
def show_data(*args: object, **kwargs: object) -> None:
print(args)
print(kwargs)
data = {"a": 1, "b": 2, "c": 3}
show_data(*data)
show_data(**data)
输出类似:
('a', 'b', 'c')
{}
()
{'a': 1, 'b': 2, 'c': 3}
也就是说:
*data传入的是字典的键**data传入的才是键值对
4. 什么时候值得用它们
适合使用 *args / **kwargs 的场景:
- 写包装函数或装饰器
- 函数需要兼容不固定参数
- 需要把参数继续转发给其他函数
例如:
def wrapper(*args: object, **kwargs: object) -> object:
print("before call")
return target(*args, **kwargs)
这类场景在装饰器、日志包装和框架代码里都非常常见。
实战建议:
- 不要把
*args/**kwargs当成“高级语法炫技” - 能写清晰签名时,优先写明确参数
- 只有在“参数数量不固定”或“需要透传参数”时再使用它们
- 遇到字典解包时,先确认你到底需要键,还是需要键值对
lambda 表达式
lambda 经常被叫作匿名函数。它适合写那种“逻辑很短、只想临时用一下”的小函数。
最基本的形式可以记成:
lambda 参数列表: 返回值表达式
1. 普通参数
add = lambda a, b: a + b
print(add(1, 2)) # 3
2. 带默认值参数
calc = lambda a, b, c=100: a + b + c
print(calc(10, 20)) # 130
3. 接收可变位置参数
collect = lambda *args: args
print(collect(10, 20, 30)) # (10, 20, 30)
4. 接收可变关键字参数
collect = lambda **kwargs: kwargs
print(collect(name="python", age=20))
# {'name': 'python', 'age': 20}
初学阶段先记住两个特点就够了:
lambda一般只写一个表达式- 它会把这个表达式的结果直接作为返回值
四、模块入门:模块、包与导入方式
学到这里,除了会写函数,还要开始知道:一个 .py 文件在项目里到底扮演什么角色。
模块和包怎么区分
- 一个
.py文件通常就可以看作一个模块 - 一个目录如果用来组织多个模块,通常就可以看作一个包
例如:
demo_project/
├── app.py
├── utils.py
└── services/
├── __init__.py
└── user.py
这里可以这样理解:
app.py、utils.py是模块services是包services/__init__.py是包的初始化文件,也说明这个目录是一个 Python 包services.user表示包里的模块
import 和 from ... import ...
两种写法的差别主要在可读性与命名空间控制。
如果按课堂入门资料的角度理解,可以先记一句话:模块本质上通常就是一个 Python 文件。
最常见的两种导入方式如下。
方式 1:import 模块名
import random
print(random.randint(1, 10))
这种写法的特点是:调用时要带上模块名,像 random.randint(...)。
方式 2:from 模块名 import 名称
from random import randint
print(randint(1, 10))
这种写法的特点是:调用更短,但当前作用域里更容易出现命名冲突。
经验上可以这样理解:
import module:更清楚,适合长期维护代码from module import name:调用更短,但更容易和本地变量重名as可以为模块或对象起别名
__all__ 是做什么的
__all__ 通常出现在模块或包的 __init__.py 里,用来声明“当别人使用 from xxx import * 时,哪些名字应该被导出”。
例如:
# services/__init__.py
from .user import get_user
from .order import create_order
__all__ = ["get_user", "create_order"]
如果其他文件这样写:
from services import *
那么当前作用域里通常只会导入 __all__ 中列出的 get_user 和 create_order。
可以先把它理解成两点:
__all__主要影响from module import *这种批量导入写法- 它常用于包的统一导出,让调用方不必关心内部模块拆分
不过要注意:
__all__不是“权限控制”,别人仍然可以显式写from services.user import get_user- 日常业务代码里通常不推荐大量使用
import *,因为可读性差,也容易污染命名空间 - 但在教学示例、包对外暴露公共 API、或框架封装里,
__all__仍然是一个常见知识点
如果结合前面的目录结构,可以把它理解成:services/__init__.py 不只是包初始化文件,也可以顺手定义这个包“默认希望暴露给外部使用的名字”。
__name__ == "__main__" 是什么
很多脚本都会写这一段:
def main() -> None:
print("run script")
if __name__ == "__main__":
main()
它的作用是:
- 直接运行当前文件时,执行
main() - 作为模块被别的文件导入时,不自动执行这段脚本入口逻辑
这在写命令行脚本、临时工具和小型项目入口时非常常见。
五、文件操作与 with
文件打开模式:
r:读取文本文件w:写入文本文件,原内容会被覆盖a:向文本文件末尾追加内容rb:以二进制方式读取文件,例如图片、音频、压缩包wb:以二进制方式写入文件,原内容会被覆盖ab:以二进制方式追加内容
使用 with 可以在用完文件后自动关闭它。
这里的 b 表示按二进制数据处理,不按文本处理。
可以先这样记:
- 文本模式读到的是
str - 二进制模式读到的是
bytes - 文本模式通常要关心
encoding - 二进制模式通常不写
encoding
读写文件:
from pathlib import Path
file_path = Path("example.txt")
# 写入文件,w 模式会覆盖原内容。使用 with 语句可以自动管理文件的打开和关闭。
with file_path.open("w", encoding="utf-8") as file:
file.write("Hello Python\n")
# 读取文件,r 模式为只读。
with file_path.open("r", encoding="utf-8") as file:
# 逐行读取文件内容,避免一次性加载大文件导致内存占用过高。
for line in file:
print(line.strip())
文本替换示例:
from pathlib import Path
source = Path("yesterday.txt")
target = Path("yesterday.txt.bak")
find_text = "旧内容"
replace_text = "新内容"
# 同时打开源文件(读)和目标文件(写)
with source.open("r", encoding="utf-8") as src, target.open("w", encoding="utf-8") as dst:
for line in src:
# 找到目标片段后再替换,其余内容原样写回。
dst.write(line.replace(find_text, replace_text))
b、rb、wb、ab 是什么
当文件内容不是“普通文本”,而是图片、PDF、视频、压缩包这类原始字节数据时,就应该使用二进制模式。
from pathlib import Path
source = Path("avatar.png")
target = Path("avatar-copy.png")
# rb: 以二进制方式读取,返回 bytes
with source.open("rb") as file:
data = file.read()
# wb: 以二进制方式写入,覆盖原内容
with target.open("wb") as file:
file.write(data)
# ab: 以二进制方式追加到文件末尾
with target.open("ab") as file:
file.write(b"\n")
注意:
- 如果是文本文件,优先使用
r、w、a - 如果是图片、音频、压缩包等非文本文件,优先使用
rb、wb、ab - 不要在二进制模式下传
encoding="utf-8" - 不要把文本模式和二进制模式混着理解
六、异常处理基础
很多初学者一开始只会“让代码跑起来”,但真实程序还必须处理失败情况。
文件不存在、输入格式错误、网络超时、字典缺键,这些都属于异常处理要面对的问题。
try / except / else / finally
try:
number = int(input("请输入数字: "))
except ValueError:
print("输入的不是合法整数")
else:
print("你输入的是:", number)
finally:
print("这段代码总会执行")
可以这样理解:
try:放可能出错的代码except:出错后怎么处理else:没有异常时执行finally:无论是否异常,最后都会执行
文件读取时为什么经常要配合异常处理
因为文件系统属于外部环境,不完全受程序控制,所以即使代码本身没问题,读文件时也可能失败:
- 文件不存在(
FileNotFoundError) - 权限不足(
PermissionError) - 文件被占用、锁定或损坏
如果不做异常处理,程序就可能直接报错退出;使用 try ... except 可以捕获这些外部失败,并给出备用逻辑或更友好的提示。
from pathlib import Path
file_path = Path("config.txt")
try:
content = file_path.read_text(encoding="utf-8")
except FileNotFoundError:
print("配置文件不存在")
else:
print(content)
一个实用原则
- 预期内的失败,要明确处理
- 不要用裸
except:把所有异常一把吞掉 - 暂时处理不了的异常,可以记录后继续向上抛出
小结
这一篇重点是建立从“会写语句”到“会写函数”再到“会处理模块、文件与异常”的最小能力:
- 能写带参数、带返回值、带默认值的函数
- 理解全局变量与局部变量的边界,知道何时用
global - 理解
*args/**kwargs的收集作用,以及调用处*/**的解包作用 - 区分模块与包,掌握
import和from ... import的取舍 - 熟练使用
with做文件读写,理解r、w、a、b这些常见模式 - 理解
try / except / else / finally各自的角色,知道外部操作一定会失败
类、对象、__init__()、__new__()、__call__() 这些面向对象内容,统一放到《Python 进阶特性》继续展开。
把这些基础打牢后,再进入参数解包、路径处理、日志、网络请求这些主题时,就会顺很多。