【问题标题】:Python: Typehints for argparse.Namespace objectsPython:argparse.Namespace 对象的类型提示
【发布时间】:2017-07-05 20:47:09
【问题描述】:

有没有办法让 Python 静态分析器(例如,在 PyCharm 中,其他 IDE 中)在 argparse.Namespace 对象上的 Typehints 上拾取?示例:

parser = argparse.ArgumentParser()
parser.add_argument('--somearg')
parsed = parser.parse_args(['--somearg','someval'])  # type: argparse.Namespace
the_arg = parsed.somearg  # <- Pycharm complains that parsed object has no attribute 'somearg'

如果我删除内联注释中的类型声明,PyCharm 不会抱怨,但它也不会拾取无效属性。例如:

parser = argparse.ArgumentParser()
parser.add_argument('--somearg')
parsed = parser.parse_args(['--somearg','someval'])  # no typehint
the_arg = parsed.somaerg   # <- typo in attribute, but no complaint in PyCharm.  Raises AttributeError when executed.

有什么想法吗?


更新

受下面Austin's answer 的启发,我能找到的最简单的解决方案是使用namedtuples

from collections import namedtuple
ArgNamespace = namedtuple('ArgNamespace', ['some_arg', 'another_arg'])

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: ArgNamespace

x = parsed.some_arg  # good...
y = parsed.another_arg  # still good...
z = parsed.aint_no_arg  # Flagged by PyCharm!

虽然这令人满意,但我仍然不喜欢重复参数名称。如果参数列表显着增长,更新两个位置将是乏味的。理想的方法是以某种方式从 parser 对象中提取参数,如下所示:

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
MagicNamespace = parser.magically_extract_namespace()
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: MagicNamespace

我无法在 argparse 模块中找到任何可以使这成为可能的东西,我仍然不确定 any 静态分析工具是否足够聪明以获取这些值并且不要让 IDE 陷入停顿。

仍在搜索中...


更新 2

根据 hpaulj 的评论,我能找到的最接近上述方法的“神奇地”提取已解析对象的属性的方法是从每个解析器的 _actions 中提取 dest 属性。 :

parser = argparse.ArgumentParser()
parser.add_argument('--some-arg')
parser.add_argument('--another-arg')
MagicNamespace = namedtuple('MagicNamespace', [act.dest for act in parser._actions])
parsed = parser.parse_args(['--some-arg', 'val1', '--another-arg', 'val2'])  # type: MagicNamespace

但这仍然不会导致在静态分析中标记属性错误。如果我在 parser.parse_args 调用中传递 namespace=MagicNamespace 也是如此。

【问题讨论】:

  • 一个快速的谷歌说你可以在第一次使用局部变量时使用类型提示。在parser = argparse.ArgumentParser() # type: argparse.Namespace 上尝试一下,看看它是否有效。
  • @Austin: parser 在这种情况下是 argparse.ArgumentParser 对象,而不是 argparse.Namespace 对象。我希望用 args 作为属性填充 parsed 对象。
  • 你是对的。我错过了parsedparser. 你真正想要的似乎是 PyCharm 在构建 ArgumentParser 时解析方法参数。我怀疑这是否行得通。
  • add_argument 返回它刚刚创建的 Action 对象。看它的属性。 parser._actions 是所有这些操作的列表,解析器在解析期间使用这些操作。我在之前的 SO 答案中提到过它们。
  • 在您的新编辑中,您是否将新命名空间传递给parse_args

标签: python pycharm argparse type-hinting python-typing


【解决方案1】:

Typed argument parser 正是为此目的而制作的。它包装了argparse。您的示例实现为:

from tap import Tap


class ArgumentParser(Tap):
    somearg: str


parsed = ArgumentParser().parse_args(['--somearg', 'someval'])
the_arg = parsed.somearg

这是它的图片。

它在 PyPI 上,可以安装:pip install typed-argument-parser

完全披露:我是这个库的创建者之一。

