21、Python 模块、包部署及输入输出全解析

Python 模块、包部署及输入输出全解析

1. Python 包部署

在 Python 开发中,将代码分享给他人使用是一个重要的环节。为了实现这一目标,有一些关键的步骤和注意事项。

首先,要确保代码以自包含项目的形式存在,所有代码都应位于一个合适的包中。给包取一个独特的名称非常重要,可通过访问 Python 包索引 来选择合适的名称,避免与其他依赖冲突。在组织代码结构时,应尽量保持简单,虽然模块和包系统有很多高级功能,但不应从复杂的操作开始。

若要分发纯 Python 代码,可使用 setuptools 模块或内置的 distutils 模块。以下是一个具体的操作步骤示例:
1. 假设你有一个项目结构如下:

spam-project/
README.txt
Documentation.txt
spam/
# A package of code
__init__.py
foo.py
bar.py
runspam.py
# A script to run as: python runspam.py
  1. 在项目的最顶层目录(如 spam-project/ )创建一个 setup.py 文件,并添加以下代码:
# setup.py
from setuptools import setup
setup(name="spam",
      version="0.0",
      packages=['spam'],
      scripts=['runspam.py'],
)

setup() 函数调用中, packages 是所有包目录的列表, scripts 是脚本文件的列表。如果软件没有某些部分,这些参数可以省略。 name 是包的名称, version 是版本号(以字符串形式表示)。 setup() 还支持其他各种参数,可提供有关包的各种元数据,完整列表可查看 这里

  1. 创建 setup.py 文件后,就可以创建软件的源代码分发版本。在 shell 中输入以下命令:
bash $ python setup.py sdist
...
bash $

这将在 spam/dist 目录下创建一个存档文件,如 spam-1.0.tar.gz spam-1.0.zip 。用户可以使用 pip 命令来安装该软件,例如:

shell $ python3 -m pip install spam-1.0.tar.gz

软件将被安装到本地 Python 发行版中,代码通常会安装到 Python 库的 site-packages 目录中,可通过检查 sys.path 的值来确定其确切位置。脚本通常会安装到与 Python 解释器相同的目录中。

需要注意的是,如果脚本的第一行以 #! 开头并包含 python 文本,安装程序会将该行重写为指向本地 Python 安装路径。因此,即使脚本硬编码了特定的 Python 位置,在其他系统上安装时也能正常工作。不过,这里对 setuptools 的使用是非常基础的,大型项目可能涉及 C/C++ 扩展、复杂的包结构等,你可以参考 https://python.org https://pypi.org 获取最新建议。

此外,在开始新程序时,建议从包开始。很多人习惯从一个简单的 Python 文件开始编写脚本,但随着脚本功能的增加,可能需要拆分成多个文件,这时就容易出现问题。因此,从一开始就将程序作为一个包来开发是个好习惯。例如,不创建 program.py 文件,而是创建一个名为 program 的包目录:

program/
__init__.py
__main__.py

将起始代码放在 __main__.py 中,使用 python -m program 命令运行程序。随着代码的增加,可向包中添加新文件并使用包相对导入。使用包的好处是代码保持隔离,可随意命名文件,不用担心与其他包、标准库模块或同事的代码冲突。虽然一开始设置包需要更多工作,但后期能避免很多麻烦。

同时,在处理模块和包系统时,要保持简单。尽管有很多高级技巧,但不要进行过度的模块修改。管理模块、包和软件分发一直是 Python 社区的痛点,很多问题是由于人们对模块系统进行不当修改导致的。当同事提出一些复杂的修改建议时,要有勇气拒绝。

2. Python 输入输出基础

输入输出(I/O)是所有程序的重要组成部分,Python 的 I/O 涉及数据编码、命令行选项、环境变量、文件 I/O 和数据序列化等方面。

2.1 数据表示

在 Python 中,I/O 的核心问题是如何与外部世界进行数据交互,因此数据的正确表示至关重要。Python 处理两种基本数据类型:表示任何原始未解释数据的字节( bytes )和表示 Unicode 字符的文本( str )。

  • 字节类型 :使用 bytes bytearray 两种内置类型来表示字节。 bytes 是整数字节值的不可变字符串, bytearray 是可变的字节数组,兼具字节字符串和列表的特性,适合逐步构建字节组。以下是一些示例:
