hidewood

博客园 首页 新随笔 联系 订阅 管理

模块

随着程序越写越大,如果所有代码都放在一个 .py 文件里,会越来越难维护。

Python 使用模块和包来组织代码:

  • 一个 .py 文件就是一个模块(module)
  • 一个包含多个模块的目录可以组成包(package)
  • 模块可以被其他模块导入和复用

模块的基本概念

什么是模块

一个 Python 文件就是一个模块。

例如:

abc.py

它就是一个名为 abc 的模块。

再例如:

hello.py

它就是一个名为 hello 的模块。

在其他代码里可以通过 import 导入模块:

import hello

模块的好处:

  • 提高代码可维护性
  • 方便代码复用
  • 避免把所有函数和变量堆在一个文件里
  • 不同模块中可以存在同名函数或变量

模块命名规范

创建自己的模块时,建议遵守:

  • 文件名使用小写字母
  • 多个单词用下划线连接,例如 user_utils.py
  • 不要使用中文、空格和特殊字符
  • 不要和 Python 内置模块同名,例如 sys.pytime.pyrandom.py

错误示例:

sys.py
random.py
my module.py
学生管理.py

推荐示例:

user.py
user_utils.py
student_manager.py

如果你把自己的文件命名为 random.py,再写:

import random

Python 可能会先导入你自己的 random.py,而不是标准库里的 random 模块,从而造成非常隐蔽的问题。

为什么需要包

如果不同人都写了一个 utils.py,模块名就可能冲突。

为了解决模块名冲突,Python 使用包来组织模块。

例如:

mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py

此时:

  • abc.py 的完整模块名是 mycompany.abc
  • xyz.py 的完整模块名是 mycompany.xyz
  • mycompany 是一个包

导入方式:

import mycompany.abc

或者:

from mycompany import abc

__init__.py

传统 Python 包目录中通常会有一个 __init__.py 文件:

mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py

__init__.py 可以是空文件,也可以写 Python 代码。

它的作用:

  • 标记当前目录是一个普通 Python 包
  • 包被导入时,__init__.py 会先执行
  • 可以在里面定义包级别变量或导入常用对象

例如:

# mycompany/__init__.py
__version__ = '1.0.0'

使用:

import mycompany

print(mycompany.__version__)

补充说明:现代 Python 支持“命名空间包”,某些目录没有 __init__.py 也可能被导入。但初学阶段建议创建普通包时保留 __init__.py,更清晰,也更符合大多数教程和项目结构。

多级包

包可以有多级目录:

mycompany
├─ __init__.py
├─ abc.py
├─ utils.py
└─ web
   ├─ __init__.py
   ├─ utils.py
   └─ www.py

各模块完整名称:

mycompany.abc
mycompany.utils
mycompany.web
mycompany.web.utils
mycompany.web.www

注意:

mycompany.web

本身也是一个模块,它对应的文件是:

mycompany/web/__init__.py

使用模块

模块文件模板

