使用 "getter" 作为 python 函数的默认值
Using a "getter" as a default value of a python function
我在 python 的函数默认值中发现了这种非常奇怪的行为,对于我在看似相同的事物上出现两种不同行为的原因,我将不胜感激。
我们有一个非常简单的自定义 int class:
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
Class实例化:
test = CustomInt()
然后我们定义我们的函数接受一个getter作为默认参数
def move_selected(file_i = test.get()):
global test
test.increment()
print(file_i)
print(test)
如果我们点击 move_selected()
一次,我们会得到一个 test 的本地副本(又名 file_i)我们的全局变量 test 已更新(我们得到 0\n1
)
我们第二次调用时move_selected()
的默认值仍然是0(我们得到0\n2
)。即使测试已更新。如果我们明确地写 move_selected(test.get())
结果是不一样的(我们会得到 1\n2
)。
为什么?我们不应该将函数作为默认参数传递吗?
默认值是在定义函数时计算的,而不是在调用函数时计算的。当您定义函数时,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)
根据你的问题,由我加粗:
Then we define our function accepting a getter as a default argument
def move_selected(file_i = test.get()):
global test
test.increment()
print(file_i)
print(test)
[Why doesn't it update?] Are we not supposed to pass functions as default arguments?
你 不是 passing/accepting getter/function。您是 passing/accepting 调用 getter 的 结果 。这是 实际上 pass/accept getter 的方法。您所要做的就是将 ()
向下移动,因此默认为 实际上 getter 函数,您稍后 use/call 它.
def move_selected(file_i = test.get):
global test
test.increment()
print(file_i())
print(test)
然后输出:
1
1
2
2
仍然不知道为什么这会被否决,因为它显示了他们如何没有按照他们所说的那样去做,以及如何正确地做到这一点以使其发挥作用。它是做什么的,从上面的输出中也可以看到 try yourself online.
是处理此问题的快速且正确的方法。不过,这需要一些解释,为什么这是 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
当程序运行s,在def f...
执行到函数f
为'defined'时,t
已经解析为一个具体的价值,所以这行得通。不过,OP 有一个更微妙的问题——如果我们希望 t
的值在 运行 时间 是动态的 怎么办?这意味着当程序到达调用点(调用 f()
)时,它 然后 解析 t
.
的值
'natural' 常识性尝试是这样的:
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()
,其中 return 是一个值。这是 mutable default arguments.
周围常见 'gotcha' 的变体
那么如何在运行时间内动态获取x
的值呢?这就是问题的症结所在。有一些选择。常见的最佳做法是分配一个所谓的 'sentinel value'。 (旁白:None
是一个常见的哨兵值,但也经常是一个完全有效的实际值。)一个哨兵说 'we do not have a value for this, act accordingly'.
然后,在函数内,我们可以分配一个实际值。那看起来像什么?我们将使用 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`.
这有效!并且等同于已接受的答案,并且 运行s 符合您通常认为的最佳实践。很清楚,不需要对任何以原始方式调用它的调用站点进行任何更改f(t)
。
在默认值本身中定义 h
怎么样?我们不能在那里传递一个函数吗?对此的第一遍答案是 'yes'。让我们看看它是如何工作的:
def f(x: T = h) -> None:
x = x if x is not None else h()
# ... our code
之所以有效,是因为 h
具有 Callable[[], T]
类型,这意味着一旦调用它 return 就是 T
类型的值。我们没有使用 None
作为标记类型,而是使用 h
作为标记类型。它不会 运行 与过早定义冲突,因为 h
仅在 内部 函数中被调用,每次函数都是 运行 而不是仅定义函数时一次。
关于编译的高级旁白:Python 将 运行 遍历代码并建立所有函数,类 等,然后再编译或执行函数内的代码。因此,如果函数签名(即 def f(x: = h):
中有一个变量 (h
),它将在将该函数存储为可以在其他地方调用的东西之前解析该变量。但是,它将 not 在调用函数之前计算函数体。这就是为什么上面的节有效,其中 (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
就会执行它并且 return 一个值。尽管 return 值 (?
) 的类型是 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.
我在 python 的函数默认值中发现了这种非常奇怪的行为,对于我在看似相同的事物上出现两种不同行为的原因,我将不胜感激。
我们有一个非常简单的自定义 int class:
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
Class实例化:
test = CustomInt()
然后我们定义我们的函数接受一个getter作为默认参数
def move_selected(file_i = test.get()):
global test
test.increment()
print(file_i)
print(test)
如果我们点击 move_selected()
一次,我们会得到一个 test 的本地副本(又名 file_i)我们的全局变量 test 已更新(我们得到 0\n1
)
我们第二次调用时move_selected()
的默认值仍然是0(我们得到0\n2
)。即使测试已更新。如果我们明确地写 move_selected(test.get())
结果是不一样的(我们会得到 1\n2
)。
为什么?我们不应该将函数作为默认参数传递吗?
默认值是在定义函数时计算的,而不是在调用函数时计算的。当您定义函数时,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)
根据你的问题,由我加粗:
Then we define our function accepting a getter as a default argument
def move_selected(file_i = test.get()): global test test.increment() print(file_i) print(test)
[Why doesn't it update?] Are we not supposed to pass functions as default arguments?
你 不是 passing/accepting getter/function。您是 passing/accepting 调用 getter 的 结果 。这是 实际上 pass/accept getter 的方法。您所要做的就是将 ()
向下移动,因此默认为 实际上 getter 函数,您稍后 use/call 它.
def move_selected(file_i = test.get):
global test
test.increment()
print(file_i())
print(test)
然后输出:
1
1
2
2
仍然不知道为什么这会被否决,因为它显示了他们如何没有按照他们所说的那样去做,以及如何正确地做到这一点以使其发挥作用。它是做什么的,从上面的输出中也可以看到 try yourself online.
你原来的函数有这个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
当程序运行s,在def f...
执行到函数f
为'defined'时,t
已经解析为一个具体的价值,所以这行得通。不过,OP 有一个更微妙的问题——如果我们希望 t
的值在 运行 时间 是动态的 怎么办?这意味着当程序到达调用点(调用 f()
)时,它 然后 解析 t
.
'natural' 常识性尝试是这样的:
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()
,其中 return 是一个值。这是 mutable default arguments.
那么如何在运行时间内动态获取x
的值呢?这就是问题的症结所在。有一些选择。常见的最佳做法是分配一个所谓的 'sentinel value'。 (旁白:None
是一个常见的哨兵值,但也经常是一个完全有效的实际值。)一个哨兵说 'we do not have a value for this, act accordingly'.
然后,在函数内,我们可以分配一个实际值。那看起来像什么?我们将使用 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`.
这有效!并且等同于已接受的答案,并且 运行s 符合您通常认为的最佳实践。很清楚,不需要对任何以原始方式调用它的调用站点进行任何更改f(t)
。
在默认值本身中定义 h
怎么样?我们不能在那里传递一个函数吗?对此的第一遍答案是 'yes'。让我们看看它是如何工作的:
def f(x: T = h) -> None:
x = x if x is not None else h()
# ... our code
之所以有效,是因为 h
具有 Callable[[], T]
类型,这意味着一旦调用它 return 就是 T
类型的值。我们没有使用 None
作为标记类型,而是使用 h
作为标记类型。它不会 运行 与过早定义冲突,因为 h
仅在 内部 函数中被调用,每次函数都是 运行 而不是仅定义函数时一次。
关于编译的高级旁白:Python 将 运行 遍历代码并建立所有函数,类 等,然后再编译或执行函数内的代码。因此,如果函数签名(即 def f(x: = h):
中有一个变量 (h
),它将在将该函数存储为可以在其他地方调用的东西之前解析该变量。但是,它将 not 在调用函数之前计算函数体。这就是为什么上面的节有效,其中 (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
就会执行它并且 return 一个值。尽管 return 值 (?
) 的类型是 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.