从Python代码到诗

从Python代码到诗_第1张图片

序言

  在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关于计时比较有代表性的两个库是timetimeittime库中有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等工具处理数据,提高脚本执行效率等…

你可能感兴趣的:(python)