【讨论】:

    【解决方案2】:

    考虑为argparse.Namespace 定义一个扩展类,以提供所需的类型提示:

    class MyProgramArgs(argparse.Namespace):
        def __init__():
            self.somearg = 'defaultval' # type: str
    

    然后使用namespace= 将其传递给parse_args

    def process_argv():
        parser = argparse.ArgumentParser()
        parser.add_argument('--somearg')
        nsp = MyProgramArgs()
        parsed = parser.parse_args(['--somearg','someval'], namespace=nsp)  # type: MyProgramArgs
        the_arg = parsed.somearg  # <- Pycharm should not complain
    

    【讨论】:

    • 在此类中定义的defaultval 会覆盖解析器方法中定义的任何默认参数。这可能是可取的。但这是使用自定义命名空间时需要注意的一个细节。
    【解决方案3】:

    我对 PyCharm 如何处理这些类型提示一无所知,但了解Namespace 代码。

    argparse.Namespace 是一个简单的类;本质上是一个带有一些方法的对象,可以更轻松地查看属性。为了便于单元测试,它有一个__eq__ 方法。您可以阅读argparse.py 文件中的定义。

    parser 以最通用的方式与命名空间交互——getattrsetattrhasattr。因此,您几乎可以使用任何dest 字符串,即使是您无法使用.dest 语法访问的字符串。

    确保不要混淆add_argumenttype=参数;这是一个函数。

    按照其他答案中的建议使用您自己的 namespace 类(从头开始或子类化)可能是最佳选择。文档中对此进行了简要描述。 Namespace Object。尽管我已经建议过几次以处理特殊的存储需求,但我还没有看到这样做太多。所以你必须尝试一下。

    如果使用子解析器,使用自定义命名空间类可能会中断,http://bugs.python.org/issue27859

    注意处理默认值。大多数argparse 操作的默认默认值为None。如果用户没有提供此选项,则在解析后使用它来做一些特殊的事情会很方便。

     if args.foo is None:
         # user did not use this optional
         args.foo = 'some post parsing default'
     else:
         # user provided value
         pass
    

    这可能会妨碍输入提示。无论您尝试何种解决方案,请注意默认设置。


    namedtuple 不能用作 Namespace

    首先,自定义命名空间类的正确使用是:

    nm = MyClass(<default values>)
    args = parser.parse_args(namespace=nm)
    

    也就是说,您初始化该类的一个实例,并将其作为参数传递。返回的args 将是同一个实例,具有通过解析设置的新属性。

    第二,namedtuple只能创建,不能更改。

    In [72]: MagicSpace=namedtuple('MagicSpace',['foo','bar'])
    In [73]: nm = MagicSpace(1,2)
    In [74]: nm
    Out[74]: MagicSpace(foo=1, bar=2)
    In [75]: nm.foo='one'
    ...
    AttributeError: can't set attribute
    In [76]: getattr(nm, 'foo')
    Out[76]: 1
    In [77]: setattr(nm, 'foo', 'one')    # not even with setattr
    ...
    AttributeError: can't set attribute
    

    命名空间必须与 getattrsetattr 一起使用。

    namedtuple 的另一个问题是它没有设置任何类型的type 信息。它只是定义字段/属性名称。所以静态类型不需要检查。

    虽然从parser 获取预期的属性名称很容易,但您无法获得任何预期的类型。

    对于一个简单的解析器:

    In [82]: parser.print_usage()
    usage: ipython3 [-h] [-foo FOO] bar
    In [83]: [a.dest for a in parser._actions[1:]]
    Out[83]: ['foo', 'bar']
    In [84]: [a.type for a in parser._actions[1:]]
    Out[84]: [None, None]
    

    Actions dest 是正常的属性名称。但是type 不是该属性的预期静态类型。它是一个可能会也可能不会转换输入字符串的函数。这里None 表示输入字符串按原样保存。

    因为静态类型和argparse 需要不同的信息,所以没有一种简单的方法可以从另一个生成一个。

    我认为你能做的最好的事情就是创建你自己的参数数据库,可能在字典中,然后使用你自己的实用函数创建命名空间类和解析器。

    假设dd 是带有必要键的字典。然后我们可以创建一个参数:

    parser.add_argument(dd['short'],dd['long'], dest=dd['dest'], type=dd['typefun'], default=dd['default'], help=dd['help'])
    

    您或其他人必须想出一个命名空间类定义,该定义从这样的字典中设置default(简单)和静态类型(困难?)。

    【讨论】:

      【解决方案4】:

      如果您处于可以从头开始的情况,有一些有趣的解决方案,例如

      但是,就我而言,它们并不是一个理想的解决方案,因为:

      1. 我有许多基于 argparse 的现有 CLI,我无法使用此类 args-inferred-from-types 方法重新编写它们。
      2. 从类型推断 args 时,支持普通 argparse 支持的所有高级 CLI 功能可能会很棘手。
      3. 与替代方法相比,在普通命令式 argparse 中重用多个 CLI 中的常见 arg 定义通常更容易。

      因此,我开发了一个小型库 typed_argparse,它允许引入类型化的 args 而无需进行太多重构。这个想法是添加一个派生自特殊 TypedArg 类的类型,然后简单地包装普通的 argparse.Namespace 对象:

      # Step 1: Add an argument type.
      class MyArgs(TypedArgs):
          foo: str
          num: Optional[int]
          files: List[str]
      
      
      def parse_args(args: List[str] = sys.argv[1:]) -> MyArgs:
          parser = argparse.ArgumentParser()
          parser.add_argument("--foo", type=str, required=True)
          parser.add_argument("--num", type=int)
          parser.add_argument("--files", type=str, nargs="*")
          # Step 2: Wrap the plain argparser result with your type.
          return MyArgs(parser.parse_args(args))
      
      
      def main() -> None:
          args = parse_args(["--foo", "foo", "--num", "42", "--files", "a", "b", "c"])
          # Step 3: Done, enjoy IDE auto-completion and strong type safety
          assert args.foo == "foo"
          assert args.num == 42
          assert args.files == ["a", "b", "c"]
      

      这种方法略微违反了单源真实性原则,但该库执行完整的运行时验证以确保类型注释与 argparse 类型匹配,并且它只是迁移到类型化 CLI 的一个非常简单的选项。

      【讨论】:

        【解决方案5】:

        这些答案中的大多数都涉及使用另一个包来处理打字。只有当没有像我将要提出的那样简单的解决方案时,这才是一个好主意。

        步骤 1. 类型声明

        首先,在数据类中定义每个参数的类型,如下所示:

        from dataclasses import dataclass
        
        @dataclass
        class MyProgramArgs:
            first_var: str
            second_var: int
        

        步骤 2. 参数声明

        然后,您可以使用匹配的参数设置您喜欢的解析器。例如:

        import argparse
        
        parser = argparse.ArgumentParser("This CLI program uses type hints!")
        parser.add_argument("-a", "--first-var")
        parser.add_argument("-b", "--another-var", type=int, dest="second_var")
        

        步骤 3. 解析参数

        最后,我们以静态类型检查器知道每个参数类型的方式解析参数:

        my_args = MyProgramArgs(**vars(parser.parse_args())
        

        现在类型检查器知道my_argsMyProgramArgs 类型,因此它确切知道哪些字段可用以及它们的类型。

        【讨论】:

          【解决方案6】:

          如果您的论点很少,另一种可能是理想的方法如下。

          首先创建一个函数来设置解析器并返回命名空间。例如:

          def parse_args() -> argparse.Namespace:
              parser = argparse.ArgumentParser()
              parser.add_argument("-a")
              parser.add_argument("-b", type=int)
              return parser.parse_args()
          

          然后你定义一个主函数,它接收你在上面单独声明的参数;像这样。

          def main(a: str, b: int):
              print("hello world", a, b)
          

          当你调用你的 main 时,你会这样做:

          if __name__ == "__main__":
              main(**vars(parse_args())
          

          从你的 main 开始,你的变量 ab 将被你的静态类型检查器正确识别,尽管你不会再有一个包含所有参数的对象,这可能是一个好的或坏事取决于你的用例。

          【讨论】:

            猜你喜欢
            • 2017-11-21
            • 2013-07-18
            • 2011-12-11
            • 2016-10-15
            • 2010-10-21
            • 2023-03-22
            • 2013-03-11
            • 2017-09-09
            相关资源
            最近更新 更多