(受到有关 AEC 到 WebAssembly 编译器及其答案的的启发,我可以想象这可能很重要。)

简单的a() < b() and b() < c()并不等同,因为b()可能会被调用两次。

使用变量更好:

_a = a()
_b = b()
_a < _b and _b < c()

但即便如此也不是 100% 等效。特别是, 的值a()一直保持有效,直到第二次比较之后,这可能会出现问题。我记录了所有三个版本的事件:

chained:
a() b() (3 4) del c() (3 3) del del 

simple_and:
a() b() (3 3) del del b() c() (3 3) del del 

variables:
a() b() (4 4) c() (4 3) del del del 

del记录函数返回的对象的“删除”,数字对记录比较期间两个比较对象的引用计数。)

是否有一种无需链接的等效方法?

测试脚本:

def chained():
    return a() < b() < c()

def simple_and():
    return a() < b() and b() < c()

def variables():
    _a = a()
    _b = b()
    return _a < _b and _b < c()


import sys

class X:
    def __lt__(self, other):
        print(end=f'({sys.getrefcount(self)} {sys.getrefcount(other)}) ')
        return True
    def __del__(self):
        print(end='del ')

def a(): print(end='a() '); return X()
def b(): print(end='b() '); return X()
def c(): print(end='c() '); return X()

for f in chained, simple_and, variables:
    print(f.__name__ + ':')
    f()
    print('\n')

6

  • 5
    如果您是足够疯狂的科学家来编写此类代码,则每个代码都可以检查 AST 并表现出不同的行为。 😉 您想用它来解决实际问题吗?这个问题有点“XY 问题”的味道,尽管我也有同样的好奇心。


    – 

  • 1
    寻找一个满足 Python 实现内部结构的“等效”结构似乎不太值得。基于变量的方法应该允许 Python_a在评估_a < _b.它不这样做只是 Python 实现的一个限制;更复杂的实现将可以自由地做到这一点。


    – 

  • 1
    @UlrichEckhardt是的,如果您允许代码检查,那么当然没有等效的。这会让这个问题变得毫无意义,所以让我们说除此之外的等价物。对来说没有实际问题,我只是看到了其他问题及其答案,我想对他们来说这可能很重要。特别是建议保存a()在变量中并且在第二次比较之前不删除它的答案。这确实可能会产生后果。


    – 


  • 1
    @Luatic实际上,它可能不能。将 Python 对象的垃圾回收资格基于活动性而不是可达性,可能会对终结器造成严重破坏,并引发无数的释放后使用错误。


    – 

  • 这是否也会根据 Python 实现而改变,即它是否使用引用计数或垃圾收集器?


    – 


4 个回答
4

您可以使用 walrus 运算符来保存以下值b()

(a() < (temp_b := b())) and (temp_b < c())

我在回答启发您的问题时使用了相同的结构。

11

  • 哎呀,我没有一直向下滚动,错过了你的答案。但它仍然不等同。第二次比较期间的参考计数是 (4 3) 而不是 (3 3)。


    – 

  • 仅使用变量,但仅b()存储在变量中,似乎也可行。虽然参考计数不同,但我认为这没有问题。


    – 


  • @Luatic如果你的意思是我认为你的意思,那么b()之前就会被调用a()。那就更糟了。


    – 

  • @Luatic我不确定你的意思。我已经只存储b()在变量中。如果链更长,我们需要所有中间体的变量。


    – 

  • 2
    @Luatic实际上,如果您只使用一个变量,它确实可以很好地推广到更长的链:(a() < (tmp := b())) and (tmp < (tmp := c())) and (tmp < (tmp := d())) and (tmp < e())。如果你完成了,那么它甚至与你得到的and (tmp < ((tmp := None) or e()))相同a() b() (3 4) del c() (3 4) del d() (3 4) del e() (3 3) del dela() < b() < c() < d() < e()


    – 


如果将函数放在可迭代中,例如 [a, b, c],则允许​​“all”仅拉取所需的量,从而复制链式“and”操作的短路,但您可以控制执行“调用”操作,因此只发生一次。

from itertools import pairwise
from operator import call

def strictly_increasing(values) -> bool:
    """ test if values are strictly increasing """
    return all((x < y for x, y in pairwise(values)))

def function_values_increasing(functions) -> bool:
    """ test if function values are strictly increasing with no more than one call """
    return strictly_increasing(map(call, functions))\


def a():
    print ("a 1")
    return 1

def b():
    print("b 2")
    return 2

def c():
    print("c 3")
    return 3

print(function_values_increasing([a, b, c]))
print(function_values_increasing([c, b, a]))

a 1

b 2


c 3


正确


c 3


b 2


错误

第一个测试用例用于增加值,并显示每个函数仅被调用一次。

第二个测试用例是减小值,并表明由于预期的短路,最后一个函数未被正确调用。