下面是一个简单的 hello.py 模块:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""A test module."""

__author__ = 'Michael Liao'

import sys


def test():
    args = sys.argv
    if len(args) == 1:
        print('Hello, world!')
    elif len(args) == 2:
        print('Hello, %s!' % args[1])
    else:
        print('Too many arguments!')


if __name__ == '__main__':
    test()

说明:

  • #!/usr/bin/env python3:在 Unix/Linux/macOS 中可以让脚本直接运行
  • # -*- coding: utf-8 -*-:声明文件编码,现代 Python 3 默认就是 UTF-8,但写上也没问题
  • """A test module.""":模块文档字符串,可通过 hello.__doc__ 查看
  • __author__:模块作者信息
  • import sys:导入标准库模块 sys
  • if __name__ == '__main__'::只在当前文件被直接运行时执行代码

import

导入模块:

import sys

导入后,可以通过模块名访问模块里的变量、函数和类:

import sys

print(sys.argv)
print(sys.path)

这种写法的优点是来源清楚:

sys.argv

一眼就能看出 argv 来自 sys 模块。

from ... import ...

也可以从模块中导入某个对象:

from sys import argv

print(argv)

导入多个对象:

from math import sqrt, pi

print(sqrt(9))
print(pi)

这种写法更短,但如果导入太多名字,容易造成命名冲突。

import ... as ...

可以给模块或对象起别名:

import numpy as np

这在第三方库中很常见。

也可以给函数起别名:

from math import sqrt as square_root

print(square_root(16))

不推荐使用 from module import *

下面这种写法会导入模块里的很多公开名字:

from math import *

不推荐初学者使用,因为你很难看出当前文件中的变量或函数来自哪里,也容易覆盖已有名字。

更推荐:

import math

print(math.sqrt(9))

或者:

from math import sqrt

print(sqrt(9))

sys.argv

sys.argv 是命令行参数列表。

例如有文件 hello.py

import sys


def test():
    args = sys.argv
    print(args)


if __name__ == '__main__':
    test()

在命令行运行:

python hello.py

可能输出:

['hello.py']

运行:

python hello.py Michael

可能输出:

['hello.py', 'Michael']

也就是说:

  • sys.argv[0] 通常是脚本文件名
  • sys.argv[1] 开始才是用户传入的参数

改成问候程序:

import sys


def test():
    args = sys.argv
    if len(args) == 1:
        print('Hello, world!')
    elif len(args) == 2:
        print('Hello, %s!' % args[1])
    else:
        print('Too many arguments!')


if __name__ == '__main__':
    test()

运行效果:

python hello.py
# Hello, world!

python hello.py Michael
# Hello, Michael!

__name__ == '__main__'

这是模块中非常重要的写法。

if __name__ == '__main__':
    test()

含义是:

  • 如果当前文件被直接运行,__name__ 的值是 '__main__'
  • 如果当前文件被其他文件导入,__name__ 的值是模块名

例如 hello.py

def test():
    print('hello')


if __name__ == '__main__':
    test()

直接运行:

python hello.py

会执行 test()

如果在另一个文件中导入:

import hello

不会自动执行 test()

这样做的好处是:

  • 模块可以被直接运行,用于测试
  • 模块也可以被导入复用,不会自动执行测试代码

作用域

公开变量和私有变量

在模块中,我们可能会定义很多函数和变量。有些希望给外部使用,有些只希望模块内部使用。

Python 主要通过命名约定表达作用域意图。

公开名字:

PI = 3.14


def area(radius):
    return PI * radius * radius

这些名字可以被外部直接使用。

特殊变量:

__author__ = 'Michael Liao'
__name__
__doc__

这类前后双下划线的名字通常有特殊含义。我们自己的普通变量不要随便写成 __xxx__

非公开名字:

_private_name = 'internal'


def _helper():
    pass

一个下划线开头表示:这是模块内部使用的名字,不建议外部直接访问。

注意:Python 并不会真正禁止你访问 _helper()。它只是一个约定。

封装内部逻辑

例如:

def _private_1(name):
    return 'Hello, %s' % name


def _private_2(name):
    return 'Hi, %s' % name


def greeting(name):
    if len(name) > 3:
        return _private_1(name)
    return _private_2(name)

外部只需要调用:

greeting('Bob')

不需要关心 _private_1()_private_2() 的内部细节。

这就是封装:

  • 外部只暴露必要接口
  • 内部实现可以自由调整
  • 调用方不需要知道内部细节

__all__

如果模块中定义了 __all__,它会影响:

from module import *

例如:

__all__ = ['greeting']


def greeting(name):
    return 'Hello, %s' % name


def _helper():
    return 'internal'

当别人写:

from module import *

只会导入 __all__ 中列出的公开名字。

初学阶段可以先知道这个机制,不必急着大量使用。

安装第三方模块

Python 标准库自带很多模块,比如:

  • sys
  • os
  • time
  • datetime
  • math
  • random

除了标准库,还可以安装第三方模块。

第三方模块通常通过 pip 安装。

pip 基本用法

安装模块:

python -m pip install Pillow

查看已安装模块:

python -m pip list

升级模块:

python -m pip install --upgrade Pillow

卸载模块:

python -m pip uninstall Pillow

为什么推荐写 python -m pip

因为它能确保使用的是当前这个 python 对应的 pip,减少多版本 Python 环境中装错位置的问题。

示例:Pillow

Pillow 是常用的图像处理库。

安装:

python -m pip install Pillow

使用:

from PIL import Image

img = Image.open('test.jpg')
print(img.size)

如果导入时报错:

ModuleNotFoundError: No module named 'PIL'

通常说明:

  • Pillow 没有安装
  • 安装到了另一个 Python 环境
  • 当前虚拟环境没有激活

requirements.txt

项目中通常会用 requirements.txt 记录依赖:

Pillow
requests

安装依赖:

python -m pip install -r requirements.txt

导出当前环境依赖:

python -m pip freeze > requirements.txt

注意:pip freeze 会导出当前环境里几乎所有包。学习阶段可以手动维护一个简洁的 requirements.txt

虚拟环境

不同项目可能依赖不同版本的第三方库。为了避免互相影响,通常会给每个项目创建虚拟环境。

创建虚拟环境:

python -m venv .venv

Windows PowerShell 激活:

.\.venv\Scripts\Activate.ps1

激活后安装依赖:

python -m pip install requests

退出虚拟环境:

deactivate

虚拟环境的好处:

  • 不污染系统 Python
  • 不同项目依赖相互隔离
  • 更容易复现项目环境

模块搜索路径

当执行:

import mymodule

Python 会按照一定路径搜索模块。

搜索路径保存在:

import sys

print(sys.path)

常见搜索位置包括:

  • 当前脚本所在目录
  • 标准库目录
  • 第三方库目录 site-packages
  • 环境变量 PYTHONPATH 指定的目录

如果找不到模块,会报错:

ModuleNotFoundError: No module named 'mymodule'

临时添加搜索路径

可以在代码运行时修改 sys.path

import sys

sys.path.append(r'E:\Study\Python\my_scripts')

这种方式只在当前程序运行期间有效,程序结束后失效。

使用 PYTHONPATH

也可以设置环境变量 PYTHONPATH,让 Python 自动把指定目录加入搜索路径。

不过初学阶段更推荐:

  • 把文件放到合理的项目目录结构中
  • 在项目根目录运行程序
  • 使用包和模块的正常导入方式

不要在项目里到处手动修改 sys.path,否则项目结构会越来越难理解。

常见导入方式对比

import module

import math

print(math.sqrt(9))

优点:来源清楚,不容易冲突。

from module import name

from math import sqrt

print(sqrt(9))

优点:调用更短。

import module as alias

import numpy as np

优点:适合常用第三方库的约定别名。

from module import *

from math import *

不推荐。名字来源不清楚,容易冲突。

常见误区

  • 把自己的文件命名为标准库模块名,例如 sys.pyrandom.py
  • 导入模块时,以为模块中的所有代码都会“只导入函数”,其实顶层代码会执行
  • 忘记使用 if __name__ == '__main__':,导致模块一导入就运行测试代码
  • 在多个地方随意修改 sys.path
  • 安装第三方库时使用了错误的 Python 环境
  • 以为下划线开头的变量真的不能访问,其实它只是约定为非公开

本章综合小结

  • 一个 .py 文件就是一个模块
  • 包用于组织多个模块,避免模块名冲突
  • 普通包通常包含 __init__.py
  • import module 可以导入模块
  • from module import name 可以导入模块中的指定对象
  • __name__ == '__main__' 用于区分“直接运行”和“被导入”
  • 下划线开头的名字表示模块内部使用
  • 第三方模块通常用 python -m pip install 包名 安装
  • 虚拟环境可以隔离不同项目的依赖
  • sys.path 决定 Python 从哪些目录查找模块

练习题

下面练习按难度从低到高排列。

基础题

  1. 新建文件 hello.py,定义函数 say_hello(name),返回 'Hello, name!'
  2. 新建文件 main.py,导入 hello 模块并调用 hello.say_hello('Alice')
  3. hello.py 中加入:
if __name__ == '__main__':
    print(say_hello('World'))

分别直接运行 hello.py 和在 main.py 中导入它,观察区别。

  1. 编写模块 calc.py,包含 add(x, y)sub(x, y)mul(x, y)div(x, y) 四个函数。
  2. 在另一个文件中分别用下面几种方式导入并调用 calc.add()
import calc
from calc import add
import calc as c

包练习

  1. 创建如下目录结构:
tools
├─ __init__.py
├─ string_utils.py
└─ number_utils.py
  1. string_utils.py 中定义 trim_and_title(s),去掉首尾空格并把首字母大写。
  2. number_utils.py 中定义 is_even(n),判断是否为偶数。
  3. 在项目根目录新建 main.py,导入并使用:
from tools.string_utils import trim_and_title
from tools.number_utils import is_even
  1. tools/__init__.py 中定义 __version__ = '1.0.0',并在 main.py 中打印它。

作用域练习

  1. 在模块中定义 _format_name(name) 作为内部函数,再定义公开函数 greeting(name) 调用它。
  2. 尝试从外部访问 _format_name(),思考为什么能访问但不推荐。
  3. 给模块添加 __all__ = ['greeting'],观察 from module import * 的变化。

sys 和命令行参数练习

  1. 编写 args_demo.py,打印 sys.argv
  2. 编写 greet.py
    • 没有参数时输出 Hello, world!
    • 有一个参数时输出 Hello, 参数!
    • 参数过多时输出 Too many arguments!
  3. 编写 calc_cli.py,支持命令:
python calc_cli.py add 3 5
python calc_cli.py mul 4 6

输出对应计算结果。

pip 和环境练习

  1. 使用下面命令查看当前 Python 对应的 pip 版本:
python -m pip --version
  1. 使用下面命令查看已安装包:
python -m pip list
  1. 创建虚拟环境 .venv 并激活。
  2. 在虚拟环境中安装 requests,然后运行:
import requests

print(requests.__version__)

小项目练手

项目:命令行工具包

目标:把多个功能拆成模块和包,练习模块导入、包结构、__name__ == '__main__'、命令行参数和作用域。

项目结构:

mini_tools
├─ __init__.py
├─ text.py
├─ number.py
└─ cli.py
main.py

功能要求

  1. mini_tools/text.py 提供文本工具:
    • normalize_name(name):去掉首尾空格,首字母大写
    • is_palindrome(text):判断字符串是否回文
  2. mini_tools/number.py 提供数字工具:
    • is_even(n):判断偶数
    • factorial(n):计算阶乘
  3. mini_tools/cli.py 处理命令行参数。
  4. main.py 作为程序入口。

推荐代码

mini_tools/text.py

def normalize_name(name):
    name = name.strip()
    if not name:
        return ''
    return name[0].upper() + name[1:].lower()


def is_palindrome(text):
    text = str(text)
    return text == text[::-1]

mini_tools/number.py

def is_even(n):
    return n % 2 == 0


def factorial(n):
    if n < 0:
        raise ValueError('n must be >= 0')

    result = 1
    for x in range(1, n + 1):
        result = result * x
    return result

mini_tools/cli.py

from mini_tools.number import factorial, is_even
from mini_tools.text import is_palindrome, normalize_name


def run(args):
    if len(args) < 2:
        print('Usage:')
        print('  python main.py name alice')
        print('  python main.py palindrome level')
        print('  python main.py even 10')
        print('  python main.py factorial 5')
        return

    command = args[0]
    value = args[1]

    if command == 'name':
        print(normalize_name(value))
    elif command == 'palindrome':
        print(is_palindrome(value))
    elif command == 'even':
        print(is_even(int(value)))
    elif command == 'factorial':
        print(factorial(int(value)))
    else:
        print('Unknown command:', command)

main.py

import sys

from mini_tools.cli import run


def main():
    run(sys.argv[1:])


if __name__ == '__main__':
    main()

运行示例

python main.py name alice
# Alice

python main.py palindrome level
# True

python main.py even 10
# True

python main.py factorial 5
# 120

进阶要求

  • mini_tools/__init__.py 中添加 __version__
  • factorial() 添加更多参数检查
  • cli.py 的错误输入添加提示
  • 新增 mini_tools/file.py,实现读取文本文件行数
  • 添加 README.md 说明模块结构和运行方式

项目复盘

完成后重点回顾:

  • 哪些代码应该放在模块里?
  • 哪个文件是程序入口?
  • 为什么 main.py 里要写 if __name__ == '__main__':
  • 哪些函数是公开接口,哪些可以用下划线表示内部函数?
  • 如果某个模块导入失败,应该检查哪些搜索路径和文件名?
posted on 2026-05-15 16:49  hidewood  阅读(12)  评论(0)    收藏  举报