accepted answer 是处理此问题的快速且正确的方法。不过,它需要解释一下为什么这是 Python 中的最佳实践。
你原来的函数有这个type signature:
f(x: T) -> None
(注意这是一个类型签名,不是带有类型提示的函数定义,因此缺少def)。
T 这里是file_i 的类型。 (虽然 OP 不清楚这是什么类型,但我们可以通过简单地使用 T 作为任何类型的替代来满足自己。)函数f 的调用站点将具有以下内容:
t = T() # t is created
# ... other code
f(t)
问题围绕着如何在呼叫站点执行此操作:
f() # No argument provided.
为此,新的函数签名更改为:
f(x: Option[T]) -> None
在 Python 中,处理这种情况的方式是通过default arguments。所以,我们可能会这样说,为f 提供一个默认参数:
t = T()
def f(x: T = t) -> None:
# ... our function
当程序运行时,在执行def f... 并且函数f 被“定义”时,t 已被解析为一个具体值,所以这是可行的。不过,OP 有一个更微妙的问题 - 如果我们希望 t 的值在运行时是动态的,该怎么办??这意味着当程序到达调用站点(调用f())时,它然后解析t的值。
“自然”的常识尝试是这样的:
def h() -> T:
t = T() # or however you want to dynamically create this.
def f(x: T = h()) -> None: # Instead of a concrete value, call `h()`!
# ... our code
f() # The call site, which relies on `h()` to fill in `x`.
不幸的是,这不起作用,因为当 Python 解析 f 的定义时会发生什么。它发现它需要为x 分配一个默认值,为了获得该值,它调用h(),它返回一个值。这是mutable default arguments 周围常见“陷阱”的变体。
那么如何在运行时动态获取x 的值呢?这是问题的症结所在。有一些选择。常见的最佳实践是分配一个所谓的'sentinel value'。 (旁白:None 是一个常见的哨兵值,但也有一个缺点,即通常也是一个完全有效的实际值。)哨兵说“我们对此没有价值,请采取相应的行动”。
然后,在函数中,我们可以分配一个实际值。那看起来像什么?我们将使用None 作为我们的哨兵。
def h() -> T:
t = T() # or however you want to dynamically create this.
def f(x: T = None) -> None: # If no value is provided, use the Sentinel.
x = x if x is not None else h()
# ... our code
f() # The call site, which relies on `h()` to fill in `x`.
这行得通!并且等同于公认的答案,并且符合您通常认为的最佳实践。很清楚,不需要对任何以原始方式调用它的调用站点进行任何更改f(t)。
在默认值本身中定义h 怎么样?我们不能在那里传递一个函数吗?对此的第一次回答是“是”。让我们看看它是如何工作的:
def f(x: T = h) -> None:
x = x if x is not None else h()
# ... our code
这是因为h 具有Callable[[], T] 类型,这意味着一旦调用它就会返回T 类型的值。我们没有使用None 作为我们的哨兵类型,而是使用h 作为我们的哨兵类型。它不会与过早定义发生冲突,因为h 仅在函数内部被调用,每次函数运行而不是在函数定义时只运行一次。
关于编译的高级问题:Python 将运行代码并在函数内编译或执行代码之前建立所有函数、类等。因此,如果一个函数签名(即def f(x: = h): 中有一个变量(h),它将在将该函数存储为可以在其他地方调用的东西之前解析该变量。但是,它不会 评估函数的主体,直到它被调用。这就是为什么上面的节有效,而 (def f(x: = h())) 无效。
这有一个可能需要的问题,我们可以在新的函数签名中看到:
f(x: Union[T, Callable[[], T]]) -> None
这意味着在呼叫站点我可以执行以下任何操作:
f(t) # the original way
f() # use the default value
f(g) # !!!
g 是什么?好吧,g 是任何类型为Callable[[], ?] 的已定义函数。只要g 不带参数,我们的函数f 就会执行它并返回一个值。尽管返回值 (?) 的类型为 T,但我们在这里无法保证。这种形式允许调用站点传递它自己的函数来确定该值 - 考虑到您的特定用例,这可能会更好!也许这很危险。这是根据上下文决定的。
请注意,这是一个容易犯的错误:
def f(x: T = h) -> None:
x = x() # location B (see below)
# ... our code
因为这会将我们的类型签名更改为:
f(x: Callable[[], T]) -> None
这是不同的,因为我们的呼叫站点发生了什么:
f(t) # original way, now can fail because `t` is not necessarily a `Callable` and location B will break.
f() # works
f(g) # also works
根据公认的答案,所有这些都是说处理这个问题的最简单和最好的方法是使用哨兵。
脚注
-
我忽略了 OP 并接受了答案对 global 的使用。为什么这是一个不好的做法是answered elsewhere。
-
如果希望 None 也成为我们的呼叫站点可以传递并期望使用的东西,我们可以使用 None 以外的东西作为我们的哨兵。
例子:
class Sentinel:
pass
UNDEFINED = Sentinel()
def f(x: T = UNDEFINED) -> None:
x = h() if isinstance(x, Sentinel) else x # or several possible variations.