至于函数内临时对象的确切生命周期,这并不重要。

如果你没有3.11

def call(f):
    """ from operator import call """
    return f()

如果你没有3.10

def pairwise(iterable):
    # pairwise('ABCDEFG') → AB BC CD DE EF FG
    iterator = iter(iterable)
    a = next(iterator, None)
    for b in iterator:
        yield a, b
        a = b

3

  • 为了使其适用于不同代码片段的评估,我认为它将……非常相似,除了使用评估而不是调用。但 OP 只有需要调用的函数,所以我就这么做了。


    – 

  • “就函数内临时对象的确切生命周期而言,这并不重要” – 这可能很重要。取决于代码在做什么。我的日志记录代码能够显示它,其他代码也可以使用它。人们有时会做一些奇怪的事情。


    – 

  • 我可以复制不同订单的副作用并支持任意长度,但我承认,我无法复制生命周期。我相信,如果生命周期很重要,它就不会被删除,除非你专门在 GC 过程中添加副作用——我承认我没有处理过。我敢打赌pairwise持有一个参考。也许我可以稍后完善它。


    – 


我们可以通过列表和弹出来复制原始内容:

def popping():
    _a = [a()]
    _b = [b()]
    return _a.pop() < _b[0] and _b.pop() < c()

简化:

def popping_2():
    _b = [b()]
    return a() < _b[0] and _b.pop() < c()

以及受字节码启发的版本import dis; dis.dis('a < b < c')

def popping_3():
    stack = [a(), b()]
    stack.reverse()
    stack[-2:-1] *= 2
    return stack.pop() < stack.pop() and stack.pop() < c()

日志():

chained:
a() b() (3 4) del c() (3 3) del del 

popping:
a() b() (3 4) del c() (3 3) del del 

popping_2:
b() a() (3 4) del c() (3 3) del del 

popping_3:
a() b() (3 4) del c() (3 3) del del 

更长的通用案例/解决方案:

def chained():
    return a() < b() < c() < d() < e()

def popping_4():
    stack = []
    def push(x):
        stack.append(x)
        return x
    pop = stack.pop
    return (
        a() < push(b()) and
        pop() < push(c()) and
        pop() < push(d()) and
        pop() < e()
    )

日志():

chained:
a() b() (3 4) del c() (3 4) del d() (3 4) del e() (3 3) del del 

popping_4:
a() b() (3 4) del c() (3 4) del d() (3 4) del e() (3 3) del del 

残酷的解决方案:使用更多变量,当您不再需要它们时删除它们,以强制执行您想要的操作的初始化/删除和顺序(和计数)():

def variables():
    _a = a()
    _b = b()
    a_lt_b = _a < _b
    del _a
    return a_lt_b and _b < c()

输出:a() b() rc(a) = 4, rc(b) = 4 del a c() rc(b) = 4, rc(c) = 3 del c del b

这也适用于更长的链:

def variables():
    _a = a()
    _b = b()
    a_lt_b = _a < _b
    if not a_lt_b:
        return a_lt_b # short-circuiting
    del _a
    _c = c()
    b_lt_c = _b < _c
    if not b_lt_c:
        return b_lt_c
    del _b
    _d = d()
    c_lt_d = _c < _d
    del _c
    del _d
    return c_lt_d

通过准确地阐明您想要发生的事情以及何时发生,您可以获得所需的顺序

  • a()b()c()和,每个最多评估d()一次(短路);
  • 一旦计算出涉及每个项的比较(从左到右),就将其删除;
  • 删除c()before d(),也是a() < b() < c() < d()如此。

“警告”:引用计数不相等,但除非您可以构造一个导致初始化、删除或评估顺序不同的情况,否则我会认为这是一个实现细节,而不是语义等价问题。我认为增加的引用计数在实践中不会成为问题,因为它们应该与表达式中采用的引用精确一致。

6

  • 好吧,我想说程序不太可能受到不同引用计数的影响,但这是有可能的。由于这是受到某人其他人编写的任意代码编写编译器的启发,我想准确地复制引用计数。谁知道其他人会写什么代码……


    – 


  • @nocomment 如果你正在编写一个编译器,你可以完全控制它。您不需要解决 Python 的特定实现细节。仅当您正在编写针对 Python 的转译器时,您才需要关心这一点,否则您可以创建这些“变量”作为适当的临时变量。正如所说,我认为这很可能已经很好了,因为不会发生任何奇怪的事情:变量的生命周期总是在涉及它的表达式被计算后立即结束。用户怎么可能因此而受到任何破坏呢?


    – 


  • 对此不太确定。鼓舞人心的问题是关于“AEC 到 WebAssembly”,但我都不认识它们。


    – 

  • @nocomment 已修复。我习惯了强制比较运算符生成布尔值的语言(例如 Lua)。


    – 

  • 啊,这就是你名字的由来吗?你是 Lua 疯子吗?


    –