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

2024-10-01 04:49:19 发布

您现在位置: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()一次,我们会得到test的本地副本(也称为文件_i),并且我们的全局变量test会被更新(我们得到0\n1

第二次调用move_selected()的默认值仍然是0(我们得到0\n2)。即使测试已经更新。如果我们显式地写move_selected(test.get()),结果是不一样的(我们会得到1\n2

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


Tags: 函数testselfgetmovereturndefval
3条回答

从你的问题中,我大胆地说:

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?

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

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

然后输出:

1
1
2
2

仍然不知道为什么投票被否决,因为这表明他们没有按照自己说的去做,以及如何正确地去做,从而使它起作用。它是这样做的,正如您从上面的输出也可以看到的一样try yourself online

默认值是在定义函数时计算的,而不是在调用函数时计算的。定义函数时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)

{a1}是处理这个问题的快速而正确的方法。不过,这需要解释为什么这是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中,这是通过^{}处理的。因此,我们可以这样说,为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附近常见的“gotcha”的一个变体

那么,如何在运行时动态获取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`.

这管用!与公认的答案相同,并且符合您通常认为的最佳实践。这很清楚,不需要对以原始方式调用它的任何调用站点进行任何更改

在默认值本身中定义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将执行它并返回一个值。尽管返回值(^{}),但我们不能保证36>})的类型为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.

相关问题 更多 >