【问题标题】:Best practices for writing argparse parsers编写 argparse 解析器的最佳实践
【发布时间】:2023-10-07 12:12:02
【问题描述】:

是否有使用 Python 的 argparse 模块的最佳实践或样式指南?

我经常使用argparse,它很快就占用了相当多的行来处理所有配置。对于几乎所有我发现坚持接近PEP 8 会产生干净、可读的代码,但这里不是。最终的结果总是一段难看的代码,读起来很痛苦。

难读不是 Pythonic:

Beautiful is better than ugly ... Readibilty counts

那么,是否有 PEP 或其他资源可以为如何更好地格式化此代码提供指导?

丑陋的样本(主要遵循 PEP 8):

parser = argparse.ArgumentParser(description='A nontrivial modular command')
subparsers = parser.add_subparsers(help='sub-command help')

parser_load = subparsers.add_parser('load', help='Load something somewhere')
parser_load.add_argument('--config',
                         help='Path to configuration file for special settings')
parser_load.add_argument('--dir', default=os.getcwd(),
                         help='The directory to load')
parser_load.add_argument('book', help='The book to load into this big thing')
parser_load.add_argument('chapter', nargs='?', default='',
                         help='Optionally specify a chapter')
parser_load.add_argument('verse', nargs='*',
                         help='Optionally pick as many verses as you want to'
                         ' load')
parser_load.set_defaults(command='load')

parser_write = subparsers.add_parser(
                'write', help='Execute commands defined in a config file')
parser_write.add_argument('config', help='The path to the config file')
parser_write.set_defaults(command='write')

parser_save = subparsers.add_parser(
                'save',
                help='Save this big thing for use somewhere later')
parser_save.add_argument('-n', '--name', default=None,
                         help='The name of the component to save')
parser_save.add_argument('path', help="The way out of Plato's cave")
parser_save.set_defaults(command='save')

...

args = parser.parse_args()

【问题讨论】:

  • 查看 click: click.pocoo.org/5,通过装饰器获得更好的参数
  • 除了在换行的位置不一致之外,我没有发现任何问题:您似乎只是在需要符合 80 个字符的地方换行,而不是在有助于可读性的地方.我不相信这不是题外话:基于意见,因为它会根据您的编码标准而有所不同。
  • @TemporalWolf 我可以看到,这就是为什么我询问是否存在标准,而不仅仅是关于如何格式化此代码的建议
  • 也许还可以将parser_X/parser_Y 重命名为X_parser/Y_parser,这样并不是每一行都以parserX/Y 开头,这样可以更容易地区分。
  • 我倾向于使用required=False 代替nargs='?'。如果 required 为 false,如果未指定,则值为 none,这使我的代码更易于阅读,因为我知道 nargs 的意思是“准备好用列表做某事”。

标签: python argparse code-formatting


【解决方案1】:

您的代码没有任何问题,这只是使用argparse 模块的结果。我个人的偏好是将解析器的创建分解为函数。在这种情况下,您可以为您创建的每个子解析器创建一个函数。

