如何输入可变默认参数

15

处理Python中可变默认参数的方法是将它们设置为None

例如:

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

如果我输入函数定义,那么bar的唯一类型是Optional,但很明显,当我期望在其上运行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(None)foo(bar=None)的方式调用foo。(我认为没有其他更好的方式。)
编辑#2:如果您从未将bar类型化为Optional,而只将其作为List[int]类型,则Mypy将无错误运行,尽管默认值为None。然而,这是极其不推荐的,因为这种行为可能会在将来发生变化,而且它还会隐含地将参数类型定为Optional。(有关详细信息,请参见此处。)
5个回答

5

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将为false:两个空列表不是同一个对象,因为列表的可变性意味着你无法拥有一个始终由[]引用的单个空列表。

那么对于每个可变类型,我需要为其添加一个相应的新哨兵? - Pro Q
1
是的。使用单个“None”类型来表示任意类型的值的“缺席”与静态类型相反。例如,即使在具有“Maybe”类型构造函数以表示可选值的Haskell中,“Nothing:: Maybe a”值也是多态的:它不是单个值,而是一组整体的单态值,如“Nothing :: Maybe Int”,“Nothing :: Maybe String”等。 - chepner
@chepner 但这仍然是一个不良的实践。您可能会意外地发生突变,而不是有意地这样做,这会引入模糊的错误。对于像这样的简单函数,这可能没问题,但在更复杂的代码中确保不突变对象会更加困难。 - Zecong Hu
这似乎是一个不好的想法。对于任何阅读代码的人来说都会感到困惑,在生成的文档中,默认值将显示为“[]”,这看起来像是Python可变默认问题的错误。只使用=None似乎是一个更好的选择。 - user2357112
对于那些不知道 Python 可变默认问题的人来说,看到 [] 作为生成文档中的默认值会让人觉得可变默认值完全没有问题。 - user2357112
显示剩余2条评论

2
为什么不在你阴影 bar 时直接切掉铸造件呢?
def foo(bar: Optional[List[int]]=None):
    bar : List[int] = [] if bar is None else bar
    return sorted(bar)

2
mypy会针对变量类型重定义报告错误。您可以在此链接(https://mypy-play.net/?mypy=latest&python=3.9&gist=b86a0d4030c952533e29822a34974a36)上查看错误报告。 - user2357112
2
@Kraigolas mypy 是一个工具,用于验证类型提示及其使用情况,以帮助在 Python 中强制执行类型。编辑器可能对其外观感到满意,并且 Python 也能正常运行,但 mypy 对类型提示更严格,并指出了错误。 - TeddyBearSuicide
1
@Mythalorian 谢谢!对我来说,在Python中类型实际上并没有被检查,所以你必须强制转换而不是阴影似乎是武断和笨拙的(即使在Rust中,我也可以像在这个答案中所做的那样阴影一个变量),所以我会保留这个答案,以防OP对我的mypy异议感到满意,但还是谢谢你指出这一点! - Kraigolas
1
我个人正在寻找不会抛出Mypy错误的东西,但我认为这个答案对于其他可以接受Mypy错误的人仍然有帮助(也许Mypy最终会改变它的方式)。 - Pro Q
@ProQ 你是希望人们不带参数调用 foo() 吗?还是要求他们将某些东西传递给 foo - TeddyBearSuicide
显示剩余4条评论

2

我不确定这里的问题是什么,因为在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

这里有一些其他的例子可以用于类型缩小:

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的值。如果我正在阅读函数并直接从函数定义跳转到返回语句,我会感到非常困惑,因为函数定义说参数可以是None,但在返回语句中的函数不能使用None参数。 - Pro Q
2
@ProQ 我理解你的担忧,但我认为 Optional[List[int]] 在这里仍然是更好的选择。在我看来,函数签名是给用户用的,用户读取签名以了解他们需要提供哪些参数以及它们的类型。在这里,可以提供 None 作为参数,因此类型应包括 Optional。对于那些阅读函数实现的人来说,情况就不同了,因为他们需要在能够理解函数所做的事情之前阅读整个函数体,并且他们将看到您用默认值替换 None 的部分。 - Zecong Hu
如果您仍然担心清晰度,我建议使用注释或文档字符串可能会更好。此外,我相信默认情况下mypy允许省略类型提示中的“Optional”,如果默认值为None。 - Zecong Hu

0
如果您在函数的参数中添加默认值,那么它就是可选的。 此外,在您的代码中,您允许调用者不提供任何参数给函数,它仍然可以正常工作,因为它只会创建一个空列表。
此外,在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)

现在类型提示检测到bar是一个List[int]。你必须向foo传递一些东西。所以你可以做foo(None),这就是为什么你需要None检查,但现在foo()是无效的并且会抛出一个错误。
如果你确实不想让他们传入任何东西,那么只需这样做,类型提示仍然有效。
def foo(bar: List[int]=None):
   bar = [] if bar is None else bar
   return sorted(bar)

1
这是你提出的一个绝对有效的观点,但是 bar: List[int] 并没有清楚地表明 None 会以任何方式处理,并且 OP 希望类型提示在阅读代码的人能够理解时要明确。 - Kraigolas
2
我明白你的意思,但是 OP 说这个列表不是可选项,这听起来像是他们期望传入一些东西而不是一个空的函数调用,如 foo()。如果是这种情况,那么他们应该删除默认值并让 Python 抛出异常,这样调用它的人就知道它是必需的参数了。 - TeddyBearSuicide
Optional 只是假设了默认值为 None,而不是实际的列表。Optional[a] 类型提示并不意味着运行时的参数是可选的;它只是 Union[a, None] 的简写。 - chepner

0

使用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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接