我在 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
所有这些都是说,根据公认的答案,处理此问题的最简单和最好的方法是使用哨兵。
脚注
我忽略了OP并接受了答案对
global
的使用。为什么这是一种不好的做法,在别处有答案。我们可以使用
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
仍然不知道为什么这被否决了,因为它表明他们没有做他们所说的他们打算做的事情,以及如何正确地做它以使其起作用。它确实如此,正如您从上面的输出中看到的那样,您也可以在线尝试自己。