# Specify a bytes literal (note the b' prefix)
a = b'hello'
# Specify bytes from a list of integers
b = bytes([0x68, 0x65, 0x6c, 0x6c, 0x6f])
# Create and populate a bytearray from parts
c = bytearray()
c.extend(b'world')
# c = bytearray(b'world')
c.append(0x21)
# c = bytearray(b'world!')
# Access byte values
print(a[0])
# --> prints 104
for x in b:
    # Outputs 104 101 108 108 111
    print(x)

需要注意的是,访问 bytes bytearray 对象的单个元素会得到整数字节值,而不是单字符字节字符串,这与文本字符串不同,是常见的使用错误。

  • 文本类型 :文本由 str 数据类型表示,存储为 Unicode 代码点数组。例如:
d = 'hello'
# Text (Unicode)
len(d)
# --> 5
print(d[0])
# prints 'h'

Python 严格区分字节和文本,两者之间不会自动转换,比较操作结果为 False ,混合使用字节和文本的操作会导致错误。例如:

a = b'hello'
# bytes
b = 'hello'
# text
c = 'world'
# text
print(a == b)
# -> False
d = a + c
# TypeError: can't concat str to bytes
e = b + c
# -> 'helloworld' (both are strings)

在进行 I/O 操作时,要确保使用正确的数据表示类型。处理文本时使用文本字符串,处理二进制数据时使用字节。

2.2 文本编码和解码

如果处理文本,从输入读取的所有数据都必须进行解码,写入输出的数据都必须进行编码。文本和字节对象分别有 encode(text [,errors]) decode(bytes [,errors]) 方法用于显式转换。例如:

a = 'hello'
# Text
b = a.encode('utf-8')
# Encode to bytes
c = b'world'
# Bytes
d = c.decode('utf-8')
# Decode to text

encode() decode() 方法都需要指定编码名称,如 'utf-8' 'latin-1' 。常见的编码如下表所示:
| 编码名称 | 描述 |
| ---- | ---- |
| 'ascii' | 字符值范围为 [0x00, 0x7f] |
| 'latin1' | 字符值范围为 [0x00, 0xff] ,也称为 'iso-8859-1' |
| 'utf-8' | 可变长度编码,可表示所有 Unicode 字符 |
| 'cp1252' | Windows 上常见的文本编码 |
| 'macroman' | Macintosh 上常见的文本编码 |

此外,编码方法还接受一个可选的 errors 参数,用于指定编码错误时的处理行为,具体选项如下表所示:
| 值 | 描述 |
| ---- | ---- |
| 'strict' | 编码和解码错误时引发 UnicodeError 异常(默认) |
| 'ignore' | 忽略无效字符 |
| 'replace' | 用替换字符(Unicode 中为 U+FFFD ,字节中为 b'?' )替换无效字符 |
| 'backslashreplace' | 用 Python 字符转义序列替换每个无效字符(仅编码时有效) |
| 'xmlcharrefreplace' | 用 XML 字符引用替换每个无效字符(仅编码时有效) |
| 'surrogateescape' | 解码时将任何无效字节 '\xhh' 替换为 U+DChh ,编码时将 U+DChh 替换为字节 '\xhh' |

'backslashreplace' 'xmlcharrefreplace' 错误策略可以将不可表示的字符以简单 ASCII 文本或 XML 字符引用的形式表示,便于调试。 'surrogateescape' 错误处理策略允许不符合预期编码规则的字节数据在解码/编码循环中保持完整,对于某些系统接口很有用。例如:

