【问题标题】:Is there a way to return a custom value for min and max in Python?有没有办法在 Python 中为最小值和最大值返回自定义值?
【发布时间】:2017-07-25 15:54:30
【问题描述】:

我有一个自定义类,

class A:
    def __init__(self, a, b):
        self.a = a
        self.b = b

该类不可迭代或可索引或类似的东西。如果可能的话,我想保持这种状态。是否有可能有类似以下的工作?

>>> x = A(1, 2)
>>> min(x)
1
>>> max(x)
2

让我想到这一点的是minmaxdocs 中被列为“通用序列操作”。由于range 被相同的文档认为是序列类型,所以我认为必须对range 进行某种优化,也许我可以利用它。

也许有一种我不知道的神奇方法可以实现这一点?

【问题讨论】:

  • 你的意思是让它工作而不定义__iter__/__next__?我不确定我是否理解您的困惑。
  • 当您说“类不可迭代”时,您的意思是“类不可迭代但我愿意使其可迭代”,还是“类不可迭代且绝对必须保持这种状态”?
  • “我在想一定有某种可能的范围优化” 有趣的是,情况似乎并非如此;虽然理论上 O(1) 最小/最大算法应该可以用于范围对象,但无论输入类型如何,implementation 似乎都是 O(N)。
  • 如果你不介意告诉我,我有点好奇为什么你不希望你的对象如此糟糕地可迭代。是为了防止用户做一些愚蠢的事情吗?也许有更好的方法?
  • @ChristianDean。我正在研究与range 等效的float 数字。我想出了一个解决方案,当指定 step 时它是可迭代的,否则不可迭代。它仍然会提供有效的遏制测试和其他一些东西,所以minmax 是有意义的,即使该步骤不存在。我想如果没有明确的步骤,我只需要允许它遍历边界。

标签: python python-3.x magic-methods


【解决方案1】:

是的。当min 接受一个参数时,它假定它是一个可迭代的,迭代它并取最小值。所以,

class A:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __iter__(self):
        yield self.a
        yield self.b

应该可以。

附加说明:如果您不想使用__iter__,我不知道该怎么做。您可能想创建自己的 min 函数,如果传递给它的参数中有一个方法,则调用一些 _min_ 方法并调用旧的 min 否则。

oldmin = min
def min(*args):
    if len(args) == 1 and hasattr(args[0], '_min_'):
        return args[0]._min_()
    else:
        return oldmin(*args)

【讨论】:

  • PEP 8 forbids 创建您自己的 dunder 方法。请改用__min
  • 我错过了什么,你为什么不能只调用方法min 而不是任何下划线变​​体?它似乎不是一个“神奇”的方法或任何东西,它只是一个将在标准库之外的其他地方调用的方法。
  • @Mephy 说得好。我想最初的想法是模仿标准库采用的特殊方法名称方法,但这并不是必需的。 .min() 可以正常工作。不过,使用下划线是一种表示“这种方法有一些特别之处”的方式。
  • '__min__' in dir(args[0]) 是写hasttr(args[0], '__min__') 的不好方式。 dir() 的语义没有很好的定义,而且首先创建一个所有属性名称的列表然后线性扫描它也很慢,而你只需要一个字典查找。
  • @SvenMarnach。就与其他笨蛋的一致性而言,可能更好的方法是hasattr(type(arg[0]), '__min__')
【解决方案2】:

没有__min____max__ 特殊方法*。这是一种耻辱,因为range 已经看到了一些pretty nice optimizations in Python 3。你可以这样做:

>>> 1000000000000 in range(1000000000000)
False

但除非你想等待很长时间,否则不要尝试这个:

>>> max(range(1000000000000))

但是,正如Lærne 所建议的那样,创建自己的min/max 函数是一个不错的主意。

我会这样做。更新:根据PEP 8 的建议,删除了名称__min__ 以支持_min

永远不要发明这样的名字;仅按文档使用它们

代码:

from functools import wraps

oldmin = min

@wraps(oldmin)
def min(*args, **kwargs)
    try:
        v = oldmin(*args, **kwargs)
    except Exception as err:
        err = err
    try:
        arg, = args
        v = arg._min()
    except (AttributeError, ValueError):
        raise err
    try:
        return v
    except NameError:
        raise ValueError('Something weird happened.')

我认为这种方式可能会更好一些,因为它可以处理其他答案没有考虑的一些极端情况。

请注意,具有_min 方法的可迭代对象仍将照常被oldmin 使用,但返回值被特殊方法覆盖。

但是,如果 _min 方法要求迭代器仍然可以使用,这将需要调整,因为迭代器首先被 oldmin 使用。

还要注意,如果__min 方法只是通过调用oldmin 来实现,那么事情仍然可以正常工作(即使迭代器已被消耗;这是因为oldmin 在这种情况下引发了ValueError)。

* 这种方法通常被称为“魔术”,但这不是首选术语。

【讨论】:

  • __min 名称混乱,因此命名一个方法真的很尴尬。
  • @user2357112 是的,这是真的。我将其更改为 _min
【解决方案3】:

由于range 被同一个文档视为序列类型,我在想range 必须进行某种优化,也许我可以利用它。

没有对范围进行优化,也没有针对min/max 的专用魔术方法。

如果您查看the implementation for min/max,您会看到在完成一些参数解析后,a call to iter(obj)(即obj.__iter__())会获取一个迭代器:

it = PyObject_GetIter(v);
if (it == NULL) {
    return NULL;
}

然后calls to next(it)(即it.__next__)在循环中执行以获取值进行比较:

while (( item = PyIter_Next(it) )) {
    /* Find min/max  */

有没有可能做类似下面的工作?

不,如果你想使用内置的min*,你唯一的选择是实现迭代器协议。


*通过修补min,你当然可以让它做任何你想做的事。显然是以在 Pythonland 中运营为代价的。但是,如果您认为可以利用一些优化,我建议您创建一个 min 方法,而不是重新定义内置的 min

此外,如果您只有整数作为实例变量并且您不介意不同的调用,您可以随时使用vars 来获取instance.__dict__,然后将它的.values() 提供给min

>>> x = A(20, 4)
>>> min(vars(x).values())
4

【讨论】:

  • 这是有道理的,虽然令人失望。我会问另一个关于如何识别调用者的问题,以便在minmax 以外的任何人试图获取迭代器时引发异常。我希望你不介意我给其他人积分。
  • @MadPhysicist 我认为您不需要为此发布另一个问题。除非有人在这里阅读评论不同意我的观点,否则我认为如果你做这样的事情来检查它就足够了:pastebin.com/AFNGcWHw
  • @MadPhysicist:不能那样做。 (你甚至在尝试的事实是一个巨大的“重新考虑你的设计决策”红旗。)
  • @user2357112 你是说不试图抓住 min/max 成为呼叫者吗?
  • @idjaw:您无法检测到您是否被来自min/max 呼叫。
猜你喜欢
  • 1970-01-01
  • 2022-10-07
  • 2021-07-23
  • 1970-01-01
  • 2019-03-17
  • 2019-10-08
  • 1970-01-01
  • 2021-07-27
  • 1970-01-01
相关资源
最近更新 更多