def parse_args(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(description='A nontrivial modular command')
    subparsers = parser.add_subparsers(help='sub-command help')

    add_load_subparser(subparsers)
    add_write_subparser(subparsers)
    add_save_subparser(subparsers)

    return parser.parse_args(args)


def add_load_subparser(subparsers):
    parser = subparsers.add_parser('load', help='Load something somewhere')
    parser.add_argument('--config',
                        help='Path to configuration file for special settings')
    parser.add_argument('--dir', default=os.getcwd(),
                        help='The directory to load')
    parser.add_argument('book', help='The book to load into this big thing')
    parser.add_argument('chapter', nargs='?', default='',
                        help='Optionally specify a chapter')
    parser.add_argument('verse', nargs='*',
                        help='Optionally pick as many verses as you want to'
                        ' load')
    parser.set_defaults(command='load')


def add_write_subparser(subparsers):
    parser = subparsers.add_parser(
          'write', help='Execute commands defined in a config file')
    parser.add_argument('config', help='The path to the config file')
    parser.set_defaults(command='write')


def add_save_subparser(subparsers):
    parser = subparsers.add_parser(
               'save',
               help='Save this big thing for use somewhere later')
    parser.add_argument('-n', '--name', default=None,
                        help='The name of the component to save')
    parser.add_argument('path', help="The way out of Plato's cave")
    parser.set_defaults(command='save')


args = parse_args()

【讨论】:

    【解决方案2】:

    正如 TemporalWolf 所评论的,我会更一致地使用换行符,并且使用更多。即使现在代码看起来更长,我发现它更容易阅读:

    • 各个函数调用之间的垂直空间更大,因此更容易在视觉上区分
    • 每行一个参数,因此更容易查看使用了哪些参数
    • 参数更接近左边距,因此需要更少的水平眼球移动和更少的不需要的换行符(比如你拆分help字符串的地方)

    此外,通过重命名parser_X/parser_YX_parser/Y_parser,您可以更轻松地区分X/Y

    parser = argparse.ArgumentParser(
        description='A nontrivial modular command'
    )
    subparsers = parser.add_subparsers(
        help='sub-command help'
    )
    
    load_parser = subparsers.add_parser(
        'load',
        help='Load something somewhere'
    )
    load_parser.add_argument(
        '--config',
        help='Path to configuration file for special settings'
    )
    load_parser.add_argument(
        '--dir',
        default=os.getcwd(),
        help='The directory to load'
    )
    load_parser.add_argument(
        'book',
        help='The book to load into this big thing'
    )
    load_parser.add_argument(
        'chapter',
        nargs='?',
        default='',
        help='Optionally specify a chapter'
    )
    load_parser.add_argument(
        'verse',
        nargs='*',
        help='Optionally pick as many verses as you want to load'
    )
    load_parser.set_defaults(
        command='load'
    )
    
    write_parser = subparsers.add_parser(
        'write',
        help='Execute commands defined in a config file'
    )
    write_parser.add_argument(
        'config',
        help='The path to the config file'
    )
    write_parser.set_defaults(
        command='write'
    )
    
    save_parser = subparsers.add_parser(
        'save',
        help='Save this big thing for use somewhere later'
    )
    save_parser.add_argument(
        '-n', '--name',
        default=None,
        help='The name of the component to save'
    )
    save_parser.add_argument(
        'path',
        help="The way out of Plato's cave"
    )
    save_parser.set_defaults(
        command='save'
    )
    
    ...
    
    args = parser.parse_args()
    

    【讨论】:

    • 不确定您为什么没有实施您在评论中提到的命名更改。我100%同意。我的首选格式是 not that dissimilar,但根据您的建议,我找到了 this even better,并且可能允许删除 cmets。
    • @TemporalWolf 我(还)没有实现它,因为我只是在发布答案后才有了这个想法,并且不想再次编辑所有内容。
    【解决方案3】:

    开发人员之间没有讨论过这个特定模块的样式(我一直在密切关注相关的错误/问题)。

    我更关心解决问题而不是样式和布局,但我喜欢易于阅读和理解的代码。如果有大块的重复模式,我喜欢使用实用函数、字典和列表。

    最近的一个 SO 问题,How to design object oriented subparsers for argparse? 询问了 OOP 子解析器定义。我上了他的初课,加了一个方法:

      def make_sup(self,sp):
          self.parser = sp.add_parser(self.name)
          self.parser.add_argument('--foo')
          self.parser.set_defaults(action=self)
    

    因此可以定义一组对象

    cmds = []
    cmds.append(Cmd('list'))
    cmds.append(Cmd('foo'))
    cmds.append(Cmd('bar'))
    

    甚至

    cmds = [Cmd('list'), Cmd('foo'),...]
    

    然后用于填充解析器:

    parser = argparse.ArgumentParser()
    sp = parser.add_subparsers(dest='cmd')
    for cmd in cmds:
        cmd.make_sup(sp)
    

    这是一个不涉及参数的简单示例。

    unittest 文件test_argparse.py 有一个相当复杂的系统来简化解析器定义。

    class Sig(object):
    
        def __init__(self, *args, **kwargs):
            self.args = args
            self.kwargs = kwargs
    

    测试用例创建这些Sig 对象的列表:

    argument_signatures = [Sig('--foo-bar'), Sig('--baz', dest='zabbaz')]
    argument_signatures = [
        Sig('-x', type=float),
        Sig('-3', type=float, dest='y'),
        Sig('z', nargs='*'),
    ]
    

    解析器测试类有如下方法:

        def no_groups(parser, argument_signatures):
            """Add all arguments directly to the parser"""
            for sig in argument_signatures:
                parser.add_argument(*sig.args, **sig.kwargs)
    

    Ipython 有(或至少有几个版本)创建一个大型 argparse 解析器的代码,使用 config 文件条目来定义参数。

    【讨论】:

      最近更新 更多