前言
Python 程序的基本积木只有两样东西:函数(function)把一段逻辑打包成可以 反复调用的单元,模块(module)把一个.py文件打包成可以反复导入的单元。 两者的关系是"层层套娃"——模块里装函数,包(package)里装模块。
初学者常有的误解有两个。第一,以为"模块必须是一个很正式的东西",其实任何 一个.py文件就是一个模块,你随手写的tools.py就是一个模块,import tools就能用它。第二,以为import只是"引用一下",不会产生副作用——实际上import会从头到尾执行目标模块的顶层语句,这决定了模块里哪些代码该写、 哪些该放进函数里。
本文讲清楚def的定义与调用规则、模块与导入的几种写法、以及__name__的 作用。所有写法都以 Python 3 为准,不涉及早已停止维护的 Python 2。
一、函数:定义与调用
用def定义函数,用"函数名加括号"调用。函数体靠缩进界定,return把结果 交回调用方。
# 适用于 Python 3.8+
def area(width, height):
"""返回矩形面积。"""
return width * height
result = area(3, 4)
print(result) # 12要点:
return是可选参数。没写return,或者只写return不带值,函数都
返回None。这不是"什么都不返回",而是"返回None"。
def是可执行语句。解释器执行到def那一行时,才创建函数对象并把
名字绑定上去。所以函数必须在调用之前定义(在模块顶层按从上到下的顺序)。
- 参数有五种形态:普通参数、带默认值的参数、
*args(收集多余位置参数)、
**kwargs(收集多余关键字参数)、以及*后面的仅关键字参数。默认值在函数定义时求值一次,这一点在"常见坑点"里单独说。
二、模块:一个文件就是一个模块
把上面那个函数单独存成shapes.py,它就变成一个模块。另一个文件里这样用:
# 适用于 Python 3.8+
import shapes
print(shapes.area(2, 5)) # 10import做三件事:找到shapes.py、执行它的顶层代码、把得到的模块对象 绑定到名字shapes上。之后用shapes.area访问里面的函数。
导入有多种写法,各有适用场景:
| 写法 | 调用方式 | 适用场景 |
|---|
import shapes | shapes.area() | 最推荐,来源清晰 |
import shapes as sp | sp.area() | 模块名太长 |
from shapes import area | area() | 只用一两个名字 |
from shapes import area as a | a() | 避免与本地名字冲突 |
from shapes import * | area() | 不推荐,污染名字空间 |
包(package)就是含有__init__.py的目录,里面可以放多个模块,用点号 分层导入,例如import mypkg.shapes。从 Python 3.3 起还有"命名空间包", 不一定需要__init__.py,但初学阶段老老实实放一个__init__.py最不容易 出问题。
三、模块的执行与 __name__
前面说过import会执行模块的顶层代码。那如果这个模块既想被导入、又想在 命令行直接运行时执行一些额外逻辑,怎么办?答案是判断__name__。
__name__是每个模块都有的变量:被导入时,它的值就是模块名;作为脚本直接 运行时,它的值是字符串"__main__"。于是就有了那句著名的守卫:
# 适用于 Python 3.8+
# 文件 shapes.py
def area(width, height):
return width * height
if __name__ == "__main__":
print(area(3, 4))- 直接
python shapes.py运行:__name__ == "__main__"成立,会打印 12。 - 在别的文件里
import shapes:__name__ == "shapes",那段print**不会
执行**,只把area导入进来。
这就是"主入口"的含义:把只应该在直接运行时做的事,放进if __name__ == "__main__":里。背后的机制是:直接运行的那个脚本文件,其模块名被设为"__main__"。
四、把函数和模块组织好的几条经验
- 一个模块负责一件事。
shapes.py就只放几何相关的函数,不要塞进数据库
连接、字符串工具等无关内容。
- 顶层只放定义,不放动作。顶层写
def、class、常量赋值是安全的;
写"打印、读写文件、发网络请求"这类动作就要小心,因为导入方一import就会执行。
- 函数尽量小而单一。一个函数只做一件事,参数和返回值都清晰,测试和复用
都更容易。
- 数据用参数传,不要藏在全局变量里。全局可变状态会让函数行为随调用
顺序变化,很难排查。
# 适用于 Python 3.8+
# tools.py
def normalize(text):
"""去掉首尾空白并转小写。"""
return text.strip().lower()
def split_words(text):
"""按空白切成单词列表。"""
return normalize(text).split()# 适用于 Python 3.8+
# main.py
from tools import split_words
print(split_words(" Hello World ")) # ['hello', 'world']五、函数签名速查
| 语法 | 名称 | 行为 |
|---|
def f(a, b) | 普通参数 | 必须传,可按位置或关键字 |
def f(a=1) | 带默认值 | 可省略,省略时用 1 |
def f(*args) | 可变位置参数 | 多余的按位置参数收成元组 |
def f(**kwargs) | 可变关键字参数 | 多余的关键字参数收成字典 |
def f(a, *, b) | 仅关键字参数 | b只能写b=...传 |
def f(a, /, b) | 仅位置参数 | a只能按位置传(3.8+) |
def f(a, /, b)的/是Python 3.8 才引入的(PEP 570)。在 3.7 及更早 版本里写/会直接语法错误。仅位置参数常用于内建函数那种"参数名不重要、 不该按关键字传"的接口。
常见坑点
- 忘了 return,结果拿到 None
``python # ❌ 只打印不返回,外面拿到的是 None def double(x): print(x * 2) # ✅ 用 return 把结果交出去 def double(x): return x * 2``
- 函数在定义之前调用
``python # ❌ def 还没执行到,名字不存在 foo() def foo(): pass # ✅ 先定义后调用 def foo(): pass foo()``
- 导入模块时执行了不该执行的动作
``python # ❌ 顶层直接干活,一 import 就触发 # report.py print("正在生成报表...") # ✅ 放进守卫或函数里 if __name__ == "__main__": print("正在生成报表...")``
- **用 from x import * 造成名字覆盖**
``python # ❌ 不知道导入了什么,可能覆盖本地名字 from math import * # ✅ 只导入用到的 from math import sqrt, pi``
- 循环导入
``python # ❌ a.py 导入 b,b.py 又导入 a,可能 ImportError # ✅ 把共用部分抽到第三个模块;或在函数内部延迟导入 def need_b(): import b # 延迟到调用时才导入 return b.something()``
- 把可变数据挂在模块顶层被到处改
``python # ❌ 谁都来改 CACHE,行为随调用顺序漂移 CACHE = [] def add(x): CACHE.append(x) # ✅ 用参数传,或封装进类里管理``
- 函数里给模块级变量赋值忘了 global
``python COUNT = 0 # ❌ 有赋值语句,COUNT 被当成局部变量,读取时未绑定 → UnboundLocalError def bump(): COUNT = COUNT + 1 # ✅ 声明 global 后改的才是模块级那个 def bump(): global COUNT COUNT += 1``
- 默认参数写成可变对象
``python # ❌ lst 只在定义时创建一次,会跨调用累积 def add_item(x, lst=[]): lst.append(x) return lst # ✅ 用 None 当哨兵 def add_item(x, lst=None): if lst is None: lst = [] lst.append(x) return lst``
总结
| 概念 | 关键点 |
|---|
| 函数 | def是可执行语句,必须在调用前定义 |
| 返回值 | 没有return就返回None |
| 参数 | 普通 / 默认 /*args/**kwargs/*// |
| 模块 | 一个.py文件就是一个模块 |
| 导入 | import x会执行x的顶层代码 |
| 主入口 | if __name__ == "__main__":区分脚本与导入 |
把"函数负责一件事、模块顶层只做定义、导入不做副作用"这三条原则记牢,代码结构 就不会乱。参数形态和__name__是最容易记混的两处,动手写几个小模块跑一跑 就能形成稳定的直觉。