使用 "getter" 作为 python 函数的默认值



我在 python 函数中的默认值中发现了这种非常奇怪的行为,我将不胜感激任何帮助,说明为什么我在看似同一件事上有两种不同的行为。

我们有一个非常简单的自定义 int 类:

class CustomInt(object):
def __init__(self, val=0):
self._val = int(val)
def increment(self, val=1):
self._val +=val
return self._val
def __str__(self):
return str(self._val)
def __repr__(self):
return 'CustomInt(%s)' % self._val
def get(self):
return self._val

类实例化:

test = CustomInt()

然后我们定义我们的函数接受一个 getter作为默认参数

def move_selected(file_i = test.get()):
global test
test.increment()
print(file_i)
print(test)

如果我们点击move_selected()一次,我们会得到测试的本地副本(又名file_i),并且我们的全局变量 test 会更新(我们得到0n1)

第二次调用它时move_selected()的默认值仍然是 0(我们得到0n2)。即使测试已更新。如果我们明确地写move_selected(test.get())结果就不一样了(我们会得到1n2)。

为什么?我们不应该将函数作为默认参数传递吗?

默认值是在定义函数时计算的,而不是在调用函数时计算的。定义函数时,test.get()的值为 0,所以这就是它的样子。

如果你想在函数每次运行时调用 getter,你可以在函数体中做到这一点:

def move_selected(file_i = None):
global test
test.increment()
if file_i == None:
file_i = test.get()
print(file_i)
print(test)

公认的答案是处理此问题的快速正确方法。不过,它需要解释为什么这是Python中的最佳实践。

您的原始函数具有以下类型签名:

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(),返回一个值。这是围绕可变默认参数的常见"陷阱"的变体。

那么如何在运行时动态获取x的值呢?这是问题的症结所在。有一些选择。常见的最佳实践是分配所谓的"哨兵值"。(题外话: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

所有这些都是说,根据公认的答案,处理此问题的最简单和最好的方法是使用哨兵。

脚注

  1. 我忽略了OP并接受了答案对global的使用。为什么这是一种不好的做法,在别处有答案。

  2. 我们可以使用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.

从你的问题中,我加粗:

然后我们定义我们的函数接受一个 getter作为默认参数

def move_selected(file_i = test.get()):
global test
test.increment()
print(file_i)
print(test)

[为什么不更新?我们不应该将函数作为默认参数传递吗?

没有传递/接受 getter/函数。您正在传递/接受调用该getter的结果。以下是实际传递/接受吸气器的方法。您所要做的就是将()向下移动,因此默认值实际上是getter 函数,您可以在以后使用/调用它。

def move_selected(file_i = test.get):
global test
test.increment()
print(file_i())
print(test)

然后输出:

1
1
2
2

仍然不知道为什么这被否决了,因为它表明他们没有做他们所说的他们打算做的事情,以及如何正确地做它以使其起作用。它确实如此,正如您从上面的输出中看到的那样,您也可以在线尝试自己。

最新更新