【问题标题】:How to type mutable default arguments如何键入可变的默认参数
【发布时间】:2021-08-22 02:41:46
【问题描述】:

在 Python 中处理可变默认参数的方法是 set them to None

例如:

def foo(bar=None):
    bar = [] if bar is None else bar
    return sorted(bar)

如果我输入函数定义,那么 bar 的唯一类型是 bar,而当我希望运行 sorted 函数时,显然它不是 Optional就可以了:

def foo(bar: Optional[List[int]]=None):
    bar = [] if bar is None else bar
    return sorted(bar) # bar cannot be `None` here

那我应该投吗?

def foo(bar: Optional[List[int]]=None):
    bar = [] if bar is None else bar
    bar = cast(List[int], bar) # make it explicit that `bar` cannot be `None`
    return sorted(bar)

我是否应该只希望通读函数的人看到处理默认可变参数的标准模式并理解对于函数的其余部分,参数不应该是Optional

处理此问题的最佳方法是什么?

编辑: 澄清一下,这个函数的用户应该能够调用foo作为foo()foo(None)foo(bar=None)。 (我认为任何其他方式都没有意义。)

编辑#2: 如果您从不将bar 输入为Optional 而是仅将其输入为List[int],Mypy 将run with no errors,尽管默认值为None。但是,强烈不建议这样做,因为这种行为将来可能会改变,而且它还隐式将参数键入为Optional。 (详见this。)

