(受到有关 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
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 del
a() < 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()
befored()
,也是a() < b() < c() < d()
如此。
“警告”:引用计数不相等,但除非您可以构造一个导致初始化、删除或评估顺序不同的情况,否则我会认为这是一个实现细节,而不是语义等价问题。我认为增加的引用计数在实践中不会成为问题,因为它们应该与表达式中采用的引用精确一致。
6
-
好吧,我想说程序不太可能受到不同引用计数的影响,但这是有可能的。由于这是受到某人为其他人编写的任意代码编写编译器的启发,我想准确地复制引用计数。谁知道其他人会写什么代码……
–
-
@nocomment 如果你正在编写一个编译器,你可以完全控制它。您不需要解决 Python 的特定实现细节。仅当您正在编写针对 Python 的转译器时,您才需要关心这一点,否则您可以创建这些“变量”作为适当的临时变量。正如所说,我认为这很可能已经很好了,因为不会发生任何奇怪的事情:变量的生命周期总是在涉及它的表达式被计算后立即结束。用户怎么可能因此而受到任何破坏呢?
–
-
对此不太确定。鼓舞人心的问题是关于“AEC 到 WebAssembly”,但我都不认识它们。
– -
@nocomment 已修复。我习惯了强制比较运算符生成布尔值的语言(例如 Lua)。
– -
啊,这就是你名字的由来吗?你是 Lua 疯子吗?
–
|
–
_a
在评估_a < _b
.它不这样做只是 Python 实现的一个限制;更复杂的实现将可以自由地做到这一点。–
a()
在变量中并且在第二次比较之前不删除它的答案。这确实可能会产生后果。–
–
–
|