🐳序言
在Python社区,没有强制的编码标准,这虽然赋予了开发者更多的自由,但也导致代码风格不一致性。使得部分代码变得晦涩难懂,本文将探讨一系列的开发技巧和最佳实践,开发出优雅的Python脚本。
1、参数接收与校验
argparse模块可以使用标准的命令行界面风格解析命令行参数,支持位置参数和可选参数,使用-h可查看脚本使用说明。
1.1 ArgumentParser常用属性
字段 | 说明 | 备注 |
---|---|---|
name | 参数名称或选项 | 例如:-file、–file |
default | 默认值 | |
type | 类型 | |
choices | 可选值列表 | |
required | 是否必传 | 默认为False |
nargs | 参数的数量 | 默认为None,支持int,+,* |
metavar | 帮助信息显示参数的名字 | |
action | 动作 | 支持store_true、store_false等 |
help | 帮助信息 |
1.2 获取位置参数
使用位置参数我们可以处理n多个值,输出列表
# python main.py 3 5
import argparse
parser = argparse.ArgumentParser(description="位置参数测试")
parser.add_argument("number", type=int, nargs="*", help="传入数字")
args = parser.parse_args()
# [3, 5]
print(args.number)
1.3 获取可选参数
使用可选参数可以避免传入位置不准确的问题
# python main.py --name '欢颜' --datatime '2023-08-18 13:14:52' --is_worktime
import argparse
from datetime import datetime
class ParserArgs:
def __init__(self):
parser = argparse.ArgumentParser(description='从Python代码到诗')
parser.add_argument('--name', type=str, required=True, help='输入名称')
parser.add_argument(
'--datetime',
type=self.validate_dt,
required=True,
help='日期时间(YYYY-MM-DD HH:MM:SS)'
)
parser.add_argument('--is_worktime', action='store_true', help='是否在工作时间')
self.args = parser.parse_args()
@staticmethod
def validate_dt(dt_str, fmt_dt='%Y-%m-%d %H:%M:%S'):
try:
dt_obj = datetime.strptime(dt_str, fmt_dt)
except ValueError:
raise argparse.ArgumentTypeError("Invalid datetime format.")
else:
return dt_obj
# Namespace(name='欢颜', datetime=datetime.datetime(2023, 8, 17, 0, 0), is_worktime=True)
Args = ParseArgs().args
2、日志记录器
项目开发中应避免直接使用print函数,日志记录能够更好地管理和调试代码、故障排除以及监控程序的运行状态。Python内置了logging
模块,用于创建日志记录器,实现灵活的日志记录和输出控制,通常将日志直接写入文件,输出更加优雅。
2.1 日志简单使用
#!/usr/bin/env python3
# -*-coding:utf-8 -*-
import logging
def get_logger():
# 实例化日志对象
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] [%(filename)s] [%(funcName)s:%(lineno)d] %(message)s",
"%Y-%m-%d %H:%M:%S"
)
ch.setFormatter(formatter)
logger.addHandler(ch)
return logger
logger = get_logger()
# [2023-08-18 16:40:07] [INFO] [main.py] [main:27] hello world!
logger.info("hello world!")
3、进度条的使用
在命令行界面中展示进度条,以增强长时间运行的循环、任务或操作的可视化体验。Python的tqdm
库,可以非常方便地应用于各种迭代操作,从文件读写到数据处理,都能通过进度条的方式直观地显示进度。
3.1 tqdm方法常用参数
参数 | 含义 | 类型 | 必填 | 备注 |
---|---|---|---|---|
iterable | Iterable 用进度条装饰 | iterable | 可选 | 可迭代对象 |
total | 预计迭代的次数 | int/float | 可选 | 默认为迭代器的长度 |
ncols | 整个输出消息的宽度 | int | 可选 | |
mininterval | 最小进度显示更新间隔 | float | 可选 | 默认0.1s |
ascii | 进度填充 | bool | 可选 | 默认unicode平滑块来填充仪表 |
unit | 单位 | str | 可选 | 默认为it |
color | 条形颜色 | str | 可选 | |
initial | 初始计数器值 | int/float | 可选 |
3.2 trange方法
trange方法是封装了range方法的tqdm, 它会在循环中显示进度条、多用于循环次数固定的情况。
import time
from tqdm import trange
for _ in trange(10):
time.sleep(0.1)
3.3 tqdm方法
tqdm是tqdm模块最常用的方法,用于在循环中显示进度条。传递一个迭代对象给它,然后迭代这个对象,会产生一个进度条。
import time
from tqdm import tqdm
from colorama import Fore, Style
iterable = ['Python', 'GoLang', 'Java', 'JavaScript']
progress_bar = tqdm(iterable, ncols=100, ascii=False)
for element in progress_bar:
progress_bar.set_description("{}正在处理:{}".format(Fore.LIGHTBLUE_EX, element))
time.sleep(0.5)
4、测试函数执行时间
Python关于计时比较有代表性的两个库是time
和timeit
。time
库中有time()
、perf_counter()
以及process_time()
三个函数可用来计时(以秒为单位),加后缀_ns
表示以纳秒计时。上述三者的区别如下:
time()
精度上相对没有那么高,而且受系统的影响,适合表示日期时间或者大程序的计时。perf_counter()
适合小一点的程序测试,会计算sleep()
时间。process_time()
适合小一点的程序测试,不计算sleep()
时间。
与time
库相比,timeit
有两个优点:
timeit
会根据您的操作系统和 Python 版本选择最佳计时器。timeit
在计时期间会暂时禁用垃圾回收。
4.1 使用time计时
import time
def statistic_cost(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
cost = time.time() - start_time
logger.info(f"execute {func.__name__} cost {cost:.3f} 秒")
return result
return wrapper
def test():
time.sleep(3)
if __name__ == '__main__':
test()
4.2 使用timeit
"""
timeit方法参数:
stmt: 需要计时的语句或者函数
setup: 执行stmt之前要运行的代码。通常,它用于导入一些模块或声明一些必要的变量。
timer: 计时器函数,默认为time.perf_counter()。
number: 执行计时语句的次数,默认为一百万次。
globals: 指定执行代码的命名空间。
"""
import time
from timeit import timeit
def test():
time.sleep(3)
if __name__ == '__main__':
cost = timeit(test, number=1)
print(cost)
5、使用装饰器缓存
Python支持装饰器缓存,在内存中维护特定类型的缓存,以实现最佳软件驱动速度,使用lru_cache
来实现。lru_cache
的性能优势在于重复性高、计算密集型的函数。在一些情况下,使用缓存可能会导致额外的内存使用,因为缓存会保留一部分函数调用的结果。因此,应根据具体情况权衡是否使用缓存。对于需要实时更新或频繁变动的数据,lru_cache
并不适用。它适用于那些函数的计算结果在短时间内保持一致的场景。
5.1 缓存简单使用
#!/usr/bin/env python3
# -*-coding:utf-8 -*-
# maxsize: 指定缓存中保留的最大函数调用结果数量。当超过数量,最早的结果将被移除,默认为128。
# typed: 如果设置为True,则不同类型的参数会被视为不同的参数。
import functools
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
# 从获取相同
for _ in range(10):
fibonacci(10)
5.2 缓存强制刷新
lru_cache
函数不支持主动刷新的功能, 缓存的更新是由缓存装饰器内部的缓存策略自动管理的,通常是根据函数的参数和调用次数来决定哪些缓存条目会被保留或淘汰。如果需要在特定时刻主动刷新缓存, 可以使用cache_clear()
方法强制清除缓存中的所有条目,从而让缓存失效,下次访问时会重新计算。你可以在数据发生变动时调用此方法。
# 以上面计算斐波那契函数为例
# 强制清除缓存
fibonacci.cache_clear()
6、单元测试
本文使用unittest编写单元测试
单元测试是软件开发过程中的一个关键环节,旨在验证代码中的最小功能单元是否按预期工作。通过编写单元测试,你可以有效地发现和修复代码中的错误,确保代码在不同情况下都能正确运行。在Python中有多个测试框架可以选择unittest、pytest和nose等。
6.1 单元测试简单使用
6.1.1 编写一个函数用于测试
import functools
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 0:
raise ValueError('Invalid Value')
elif n in (0, 1):
return n
return fibonacci(n - 1) + fibonacci(n - 2)
6.1.2 使用unittest完成测试
import unittest
from script import main as alg
class TestFibonacci(unittest.TestCase):
def test_fibonacci(self):
output = alg.fibonacci(10)
self.assertEqual(output, 55)
def test_fibonacci_invalid(self):
with self.assertRaises(ValueError):
alg.fibonacci(-1)
if __name__ == '__main__':
unittest.main()
6.2.1 模拟对象
模块unittest.mock
提供了两个主要的类来进行模拟和断言行为(Mock和MagicMock),Mock通常用于模拟普通方法和属性,MagicMock具有更多的魔法方法,允许一些特殊的Python行为,如迭代,上下文管理等,返回自身。
对以下函数进行测试
ParserArgs对象在参数接收于校验模块
import time
from colorama import Fore
from logger import logger
from parser_args import ParserArgs
def launch():
args = ParserArgs().args
logger.info(f"input args: {args.__dict__}")
iterable = ['Python', 'GoLang', 'Java', 'JavaScript']
progress_bar = tqdm(iterable, ncols=100, ascii=False)
for element in progress_bar:
progress_bar.set_description("{}正在处理:{}".format(Fore.LIGHTBLUE_EX, element))
return True
if __name__ == '__main__':
launch()
模拟获取参数的行为
替换ParserArgs的参数为mock对象
import argparse
import unittest
from unittest.mock import patch
from script import main as alg
class TestParserArgs(unittest.TestCase):
def setUp(self) -> None:
self.mock_args = argparse.Namespace(
name='欢颜',
datetime='2023-05-20 13:14:00',
is_worktime=True
)
@patch('script.main.ParserArgs')
def test_launch(self, mock_parser_args):
parser_args_obj = mock_parser_args.return_value
parser_args_obj.args = self.mock_args
output = alg.launch()
expect = True
self.assertEqual(output, expect)
if __name__ == '__main__':
unittest.main()
7、包的构建与分发
7.1 打包程序为可执行文件(EXE)
可以使用pyinstaller将单文件应用或多文件应用,打包为exe可执行文件,需要编译作为程序入口的文件即可。
7.1.1 打包应用程序
pip3 install pyinstaller
# pyinstaller -F app.py
def main():
print('程序开始执行')
if __name__ == '__main__':
main()
7.2.2 Pyinstaller常用选项
-h | 帮助信息 |
---|---|
-F | 产生单个的可执行文件 |
-D | 产生一个目录(包含多个文件)作为可执行程序 |
-a | 不包含 Unicode 字符集支持 |
-d | 产生 debug 版本的可执行文件 |
-w | 指定程序运行时不显示命令行窗口(仅对 Windows 有效) |
-c | 指定使用命令行窗口运行程序(仅对 Windows 有效) |
-o | 指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件 |
-p | 设置 Python 导入模块的路径 |
-n | 指定项目名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字 |
7.2 使用setuptools构建包
7.2.1 创建合适的项目结构
# 一个最简单的项目结构
pkgs/
├── LICENSE # 许可证文件
├── main.py # 入口文件
├── app # 存放子文件等
├── README.md # 应用文档
└── setup.py # 用于构建、安装和分发包的脚本文件
7.2.2 编写setup文件
一个最简单的setup示例
from setuptools import setup, find_packages
setup(
name='PkgName',
version='0.1',
description='描述信息',
author='Your Name',
packages=find_packages(),
install_requires=[
# 依赖项列表
],
)
7.2.3 构建包
在项目根目录中运行python setup.py sdist
生成源分发包,也可以生成二进制分发包bdist_wheel
。然后可以将包发布到Python包索引(PyPI),这需要在PyPI上创建一个帐户,并使用工具如twine
来上传包(twine upload dist/*)。
7.2.4 分发包
您可以通过以下方式分享生成的分发包,将其发布到代码托管平台如GitHub,上传至内部或外部的PyPI服务器,或者使用pip
来进行安装包。
🧀 小结
编写一个优雅的Python脚本关键在于遵循Python的最佳实践和编码规范(PEP8),以确保代码风格一致性和可读性。使用有意义的命名、拆分代码块成小函数、适度添加注释和文档、合理处理异常、充分利用Python的内置函数和模块、避免滥用全局变量、保持模块化和可重用性、采用测试驱动开发等方法,有助于编写出清晰、可维护、优雅的Python脚本,提高代码质量和可维护性。以上是笔者开发过程中经常使用的一些小Tips,在这里记录分享,当然,我没还可以使用pandas,numpy等工具处理数据,提高脚本执行效率等…