python中如何组织项目工程文件
一、项目工程文件目录
一个典型的Python项目工程目录结构可以帮助你更好地组织代码、资源和测试,从而使得项目更加清晰和易于维护。
my_project/
│
├── my_project/ # 项目的主代码包
│ ├── __init__.py # 包初始化文件
│ ├── module_1.py # 示例模块1
│ └── module_2.py # 示例模块2
│
├── tests/ # 测试代码目录
│ ├── __init__.py # 测试包初始化文件
│ ├── test_module_1.py # 模块1的单元测试
│ └── test_module_2.py # 模块2的单元测试
│
├── docs/ # 项目文档
│ └── ... # 文档文件或子目录
│
├── setup.py # 构建和安装脚本
├── pyproject.toml # 定义构建系统要求(PEP 518)
├── requirements.txt # 列出项目依赖
├── README.md # 项目说明文档
├── LICENSE # 许可证信息
└── .gitignore # Git版本控制忽略规则
目录和文件说明
- my_project/:这是你的Python项目的主要源代码所在的地方。每个模块可以是单独的.py文件或者更复杂的子包。
- tests/:这个目录用于存放所有与项目相关的测试代码。保持测试代码独立于源代码有助于保持代码的整洁性。
- docs/:这里存放项目的文档资料,可以包括使用指南、API文档等。
- setup.py 和 pyproject.toml:这两个文件用于定义如何打包你的项目以及它的依赖关系。setup.py是传统的配置文件,而pyproject.toml是根据PEP 518引入的新标准。
- requirements.txt:列出项目依赖的所有外部库及其版本,便于在其他环境中复现项目的依赖环境。
- README.md:提供项目的简要介绍、安装步骤、使用方法等信息。
- LICENSE:包含项目的开源许可协议文本。
- .gitignore:指定Git不应跟踪的文件模式,通常包括编译生成的文件、日志文件、本地配置等。
二、模块
在初步学习python之前我们基本上是用命令行通过 python 解释器来编程,如果你从 Python 解释器退出再进入,那么你定义的所有的方法和变量就都消失了。
为此 Python 提供了一个办法,把这些定义存放在文件中,为一些脚本或者交互式的解释器实例使用,这个文件被称为模块。一个.py文件就称之为一个模块(Module)。
模块的作用
- 代码复用:将常用的功能封装到模块中,可以在多个程序中重复使用。
- 命名空间管理:模块可以避免命名冲突,不同模块中的同名函数或变量不会互相干扰。
- 代码组织:将代码按功能划分到不同的模块中,使程序结构更清晰。
1.典型python文件
#!/usr/bin/python3
# 文件名: hello.py
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
print('命令行参数如下:')
for i in sys.argv:
print(i)
print('\n\nPython 路径为:', sys.path, '\n')
Shebang (#! /usr/bin/python3):
这一行被称为Shebang或hashbang,它位于文件的绝对开头。它的作用是指定该脚本使用的解释器路径。在这个例子中,#!/usr/bin/python3告诉操作系统这个脚本应该使用Python 3来执行。这对于在Unix、Linux和macOS等系统上直接运行脚本特别有用。
编码声明 (# – coding: utf-8 --):
这行注释指定了源代码文件的字符编码格式为UTF-8。虽然Python 3默认使用UTF-8编码,但明确指定编码可以避免一些潜在的编码问题,尤其是在处理非ASCII字符时。
模块文档字符串 (’ a test module '):
紧接在文件头部之后的是一个字符串字面量,它通常被用作模块级别的文档字符串(docstring)。这是对整个模块的目的和功能的简短描述,有助于其他开发者快速了解该模块的作用。
作者信息 (author = ‘Michael Liao’):
__author__
是一个特殊变量,用于标识脚本或模块的作者。这是一种约定俗成的做法,尽管不是必须的,但它有助于识别模块的创作者或维护者。
导入模块 (import sys):
import sys语句用于导入Python标准库中的sys模块。通过导入这个模块,你可以访问与Python解释器及其环境交互的功能,比如命令行参数(sys.argv)和Python搜索路径(sys.path)。
2.引入模块
(1)import 语句
想使用 Python 源文件,只需在另一个源文件里执行 import 语句,语法如下:
import module1[, module2[,... moduleN]
当解释器遇到 import 语句,如果模块在当前的搜索路径就会被导入。
搜索路径时一个解释器会先进行搜索的所有目录的列表。如想要导入模块 support,需要把命令放在脚本的顶端:
#!/usr/bin/python3
# Filename: support.py
def print_func( par ):
print ("Hello : ", par)
return
test.py 引入 support 模块:
#!/usr/bin/python3
# Filename: test.py
# 导入模块
import support
# 现在可以调用模块里包含的函数了
support.print_func("World")
(2)模块的搜索路径
当导入一个模块时,Python 会按照以下顺序查找模块:
- 当前目录。
- 环境变量 PYTHONPATH 指定的目录。
- Python 标准库目录。
- .pth 文件中指定的目录。
>>> ['/root', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
(3)from … import 语句
Python 的 from 语句让你从模块中导入一个指定的部分到当前命名空间中,语法如下:
from modname import name1[, name2[, ... nameN]]
例如,要导入模块 fibo 的 fib 函数,使用如下语句:
# Filename: fibo.py
# 斐波那契(fibonacci)数列模块
def fib(n): # 定义到 n 的斐波那契数列
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a+b
print()
def fib2(n): # 返回到 n 的斐波那契数列
result = []
a, b = 0, 1
while b < n:
result.append(b)
a, b = b, a+b
return result
>>> from fibo import fib, fib2
>>> fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377
这个声明不会把整个fibo模块导入到当前的命名空间中,它只会将fibo里的fib函数引入进来。
(4)给模块起别名
使用 as 关键字为模块或函数起别名:
import numpy as np # 将 numpy 模块别名设置为 np
from math import sqrt as square_root # 将 sqrt 函数别名设置为 square_root
(5)from … import * 语句
把一个模块的所有内容全都导入到当前的命名空间也是可行的,只需使用如下声明:
from modname import *
这提供了一个简单的方法来导入一个模块中的所有项目。
不推荐,容易引起命名冲突。
(6)__name__
属性
当我们在命令行运行模块文件时,Python解释器把一个特殊变量__name__
置为__main__
,而如果在其他地方导入该模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
if __name__=='__main__':
test()
一个模块被另一个程序第一次引入时,其主程序将运行。
#!/usr/bin/python3
# Filename: using_name.py
if __name__ == '__main__':
print('程序自身在运行')
else:
print('我来自另一模块')
运行输出如下:
如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用 __name__
属性来使该程序块仅在该模块自身运行时执行。
$ python using_name.py
程序自身在运行
$ python
>>> import using_name
我来自另一模块
>>>
3.标准模块
以下是一些 Python3 标准库中的模块:
-
os 模块:os 模块提供了许多与操作系统交互的函数,例如创建、移动和删除文件和目录,以及访问环境变量等。
-
sys 模块:sys 模块提供了与 Python 解释器和系统相关的功能,例如解释器的版本和路径,以及与 stdin、stdout 和 stderr 相关的信息。
-
time 模块:time 模块提供了处理时间的函数,例如获取当前时间、格式化日期和时间、计时等。
-
datetime 模块:datetime 模块提供了更高级的日期和时间处理函数,例如处理时区、计算时间差、计算日期差等。
-
random 模块:random 模块提供了生成随机数的函数,例如生成随机整数、浮点数、序列等。
-
math 模块:math 模块提供了数学函数,例如三角函数、对数函数、指数函数、常数等。
-
re 模块:re 模块提供了正则表达式处理函数,可以用于文本搜索、替换、分割等。
-
json 模块:json 模块提供了 JSON 编码和解码函数,可以将 Python 对象转换为 JSON 格式,并从 JSON 格式中解析出 Python 对象。
-
urllib 模块:urllib 模块提供了访问网页和处理 URL 的功能,包括下载文件、发送 POST 请求、处理 cookies 等。
(1)操作系统接口
os 模块提供了不少与操作系统相关联的函数,例如文件和目录的操作。
import os
# 获取当前工作目录
current_dir = os.getcwd()
print("当前工作目录:", current_dir)
# 列出目录下的文件
files = os.listdir(current_dir)
print("目录下的文件:", files)
(2)文件通配符
glob 模块提供了一个函数用于从目录通配符搜索中生成文件列表:
>>> import glob
>>> glob.glob('*.py')
['primes.py', 'random.py', 'quote.py']
(3)命令行参数
通用工具脚本经常调用命令行参数。这些命令行参数以链表形式存储于 sys 模块的 argv 变量。例如在命令行中执行 “python demo.py one two three” 后可以得到以下输出结果:
>>> import sys
>>> print(sys.argv)
['demo.py', 'one', 'two', 'three']
三、命名空间
命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。
命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。
一般有三种命名空间:
- 内置名称(built-in names), Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。
- 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
- 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)
Python 的查找顺序为:局部的命名空间 -> 全局命名空间 -> 内置命名空间。
1.命名空间的生命周期
命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。
因此,我们无法从外部命名空间访问内部命名空间的对象。
# var1 是全局名称
var1 = 5
def some_func():
# var2 是局部名称
var2 = 6
def some_inner_func():
# var3 是内嵌的局部名称
var3 = 7
如下图所示,相同的对象名称可以存在于多个命名空间中。
四、作用域
作用域就是一个 Python 程序可以直接访问命名空间的正文区域。
在一个 python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。
Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。
变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。
1.作用域的分类
Python 的作用域一共有 4 种,分别是:
有四种作用域:
- L(Local):最内层,包含局部变量,比如一个函数/方法内部。
- E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
- G(Global):当前脚本的最外层,比如当前模块的全局变量。
- B(Built-in): 包含了内建的变量/关键字等,最后被搜索。
LEGB 规则(Local, Enclosing, Global, Built-in):Python 查找变量时的顺序是: L –> E –> G –> B。
- Local:当前函数的局部作用域。
- Enclosing:包含当前函数的外部函数的作用域(如果有嵌套函数)。
- Global:当前模块的全局作用域。
- Built-in:Python 内置的作用域。
在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。
g_count = 0 # 全局作用域
def outer():
o_count = 1 # 闭包函数外的函数中
def inner():
i_count = 2 # 局部作用域
2.全局变量和局部变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。
局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。
在函数内部声明的变量只在函数内部的作用域中有效,调用函数时,这些内部变量会被加入到函数内部的作用域中,并且不会影响到函数外部的同名变量,如下实例:
#!/usr/bin/python3
total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
#返回2个参数的和."
total = arg1 + arg2 # total在这里是局部变量.
print ("函数内是局部变量 : ", total)
return total
#调用sum函数
sum( 10, 20 )
print ("函数外是全局变量 : ", total)
3.global 和 nonlocal关键字
当内部作用域想修改外部作用域的变量时,就要用到 global 和 nonlocal 关键字了。
以下实例修改全局变量 num:
#!/usr/bin/python3
num = 1
def fun1():
global num # 需要使用 global 关键字声明
print(num)
num = 123
print(num)
fun1()
print(num)
# 以上实例输出结果:
1
123
123
如果要修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量则需要 nonlocal 关键字了,如下实例:
#!/usr/bin/python3
def outer():
num = 10
def inner():
nonlocal num # nonlocal关键字声明
num = 100
print(num)
inner()
print(num)
outer()
# 以上实例输出结果:
100
100
总结
- 全局变量在函数外部定义,可以在整个文件中访问。
- 局部变量在函数内部定义,只能在函数内访问。
- 使用 global 可以在函数中修改全局变量。
- 使用 nonlocal 可以在嵌套函数中修改外部函数的变量。
4.模块间公开与私有的作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。
类似java中public
和private
修饰符,而在Python中没有修饰符关键字,是通过_
前缀来实现的。
正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI
等;
类似__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名;
类似_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc
,__abc
等;
之所以我们说,private
函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
private
函数或变量不应该被别人引用,那它们有什么用呢?请看例子:
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)
else:
return _private_2(name)
我们在模块里公开greeting()
函数,而把内部逻辑用private
函数隐藏起来了,这样,调用greeting()
函数不用关心内部的private
函数细节,这也是一种非常有用的代码封装和抽象的方法,即:
外部不需要引用的函数全部定义成private
,只有外部需要引用的函数才定义为public
。
五、包
如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。
包是一种管理 Python 模块命名空间的形式,采用"点模块名称"。
比如一个模块的名称是 A.B
, 那么他表示一个包 A
中的子模块 B
。
举个例子,一个abc.py
的文件就是一个名字叫abc
的模块,一个xyz.py
的文件就是一个名字叫xyz
的模块。
现在,假设我们的abc
和xyz
这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录存放:
mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py
模块的名字就变成了mycompany.abc
,类似的,xyz.py
的模块名变成了mycompany.xyz
。
就好像使用模块的时候,你不用担心不同模块之间的全局变量相互影响一样,采用点模块名称这种形式也不用担心不同库之间的模块重名的情况。
请注意,每一个包目录下面都会有一个__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。
__init__.py
可以是空文件,也可以有Python代码,因为__init__.py
本身就是一个模块,而它的模块名就是mycompany
。
最简单的情况,放一个空的 :file:__init__.py
就可以了。当然这个文件中也可以包含一些初始化代码或者为__all__
变量赋值。
1.案例分析
不妨假设你想设计一套统一处理声音文件和数据的模块(或者称之为一个"包")。
现存很多种不同的音频文件格式(基本上都是通过后缀名区分的,例如: .wav,:file:.aiff,:file:.au,),所以你需要有一组不断增加的模块,用来在不同的格式之间转换。
并且针对这些音频数据,还有很多不同的操作(比如混音,添加回声,增加均衡器功能,创建人造立体声效果),所以你还需要一组怎么也写不完的模块来处理这些操作。
这里给出了一种可能的包结构(在分层的文件系统中):
sound/ 顶层包
__init__.py 初始化 sound 包
formats/ 文件格式转换子包
__init__.py
wavread.py
wavwrite.py
aiffread.py
aiffwrite.py
auread.py
auwrite.py
...
effects/ 声音效果子包
__init__.py
echo.py
surround.py
reverse.py
...
filters/ filters 子包
__init__.py
equalizer.py
vocoder.py
karaoke.py
...
在导入一个包的时候,Python 会根据 sys.path
中的目录来寻找这个包中包含的子目录。
2.如何导入包中的内容?
(1)每次导入一个模块
用户可以每次只导入一个包里面的特定模块,比如:
import sound.effects.echo
这将会导入子模块:sound.effects.echo。 他必须使用全名去访问:
sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)
(2)导入子模块
还有一种导入子模块的方法是:
from sound.effects import echo
这同样会导入子模块: echo,并且他不需要那些冗长的前缀,所以他可以这样使用:
echo.echofilter(input, output, delay=0.7, atten=4)
(3)导入函数变量
还有一种变化就是直接导入一个函数或者变量:
from sound.effects.echo import echofilter
同样的,这种方法会导入子模块: echo,并且可以直接使用他的 echofilter() 函数:
echofilter(input, output, delay=0.7, atten=4)
注意当使用 from package import item
这种形式的时候,对应的 item 既可以是包里面的子模块(子包),或者包里面定义的其他名称,比如函数,类或者变量。
import
语法会首先把 item 当作一个包定义的名称,如果没找到,再试图按照一个模块去导入。如果还没找到,抛出一个 :exc:ImportError
异常。
反之,如果使用形如 import item.subitem.subsubitem
这种导入形式,除了最后一项,都必须是包,而最后一项则可以是模块或者是包,但是不可以是类,函数或者变量的名字。
3.从一个包中导入*
如果我们使用 from sound.effects import *
会发生什么呢?
Python 会进入文件系统,找到这个包里面所有的子模块,然后一个一个的把它们都导入进来。
但这个方法在 Windows 平台上工作的就不是非常好,因为 Windows 是一个不区分大小写的系统。
在 Windows 平台上,我们无法确定一个叫做 ECHO.py 的文件导入为模块是 echo 还是 Echo,或者是 ECHO。
为了解决这个问题,我们只需要提供一个精确包的索引。
(1)关于 __all__
变量
导入语句遵循如下规则:如果包定义文件 __init__.py
存在一个叫做 __all__
的列表变量,那么在使用 from package import *
的时候就把这个列表中的所有名字作为包内容导入。
作为包的作者,可别忘了在更新包之后保证 __all__
也更新了啊。
以下实例在 file:sounds/effects/__init__.py
中包含如下代码:
__all__ = ["echo", "surround", "reverse"]
这表示当你使用from sound.effects import *
这种用法时,你只会导入包里面这三个子模块。
如果 __all__
真的没有定义,那么使用from sound.effects import *
这种语法的时候,就不会导入包 sound.effects 里的任何子模块。他只是把包sound.effects
和它里面定义的所有内容导入进来(可能运行__init__.py
里定义的初始化代码)。
这会把 init.py 里面定义的所有名字导入进来。并且他不会破坏掉我们在这句话之前导入的所有明确指定的模块。看下这部分代码:
import sound.effects.echo
import sound.effects.surround
from sound.effects import *
这个例子中,在执行 from...import
前,包 sound.effects 中的 echo 和 surround 模块都被导入到当前的命名空间中了。(当然如果定义了 __all__
就更没问题了)
通常我们并不主张使用 * 这种方法来导入模块,因为这种方法经常会导致代码的可读性降低。不过这样倒的确是可以省去不少敲键的功夫,而且一些模块都设计成了只能通过特定的方法导入。
(2)推荐做法
记住,使用 from Package import specific_submodule
这种方法永远不会有错。事实上,这也是推荐的方法。除非是你要导入的子模块有可能和其他包的子模块重名。
如果在结构中包是一个子包(比如这个例子中对于包sound来说),而你又想导入兄弟包(同级别的包)你就得使用导入绝对的路径来导入。比如,如果模块sound.filters.vocoder 要使用包 sound.effects
中的模块 echo
,你就要写成 from sound.effects import echo
。
from . import echo
from .. import formats
from ..filters import equalizer
无论是隐式的还是显式的相对导入都是从当前模块开始的。主模块的名字永远是"__main__
",一个Python应用程序的主模块,应当总是使用绝对路径引用。
包还提供一个额外的属性__path__
。这是一个目录列表,里面每一个包含的目录都有为这个包服务的__init__.py
,你得在其他__init__.py
被执行前定义哦。可以修改这个变量,用来影响包含在包里面的模块和子包。
这个功能并不常用,一般用来扩展包里面的模块。
六、打包python程序成exe文件
1.打包单个文件
(1)安装PyInstaller:
你可以通过pip来安装PyInstaller,如果还没有安装的话:
pip install pyinstaller
(2)打包你的程序:
使用以下命令来打包你的Python脚本。–onefile选项用于创建单个可执行文件。
pyinstaller --onefile your_script.py
这将在dist目录下生成一个单独的.exe文件,你可以将这个文件分发给用户。
2.和配置文件一起打包
有时候程序文件有配置文件,需要一起打包
[smtp]
server = smtp.example.com
port = 587
username = your_email@example.com
password = your_password
from_addr = monitor@example.com
to_addrs = admin1@example.com;admin2@example.com
(1)安装依赖
pip install pyinstaller configparser
(2)创建打包规范文件(monitor.spec)
# -*- mode: python -*-
block_cipher = None
a = Analysis(['monitor_tool.py'],
pathex=[],
binaries=[],
datas=[('config.ini', '.'), ('ips.txt', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='NetworkMonitor',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='NetworkMonitor')
注意:生产环境使用时建议将console=False改为True以便查看运行状态
(3)执行打包命令
pyinstaller monitor.spec