【问题标题】:Using a "getter" as a default value of a python function使用“getter”作为 python 函数的默认值
【发布时间】:2021-08-28 23:37:00
【问题描述】:

我在 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 的本地副本(又名 file_i)并且我们的全局变量 test 会更新(我们会得到 @987654325 @)

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

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

【问题讨论】:

  • 该函数不是默认的。它返回的值是默认值。 test.get() 在你的函数被定义并且它给出的值成为参数的默认值时被调用。
  • @khelwood 谢谢,我知道问题出在哪里,会更新帖子并使其更具可读性
  • 我很好奇您实际上要在这里做什么?如果我在代码审查中看到这段代码,没有明确的上下文,我会将它标记为有很多代码气味。我意识到你正在做一个最小的例子,但是使用global 并且有一个潜在的类型不匹配(file_i 也是一个 CustomInt - 命名不清楚?如果它调用get() 返回一个实际的 int)是令人担忧的。可能有一种更简洁的方式来获取您要查找的内容。
  • 我投票结束这个问题,因为这不是一个问题。
  • 我已经撤回了我的近距离投票。在您编辑之前确实没有问题。

标签: python default-arguments


【解决方案1】:

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

【讨论】:

  • 请注意,编译函数时定义的默认值的一个重要含义是,如果您将默认值设置为任何可变的值(又名file_list = []),则在函数中对该值的任何修改( file_list.append(...)) 将延续到下一次调用该函数时。所以像def move_selected(file_i = test) 这样的事情可能会让你陷入类似但不同的麻烦。
【解决方案2】:

accepted answer 是处理此问题的快速且正确的方法。不过,它需要解释一下为什么这是 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

当程序运行时,在执行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 周围常见“陷阱”的变体。

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

这行得通!并且等同于公认的答案,并且符合您通常认为的最佳实践。很清楚,不需要对任何以原始方式调用它的调用站点进行任何更改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 的使用。为什么这是一个不好的做法是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.

【讨论】:

  • 我认为最常见的哨兵(None 除外)是UNDEFINED = object(),然后是if x is UNDEFINED: x = h()。例如 discussed here 引用标准库执行此操作的位置。
  • 当然,我只是想表明 Sentinel 实际上可以是任何对象。单个库,例如美国国家航空航天局批准的 attrs 库,通常会有自己的哨兵对象,以便清楚起见并允许它们在这些类上实现附加功能(例如自定义错误消息)。
  • 即使他们进行对象身份检查(我认为没有自我分配)。为什么要进行类型检查?
  • 哦,我明白你在说什么。 isinstance 是我提出的变体,但是对象身份检查也可以工作,并且可以说更好,因为您不会冒险有人创建一些在某种程度上不兼容的 other 哨兵。因此or several possible variations 评论。
  • 非常感谢你们,我实际上通过你们的讨论学到了很多东西,尤其是关于哨兵值和如何 :) 也感谢您对全局变量的评论,但这毕竟只是为了理解这一点愚蠢的事情我没有得到这个星期六晚上。很高兴看到人们花时间教我一些实际的编程。
【解决方案3】:

根据你的问题,我加粗了:

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

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

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

没有传递/接受 getter/函数。您正在传递/接受 调用那个 getter 的 result。以下是实际上传递/接受 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

【讨论】:

  • 这提出了一个不同的问题:file_iintCustomInt 还是 Callable?将函数的第一行(在全局导入之后)设置为 file_i = file_i if file_i is not None else test.get() 会更干净很多
  • @NathanielFord 嗯?不,这是get 方法。 CustomInt 实例。如他们所愿。编辑:好的,是的,这是一个可调用的。应该是这样吗?
  • 所以file_i 的类型总是Callable?还是仅在需要默认值时?请注意,CustomInt 没有实现 __call__ 函数,因此如果传入 CustomInt,它将在 print(file_i()) 返回错误结果。
  • @NathanielFord 他们应该总是让它成为一个可调用的,是的。我不确定问题是什么。他们清楚地表明了他们“接受 getter 作为默认参数”的意图,我只是解释说他们没有这样做以及如何实际做到这一点。
  • 您没有在提供的(诚然不清楚指定的)限制范围内回答问题。如果我的函数签名是 def f(i: int) -> None 并且我想知道如何提供从 getter 函数中提取的整数作为默认值,那么您提供的解决方案是将我的函数更改为 def f(i: Union[int, Callable]) -> None。 @BatWannaBe 提供了一个精简示例,说明如何在约束内解决此问题。
猜你喜欢
  • 2012-08-01
  • 1970-01-01
  • 2015-03-15
  • 2019-09-11
  • 2017-07-08
  • 1970-01-01
  • 2010-10-03
  • 1970-01-01
相关资源
最近更新 更多