【问题讨论】:

    标签: python mutable typing default-arguments


    【解决方案1】:

    None 不是唯一可用的哨兵。您可以选择您的自己的列表值作为哨兵,在运行时用新的空列表替换它(而不是None)。

    _sentinel = []
    
    def foo(bar: List[int]=_sentinel):
        bar = [] if bar is _sentinel else bar
        return sorted(bar)
    

    只要没有人使用_sentinel 作为显式参数调用foobar 总是会得到一个新的空列表。在像foo([]) 这样的调用中,bar is _sentinel 将是错误的:两个空列表不是同一个对象,因为列表的可变性意味着您不能拥有一个始终被 [] 引用的空列表。

    【讨论】:

    • 所以对于我这样做的每个可变类型,我需要添加一个对应于该类型的新哨兵?
    • 是的。使用单个None 类型来表示任何任意类型的值的“不存在”与静态类型是对立的。例如,即使在具有用于表示可选值的 Maybe 类型构造函数的 Haskell 中,值 Nothing :: Maybe a 也是多态的:它不是单个值,而是像 Nothing :: Maybe IntNothing :: Maybe String 这样的一整套单态值等。
    • 您应该将_sentinel 设为object() 而不是一个空列表。拥有可变的默认参数是一个常见的陷阱:docs.python-guide.org/writing/gotchas/…
    • 只有当你真正改变默认值时,可变默认值才是问题。我们在这里不这样做;我们对_sentinel 所做的唯一事情就是将其与bar 进行比较并将其传递给sortedsorted(_sentinel) 将始终返回一个 new 空列表,而根本不修改 _sentinel
    • @chepner 但这仍然是一个不好的做法。您最终可能会无意中对其进行突变,并且会引入晦涩的错误。对于像这样的简单函数,它可能没问题,但要确保您没有在更复杂的代码中改变对象会更加困难。
    【解决方案2】:

    当你影子bar时,为什么不直接删掉演员:

    def foo(bar: Optional[List[int]]=None):
        bar : List[int] = [] if bar is None else bar
        return sorted(bar)
    

    【讨论】:

    • mypy reports an error 用于这种变量类型的重新定义。
    • @user2357112supportsMonica 这很有趣。它在我的 python 编辑器中运行良好。
    • @Kraigolas mypy 是一个用于验证类型提示及其用法以帮助强制在 python 中键入的工具。编辑器可能对它的外观没问题,python 会运行得很好,但 mypy 对类型提示更严格,说有错误。
    • @Mythalorian 谢谢!对我来说,由于实际上并没有在 python 中检查类型,因此您 must 投射而不是 shadow 的限制似乎是任意且笨重的(即使在 Rust 中,我也可以像我在这个答案中所做的那样对变量进行遮蔽),所以我会留下答案,以防 OP 与 mypy 存在分歧,但感谢您向我指出这一点!
    • 我个人正在寻找不会引发 Mypy 错误的东西,但我认为这个答案对于其他可以接受 Mypy 错误的人仍然有帮助(也许 Mypy 最终会改变它的方式)
    【解决方案3】:

    我不确定这里有什么问题,因为在 mypy 中使用 Optional[List[int]] 作为类型非常好:https://mypy-play.net/?mypy=latest&python=3.9&gist=2ee728ee903cbd0adea144ce66efe3ab

    在你的情况下,当 mypy 看到 bar = [] if bar is None else bar 时,它足够聪明地意识到 bar 不能是 None 超出这一点,从而将类型缩小到 List[int]。在此处阅读有关 mypy 中类型缩小的更多信息:https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=narrow#union-types

    这里是缩小类型的some other examples

    from typing import *
    
    a: Optional[int]
    assert a is not None
    reveal_type(a)  # builtins.int
    
    b: Union[int, float, str]
    if isinstance(b, int):
        reveal_type(b)  # builtins.int
    else:
        reveal_type(b)  # Union[builtins.float, builtins.str]
    

    【讨论】:

    • 我知道 MyPy 有这个功能,但我担心人类程序员的清晰度。我希望很明显None 不是该参数的真正可接受的值,而是一个默认值,并将尽快替换为不是None 的值。如果我在读函数,直接从函数定义跳转到return语句,我会很困惑,因为函数定义说参数可以是None,但是return语句中的函数不能带None参数。
    • @ProQ 我理解您的担忧,但我认为Optional[List[int]] 仍然是这里更好的选择。 IMO 函数签名适用于用户,用户阅读签名以了解他们需要提供哪些参数以及它们的类型。这里可以提供None 作为参数,所以类型应该包括Optional。对于那些正在阅读函数实现的人来说,情况就不同了,因为他们需要通读整个函数体才能理解函数的作用,他们会看到你用默认值替换 None 的部分。
    • 如果您仍然担心清晰度,我会说评论或文档字符串可能会更好。另外,我相信 mypy 默认 allows omitting 类型提示中的 Optional,如果默认为 None。
    【解决方案4】:

    如果您在函数中为参数添加默认值而不是“是”,则它是可选的。 同样在您的代码中,您允许调用者不向函数提供任何内容,它仍然有效,因为它只是创建一个空列表。

    在 python 中,即使键入它也不强制类型。这就是为什么它在文档中被称为“类型提示”。

    因此,如果您想允许调用者不带参数地调用该函数,并且它只是对一个空列表进行排序,那么您的代码是正确的。

    def foo(bar=None):
        bar = [] if bar is None else bar
        return sorted(bar)
    

    但是,如果您不希望调用者不提供任何内容,甚至是 None,那么您应该更改您的函数签名。

    def foo(bar: List[int]):
        bar = [] if bar is None else bar
        return sorted(bar)
    

    现在类型提示检测到barList[int]。你必须将一些东西传递给foo。所以你可以做foo(None)这就是为什么你需要None检查但现在foo()是无效的并抛出一个错误。

    如果您确实希望他们不传递任何内容,那么只需执行此操作,输入提示仍然有效。

    def foo(bar: List[int]=None):
       bar = [] if bar is None else bar
       return sorted(bar)
    

    【讨论】:

    • 这绝对是您提出的一个有效观点,但 bar: List[int] 并没有明确说明 None 将以任何方式处理,看起来 OP 希望类型提示是明确的让阅读代码的人能够理解。
    • 我明白你在说什么,但 OP 说该列表不是可选的,这听起来像是他们希望传入一些东西,而不是像 foo() 这样的空函数调用。如果是这种情况,他们应该删除默认值并让 python 抛出异常,以便调用它的人知道它是必需的参数。
    • Optional 被假定只是因为None,而不是一个实际的列表,被用作默认值。 Optional[a] 类型提示并不意味着运行时的参数是可选的;它只是 Union[a,None] 的缩写。
    【解决方案5】:

    使用Sequence[int]怎么样?将检查参数(包括其默认值)几乎是只读

    from collections.abc import Sequence
    
    def foo(bar: Sequence[int] = []) -> list[int]:
        return sorted(bar)
    

    bar 仍然可以如下变异,但风险不会很高。 (_sentinel: list[int] = [] 也可以变异。)

    def foo(bar: Sequence[int] = []) -> list[int]:
        if isinstance(bar, list):
            # reveal_type(bar)  # => Revealed type is "builtins.list[Any]"
            bar.append(0)
        return sorted(bar)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-03-12
      • 2021-10-11
      • 2021-08-18
      • 1970-01-01
      • 2011-05-07
      • 2011-08-21
      • 2017-02-20
      相关资源
      最近更新 更多