>>> a = b'Spicy Jalape\xf1o'
# Invalid UTF-8
>>> a.decode('utf-8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1
in position 12: invalid continuation byte
>>> a.decode('utf-8', 'surrogateescape')
'Spicy Jalape\udcf1o'
>>> # Encode the resulting string back into bytes
>>> _.encode('utf-8', 'surrogateescape')
b'Spicy Jalape\xf1o'
>>>
2.3 文本和字节格式化

在处理文本和字节字符串时,字符串转换和格式化是常见问题,例如将浮点数转换为具有指定宽度和精度的字符串。可以使用 format() 函数来格式化单个值,示例如下:

x = 123.456
format(x, '0.2f')
# '123.46'
format(x, '10.4f')
# '    123.4560'
format(x, '<*10.2f')
# '123.46****'

format() 的第二个参数是格式说明符,其一般格式为 [[fill[align]][sign][0][width][,][.precision][type] ,每个部分都是可选的。 width 指定最小字段宽度, align 是对齐说明符( < 左对齐、 > 右对齐、 ^ 居中对齐),可选的填充字符 fill 用于填充空间。例如:

name = 'Elwood'
r = format(name, '<10')
# r = 'Elwood    '
r = format(name, '>10')
# r = '    Elwood'
r = format(name, '^10')
# r = '  Elwood  '
r = format(name, '*^10')
# r = '**Elwood**'

类型说明符表示数据类型,支持的格式代码如下表所示:
| 字符 | 输出格式 |
| ---- | ---- |
| d | 十进制整数或长整数 |
| b | 二进制整数或长整数 |
| o | 八进制整数或长整数 |
| x | 十六进制整数或长整数 |
| X | 十六进制整数(大写字母) |
| f , F | 浮点数,格式为 [-]m.dddddd |
| e | 浮点数,格式为 [-]m.dddddde±xx |
| E | 浮点数,格式为 [-]m.ddddddE±xx |
| g , G | 指数小于 [nd]4 或大于精度时使用 e E ,否则使用 f |
| n | 与 g 相同,但当前区域设置决定小数点字符 |
| % | 将数字乘以 100 并以 f 格式显示,后面跟 % 符号 |
| s | 字符串或任何对象,使用 str() 生成字符串 |
| c | 单个字符 |

格式说明符的 sign 部分可以是 + - 或空格。 + 表示所有数字都使用前导符号, - 是默认值,仅为负数添加符号,空格为正数添加前导空格。可选的逗号 , 可以出现在宽度和精度之间,用于添加千位分隔符。例如:

x = 123456.78
format(x, '16,.2f')
# '    123,456.78'

对于更复杂的字符串格式化,可以使用 f-strings 或 .format() 方法。f-strings 示例如下:

x = 123.456
f'Value is {x:0.2f}'
# 'Value is 123.46'
f'Value is {x:10.4f}'
# 'Value is    123.4560'
f'Value is {2*x:*<10.2f}'
# 'Value is 246.91****'

.format() 方法示例如下:

x = 123.456
'Value is {:0.2f}'.format(x)
# 'Value is 123.46'
'Value is {0:10.2f}'.format(x)
# 'Value is    123.4560'
'Value is {val:<*10.2f}'.format(val=x)
# 'Value is 123.46****'

bytes bytearray 实例可以使用 % 运算符进行格式化,其语义类似于 C 语言的 sprintf() 函数。例如:

name = b'ACME'
x = 123.456
b'Value is %0.2f' % x
# b'The value is 123.46'
bytearray(b'Value is %0.2f') % x
# b'Value is 123.46'
b'%s = %0.2f' % (name, x)
# b'ACME = 123.46'

需要注意的是,处理字节时不支持文本字符串,需要显式编码。这种格式化方式也可用于文本字符串,但被认为是较旧的编程风格,在某些库中仍会出现,如 logging 模块。

2.4 读取命令行选项

Python 启动时,命令行选项会以文本字符串的形式存储在 sys.argv 列表中。列表的第一个元素是程序的名称,后续元素是在程序名称之后添加的命令行选项。以下是一个手动处理命令行参数的简单示例:

def main(argv):
    if len(argv) != 3:
        raise SystemExit(
            f'Usage : python {argv[0]} inputfile outputfile\n')
    inputfile = argv[1]
    outputfile = argv[2]
    ...
if __name__ == '__main__':
    import sys
    main(sys.argv)

为了更好地组织代码和进行测试,建议编写一个专门的 main() 函数来接受命令行选项列表,而不是直接读取 sys.argv ,并在程序末尾添加一小段代码将命令行选项传递给 main() 函数。

对于更复杂的命令行处理,可以使用 argparse 模块。示例如下:

import argparse
def main(argv):
    p = argparse.ArgumentParser(description="This is some program")
    # A positional argument
    p.add_argument("infile")
    # An option taking an argument
    p.add_argument("-o","--output", action="store")
    # An option that sets a Boolean flag
    p.add_argument("-d","--debug", action="store_true", default=False)
    # Parse the command line
    args = p.parse_args(args=argv)
    # Retrieve the option settings
    infile = args.infile
    output = args.output
    debugmode = args.debug
    print(infile, output, debugmode)
if __name__ == '__main__':
    import sys
    main(sys.argv[1:])

这个示例只是 argparse 模块的简单用法,标准库文档提供了更高级的用法。还有一些第三方模块,如 click docopt ,可以简化更复杂的命令行解析器的编写。

最后,命令行选项可能以无效的文本编码提供给 Python,这些参数仍然会被接受,但会使用 'surrogateescape' 错误处理进行编码。如果这些参数后续会包含在任何文本输出中,并且避免崩溃很关键,就需要注意这一点,但对于不重要的边缘情况,不要过度复杂化代码。

3. 命令行选项处理流程总结

为了更清晰地展示命令行选项处理的流程,下面用 mermaid 流程图进行说明:

graph TD;
    A[Python 启动] --> B[命令行选项存入 sys.argv];
    B --> C{简单处理还是复杂处理};
    C -- 简单处理 --> D[手动处理命令行参数];
    D --> E[编写 main() 函数接受参数];
    E --> F[传递 sys.argv 给 main()];
    C -- 复杂处理 --> G[使用 argparse 模块];
    G --> H[创建 ArgumentParser 对象];
    H --> I[添加位置参数和选项];
    I --> J[解析命令行参数];
    J --> K[获取选项设置并使用];

从流程图可以看出,Python 启动后,命令行选项会被存入 sys.argv 。根据处理的复杂程度,可以选择手动处理或者使用 argparse 模块。手动处理时,编写 main() 函数接受参数并进行相应操作;使用 argparse 模块时,需要创建对象、添加参数和选项、解析参数,最后获取并使用选项设置。

4. 输入输出综合应用示例

假设我们要编写一个简单的 Python 程序,它从命令行接受一个输入文件路径和一个输出文件路径,读取输入文件的内容,对内容进行一些简单处理(例如将所有字母转换为大写),然后将处理后的内容写入输出文件。以下是实现该功能的代码:

import sys

def main(argv):
    if len(argv) != 3:
        raise SystemExit(
            f'Usage : python {argv[0]} inputfile outputfile\n')
    inputfile = argv[1]
    outputfile = argv[2]

    try:
        with open(inputfile, 'r', encoding='utf-8') as infile:
            content = infile.read()
            processed_content = content.upper()

        with open(outputfile, 'w', encoding='utf-8') as outfile:
            outfile.write(processed_content)

        print(f"Successfully processed {inputfile} and saved to {outputfile}")
    except FileNotFoundError:
        print(f"Error: File {inputfile} not found.")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == '__main__':
    main(sys.argv)

代码解释:

  1. 命令行参数检查 :在 main() 函数中,首先检查命令行参数的数量是否为 3(程序名、输入文件路径、输出文件路径),如果不是则抛出 SystemExit 异常并给出使用说明。
  2. 文件读取 :使用 open() 函数以只读模式打开输入文件,读取文件内容并存储在 content 变量中。
  3. 内容处理 :将读取的内容转换为大写形式,存储在 processed_content 变量中。
  4. 文件写入 :使用 open() 函数以写入模式打开输出文件,将处理后的内容写入文件。
  5. 异常处理 :使用 try-except 块捕获可能出现的 FileNotFoundError 异常和其他异常,并进行相应的错误提示。

操作步骤:

  1. 将上述代码保存为一个 Python 文件,例如 file_processor.py
  2. 准备一个输入文件,例如 input.txt ,并在其中写入一些文本内容。
  3. 打开命令行终端,进入代码所在的目录。
  4. 运行以下命令:
python file_processor.py input.txt output.txt

其中 input.txt 是输入文件的路径, output.txt 是输出文件的路径。运行成功后,会在当前目录下生成一个 output.txt 文件,其中包含处理后的内容。

5. 总结与建议

在 Python 开发中,模块和包的部署以及输入输出操作是非常重要的部分。以下是一些总结和建议:
- 模块和包部署
- 从项目开始就以包的形式组织代码,保持代码的隔离性和可维护性。
- 给包取一个独特的名称,避免与其他依赖冲突。
- 使用 setuptools distutils 模块进行代码分发,遵循简单的原则,避免过度复杂的配置。
- 当面对复杂的模块和包管理需求时,参考官方文档和社区资源获取最新建议。
- 输入输出操作
- 明确区分字节和文本数据类型,根据实际情况选择合适的数据表示。
- 在进行文本编码和解码时,了解常见的编码格式和错误处理策略,确保数据的正确处理。
- 掌握字符串格式化的方法,根据需求选择 format() 函数、f-strings 或 .format() 方法。
- 对于命令行选项处理,根据复杂度选择手动处理或使用 argparse 等模块,同时注意处理可能出现的编码问题。

通过遵循这些原则和建议,你可以更高效地进行 Python 开发,避免常见的错误和问题,提高代码的质量和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值