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

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

脚注

  1. 我忽略了 OP 并接受了答案对 global 的使用。为什么这是一个不好的做法是 answered elsewhere.

  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.