在Python中更好地使用'make_pass_decorator'

在Python中更好地使用'make_pass_decorator',python,decorator,python-decorators,python-click,Python,Decorator,Python Decorators,Python Click,我正在寻找一些建议,以避免必须实例化一个类两次;这更多的是一个设计模式问题。我正在使用库创建一个应用程序 我有一个Settings类,它首先将所有初始默认设置加载到字典中(硬编码到应用程序中),然后将用户计算机上的TOML文件中的所有设置覆盖(如果指定)加载到字典中,最后将两者合并,并使它们作为类实例的属性可用(设置。) 对于这些设置中的大多数,我也希望能够指定命令行标志。然后优先级变为: 命令行标志。如果未指定,则回退到 TOML文件中的用户设置。如果未指定,则最终返回到 应用程序默认值 为了

我正在寻找一些建议,以避免必须实例化一个类两次;这更多的是一个设计模式问题。我正在使用库创建一个应用程序

我有一个
Settings
类,它首先将所有初始默认设置加载到字典中(硬编码到应用程序中),然后将用户计算机上的TOML文件中的所有设置覆盖(如果指定)加载到字典中,最后将两者合并,并使它们作为类实例的属性可用(
设置。

对于这些设置中的大多数,我也希望能够指定命令行标志。然后优先级变为:

  • 命令行标志。如果未指定,则回退到
  • TOML文件中的用户设置。如果未指定,则最终返回到
  • 应用程序默认值
  • 为了实现这一结果,我发现,在使用Click的装饰器时,我必须执行以下操作:

    import click
    from myapp import Settings
    
    settings = Settings()
    pass_settings = click.make_pass_decorator(Settings, ensure=True)
    
    @click.command()
    @click.help_option('-h', '--help')
    @click.option(
        '-s', '--disk-size',
        default=settings.instance_disk_size,
        help="Disk size",
        show_default=True,
        type=int
    )
    @click.option(
        '-t', '--disk-type',
        default=settings.instance_disk_type,
        help="Disk type",
        show_default=True,
        type=click.Choice(['pd-standard', 'pd-ssd'])
    )
    @pass_settings
    def create(settings, disk_size, disk_type):
        print(disk_size)
        print(disk_type)
    
    为什么两次?
    • 需要使用
      settings=settings()
      行提供
      @单击选项
      装饰程序的
      默认值
      值。
      默认值可以来自用户覆盖TOML文件(如果存在),也可以来自应用程序默认值
    • 这似乎是交错命令的推荐方式;甚至有人提到过。在函数内部,除了传递的CLI参数外,我有时还需要引用
      设置
      类中的其他属性

    我的问题是,哪一个更好?有没有一种方法可以在另一个
    click.option
    decorators中使用
    pass\u settings
    decorator?或者我应该放弃使用
    click.make\u pass\u decorator
    ?一种解决不想实例化
    设置
    两次的方法是从
    clic继承k、 选项
    ,然后将设置实例插入 上下文直接类似于:

    自定义类: 使用自定义类: 要使用自定义类,请将
    cls
    参数传递给
    @click.option()
    装饰器,如:

    # instantiate settings
    settings = Settings()
    
    # get the setting option builder
    settings_option_cls = build_settings_option_class(settings)
    
    # decorate with an option with an appropraie option name
    @click.option("--an_option", cls=settings_option_cls('default_setting_name'))
    
    这是怎么回事? 这是因为click是一个设计良好的OO框架。
    @click.option()
    装饰器通常实例化
    单击.Option
    对象,但允许使用cls参数过度执行此行为。因此它是一个相对简单的选项 很容易在我们自己的类中继承
    click.Option
    ,并超越所需的方法

    在本例中,我们使用两个闭包来捕获设置实例和参数名 类,我们可以通过单击.Option.handle_parse_result()
    将设置对象插入到上下文中。 这允许
    pass\u settings
    decorator在上下文中查找设置,因此不需要创建新实例

    测试代码: 测试结果: 不同意见 与其修改单击调用并使用动态类构造,不如将默认设置作为设置类的类属性公开。即:

    @click.option(
    '-t','-磁盘类型',
    默认值=settings.instance\u disk\u type,
    help=“磁盘类型”,
    show_default=True,
    类型=单击。选择(['pd-standard','pd ssd'])
    )
    
    变成

    @click.option(
    '-t','-磁盘类型',
    默认值=Settings.defaults.instance\u disk\u type,
    help=“磁盘类型”,
    show_default=True,
    类型=单击。选择(['pd-standard','pd ssd'])
    )
    
    这可能比在接受的答案中使用类构造函数更清晰,并且使代码的语义(含义)更清晰


    事实上,
    Settings.defaults
    很可能是
    Settings
    的一个实例。实例化两次并不重要,因为这并不是真正的问题,而是设置对象的客户机/消费者代码必须执行实例化。如果在
    Settings
    类中完成了实例化,它仍然是一个cle一个API,不需要调用方实例化两次。

    Stephen,这是一个高质量的答案,正是我想要的!我越来越精通Python,但像我这样的问题对初学者/中级用户来说往往很麻烦。非常感谢!@ScottCrooks,谢谢你的客气话。作为回报,我会说你的问题是质量也很高。去年我已经回答了一个问题,大多数问题的表达方式都不如这一个好。一个好问题通常更容易回答……祝你Python之旅好运。
    # instantiate settings
    settings = Settings()
    
    # get the setting option builder
    settings_option_cls = build_settings_option_class(settings)
    
    # decorate with an option with an appropraie option name
    @click.option("--an_option", cls=settings_option_cls('default_setting_name'))
    
    import click
    
    class Settings(object):
    
        def __init__(self):
            self.instance_disk_size = 100
            self.instance_disk_type = 'pd-ssd'
    
    
    settings = Settings()
    settings_option_cls = build_settings_option_class(settings)
    pass_settings = click.make_pass_decorator(Settings)
    
    
    @click.command()
    @click.help_option('-h', '--help')
    @click.option(
        '-s', '--disk-size',
        cls=settings_option_cls('instance_disk_size'),
        help="Disk size",
        show_default=True,
        type=int
    )
    @click.option(
        '-t', '--disk-type',
        cls=settings_option_cls('instance_disk_type'),
        help="Disk type",
        show_default=True,
        type=click.Choice(['pd-standard', 'pd-ssd'])
    )
    @pass_settings
    def create(settings, disk_size, disk_type):
        print(disk_size)
        print(disk_type)
    
    
    if __name__ == "__main__":
        commands = (
            '-t pd-standard -s 200',
            '-t pd-standard',
            '-s 200',
            '',
            '--help',
        )
    
        import sys, time
    
        time.sleep(1)
        print('Click Version: {}'.format(click.__version__))
        print('Python Version: {}'.format(sys.version))
        for cmd in commands:
            try:
                time.sleep(0.1)
                print('-----------')
                print('> ' + cmd)
                time.sleep(0.1)
                create(cmd.split())
    
            except BaseException as exc:
                if str(exc) != '0' and \
                        not isinstance(exc, (click.ClickException, SystemExit)):
                    raise
                    
    
    Click Version: 6.7
    Python Version: 3.6.2 (default, Jul 17 2017, 23:14:31) 
    [GCC 5.4.0 20160609]
    -----------
    > -t pd-standard -s 200
    200
    pd-standard
    -----------
    > -t pd-standard
    100
    pd-standard
    -----------
    > -s 200
    200
    pd-ssd
    -----------
    > 
    100
    pd-ssd
    -----------
    > --help
    Usage: test.py [OPTIONS]
    
    Options:
      -h, --help                      Show this message and exit.
      -s, --disk-size INTEGER         Disk size  [default: 100]
      -t, --disk-type [pd-standard|pd-ssd]
                                      Disk type  [default: pd-ssd]