如何更简洁地找到缺失的值?

75
以下代码检查变量 xy 是否为不同的值(变量xyz 只能取 abc 这三个值),如果是,则将 z 设置为第三个字符:
if x == 'a' and y == 'b' or x == 'b' and y == 'a':
    z = 'c'
elif x == 'b' and y == 'c' or x == 'c' and y == 'b':
    z = 'a'
elif x == 'a' and y == 'c' or x == 'c' and y == 'a':
    z = 'b'

有没有更简洁、易读和高效的方法来实现这个?


6
答案是“是的”!Python的集合非常适用于检查不同性并计算未使用的元素。 - Raymond Hettinger
1
感谢大家的回答,我想我会使用使用集合的解决方案,因为它既快速又易读。Óscar López 基于查找表的答案也很有趣。 - Bunny Rabbit
11个回答

62
z = (set(("a", "b", "c")) - set((x, y))).pop()
我假设你的代码中有三种情况之一成立。如果是这种情况,集合set(("a", "b", "c")) - set((x, y))将只包含一个元素,并且这个元素由pop()返回。 编辑: 如评论中Raymond Hettinger建议的那样,您也可以使用元组解压缩来从集合中提取单个元素:
z, = set(("a", "b", "c")) - set((x, y))

26
如果你使用的是 Python 2.7/3.1 或更高版本,你可以使用集合字面值更加简洁地编写代码,像这样:z = ({'a', 'b', 'c'} - {x, y}).pop()。其中 z 是一个变量名,表示从集合 {'a','b','c'} 中减去另外两个变量 xy 所组成的集合后,取出剩下的唯一元素并赋值给 z - Taymon
7
pop() 操作是不必要且缓慢的。应使用元组展开代替。同时,set(("a", "b", "c")) 的值不变,因此可以预先计算一次,仅在循环中使用集合差分(如果未在循环中使用,则对速度的影响不大)。 - Raymond Hettinger
3
@Ed:我知道,但是原帖没有说明当 x == y 时应该怎么处理,所以我省略了这个测试。如果有需要,很容易添加 if x != y: - Sven Marnach
4
个人而言,我会选择第一个选项,因为它比随机逗号更明显。 - John
1
你可能想用 set("abc") 替换 set(("a", "b", "c")) - kasyc
显示剩余2条评论

47

strip方法是另一种对我来说运行速度很快的选择:

z = 'abc'.strip(x+y) if x!=y else None

2
+1 它也非常透明,与大多数答案不同,它处理x==y的情况。 - Ed Staub
1
好主意,点赞;不过我认为原帖中的“a”、“b”和“c”只是真实值的占位符。这个解决方案不能推广到除了长度为1的字符串之外的任何其他值类型。 - Sven Marnach
@chepner 很有创意!感谢您的回答,chepner。 - Bunny Rabbit

27

Sven的优秀代码做了一些多余的工作,可以使用元组解包而不是pop()。此外,它可以添加一个保护if x != y来检查xy是否不同。这是改进后的答案:

# create the set just once
choices = {'a', 'b', 'c'}

x = 'a'
y = 'b'

# the main code can be used in a loop
if x != y:
    z, = choices - {x, y}

这里有一个计时套件,用于展示相对性能的比较时间:
import timeit, itertools

setup_template = '''
x = %r
y = %r
choices = {'a', 'b', 'c'}
'''

new_version = '''
if x != y:
    z, = choices - {x, y}
'''

original_version = '''
if x == 'a' and y == 'b' or x == 'b' and y == 'a':
    z = 'c'
elif x == 'b' and y == 'c' or x == 'c' and y == 'b':
    z = 'a'
elif x == 'a' and y == 'c' or x == 'c' and y == 'a':
    z = 'b'
'''

for x, y in itertools.product('abc', repeat=2):
    print '\nTesting with x=%r and y=%r' % (x, y)
    setup = setup_template % (x, y)
    for stmt, name in zip([original_version, new_version], ['if', 'set']):
        print min(timeit.Timer(stmt, setup).repeat(7, 100000)),
        print '\t%s_version' % name

以下是时间结果:
Testing with x='a' and y='a'
0.0410830974579     original_version
0.00535297393799    new_version

Testing with x='a' and y='b'
0.0112571716309     original_version
0.0524711608887     new_version

Testing with x='a' and y='c'
0.0383319854736     original_version
0.048309803009      new_version

Testing with x='b' and y='a'
0.0175108909607     original_version
0.0508949756622     new_version

Testing with x='b' and y='b'
0.0386209487915     original_version
0.00529098510742    new_version

Testing with x='b' and y='c'
0.0259420871735     original_version
0.0472128391266     new_version

Testing with x='c' and y='a'
0.0423510074615     original_version
0.0481910705566     new_version

Testing with x='c' and y='b'
0.0295209884644     original_version
0.0478219985962     new_version

Testing with x='c' and y='c'
0.0383579730988     original_version
0.00530385971069    new_version

这些时间表明,原始版本的性能会根据各种输入值触发的if语句而有所不同。


2
你的测试似乎有偏见。所谓的“set_version”只有在受到额外的if语句保护时才会更快。 - ekhumoro
2
@ekhumoro,这就是问题规范要求的内容:“检查xy是否是不同的值,如果是,则将z设置为第三个字符”。检查值是否不同的最快(也是最直接)方法是“x!= y”。只有当它们不同时,我们才使用集合差异来确定第三个字符 :-) - Raymond Hettinger
2
我想表达的是,你的测试并没有证明set_version之所以表现更好是因为它基于集合;它之所以表现更好只是因为有保护性的if语句。 - ekhumoro
1
@ekhumoro 这是对测试结果的奇怪解读。代码确实做到了OP所要求的。时间显示了所有可能输入组的比较性能。你可以自行解释这些。使用“if x!= y:z,= choices - {x,y}”版本的时间与OP原始代码相比表现得相当不错。我不知道你的偏见从何而来——时间就是时间,据我所知,这仍然是发布的最佳答案之一。它既干净又快速。 - Raymond Hettinger
2
Sven的“set-version”添加了几个优化,但是“if-version”没有进行相同的操作。在“if-version”中添加一个if x!= y保护可能会使其比迄今为止提供的所有其他解决方案更一致且性能更好(尽管显然不如可读性和简洁)。您的“set_version”是一个非常好的解决方案-它只是测试结果看起来不太好;-) - ekhumoro
@所有人,我的代码中已经有一个 if x != y,我认为我应该在问题中包含它。 - Bunny Rabbit

18
z = (set('abc') - set(x + y)).pop()

以下是展示它有效的所有场景:

>>> (set('abc') - set('ab')).pop()   # x is a/b and y is b/a
'c'
>>> (set('abc') - set('bc')).pop()   # x is b/c and y is c/b
'a'
>>> (set('abc') - set('ac')).pop()   # x is a/c and y is c/a
'b'

15
如果问题中的三个项不是"a""b""c",而是123,您也可以使用二进制异或(XOR)操作:
z = x ^ y

更一般地说,如果你想将z设置为这三个数字abc中剩余的一个,给定这个集合中的两个数字xy,你可以使用:

z = x ^ y ^ a ^ b ^ c
当然,如果数字是固定的,你可以预先计算 a ^ b ^ c
这种方法也适用于原始字母。
z = chr(ord(x) ^ ord(y) ^ 96)

例子:

>>> chr(ord("a") ^ ord("c") ^ 96)
'b'

不要期望任何人立即理解这段代码的含义 :)


+1 这个解决方案看起来既不错又优雅;如果你将它变成自己的函数并给魔数96取一个名字,那么逻辑就非常容易跟进和维护(xor_of_a_b_c = 96 # ord('a') ^ ord('b') ^ ord('c') == 96)。然而,从原始速度上来看,这比长串的 if/elif 块慢了约33%;但比 set 方法快了500%。 - dr jimbob
@sven 感谢您向我介绍了异或运算符,您的解决方案简洁优雅,我相信这个例子会让它深深印在我的脑海中,再次感谢 :) - Bunny Rabbit

13

我认为Sven Marnach和F.J的解决方案很美妙,但在我的小测试中并不更快。这是Raymond使用预先计算的set进行优化后的版本:

$ python -m timeit -s "choices = set('abc')" \
                   -s "x = 'c'" \
                   -s "y = 'a'" \
                      "z, = choices - set(x + y)"
1000000 loops, best of 3: 0.689 usec per loop

这是原始解决方案:

$ python -m timeit -s "x = 'c'" \
                   -s "y = 'a'" \
                      "if x == 'a' and y == 'b' or x == 'b' and y == 'a':" \
                      "    z = 'c'" \
                      "elif x == 'b' and y == 'c' or x == 'c' and y == 'b':" \
                      "    z = 'a'" \
                      "elif x == 'a' and y == 'c' or x == 'c' and y == 'a':" \
                      "    z = 'b'"
10000000 loops, best of 3: 0.310 usec per loop

请注意,这是if语句的最糟糕的可能输入,因为必须尝试所有六个比较。测试所有xy的值得出:

x = 'a', y = 'b': 0.084 usec per loop
x = 'a', y = 'c': 0.254 usec per loop
x = 'b', y = 'a': 0.133 usec per loop
x = 'b', y = 'c': 0.186 usec per loop
x = 'c', y = 'a': 0.310 usec per loop
x = 'c', y = 'b': 0.204 usec per loop

基于 set 的方案在不同的输入下表现相同,但是它的速度始终比 if 语句慢2到8倍。原因是 if 语句运行的代码更简单:仅涉及相等性测试而非哈希。

我认为这两种类型的解决方案都很有价值:知道创建像集合这样"复杂"的数据结构会付出一些性能代价是很重要的——虽然它们在可读性和开发速度方面给您带来了很多好处。当代码需要改变时,复杂的数据类型也更好用:使用基于集合的方案可以轻松地扩展到四个、五个等变量,而 if 语句很快就会演变成一个难以维护的噩梦。


1
@martinGeisler,非常感谢您的回复。我之前不知道在Python中可以像这样计时。我有一种直觉,Chessmasters的解决方案会很好地发挥作用且效率高。我会像你测试其他答案一样进行测试,并让您知道结果。 - Bunny Rabbit
1
基于集合的解决方案优化了简洁性和可读性(以及优雅性)。但是也提到了效率,因此我进行了调查,评估所提出的解决方案的性能。 - Martin Geisler
1
@MartinGeisler:是的,当我注意到这一点时,我删除了我的评论。而且我通常至少会发现知道什么更快更有趣。 - Sven Marnach
1
@BunnyRabbit:timeit模块非常适合进行微基准测试。当然,你首先应该对整个程序进行分析,以确定瓶颈在哪里,但是一旦它们被识别出来,那么timeit就可以成为快速尝试不同实现的好方法。 - Martin Geisler
1
+1 -- 基准测试证明简单的比较序列是逻辑和快速的。 - Jeff Ferland
显示剩余2条评论

8
z = 'a'*('a' not in x+y) or 'b'*('b' not in x+y) or 'c'

或者更少hackish并使用条件赋值

z = 'a' if ('a' not in x+y) else 'b' if ('b' not in x+y) else 'c'

但是可能使用字典解决方案更快...你需要计时它。


8

尝试使用字典这个选项:

z = {'ab':'c', 'ba':'c', 'bc':'a', 'cb':'a', 'ac':'b', 'ca':'b'}[x+y]

当然,如果地图中没有“x+y”键,则会产生一个KeyError错误,您需要处理它。
如果字典被预先计算并存储以供将来使用,则访问速度将更快,因为每次评估时不需要创建新的数据结构,只需要进行字符串连接和字典查找:
lookup_table = {'ab':'c', 'ba':'c', 'bc':'a', 'cb':'a', 'ac':'b', 'ca':'b'}
z = lookup_table[x+y]

2
只是为了好玩,这里提供另一种字典选项:{1: 'c', 2: 'b', 3: 'a'}[ord(x)+ord(y)-ord('a')*2],然而额外的复杂性可能不值得节省的空间。 - Andrew Clark
2
@F.J: z = {1: 'a', 2: 'b', 3: 'c'}[2*('a' in x+y)+('b' in x+y)] 这真有趣... - ChessMaster
它比原始代码OP快吗?如果是,为什么?如何计算哈希值可以比简单比较更快? - max
@max 是单个哈希计算,而不是大量比较和条件表达式。 - Óscar López
太酷了.. 没想到哈希函数的速度这么快! - max

2
我认为它应该看起来像这样:

z = (set(("a", "b", "c")) - set((x, y))).pop() if x != y else None

12
“len(set((x, y))) == 2” 是我见过的最难读懂的写法,其实就是想表达 “x != y”。 :) - Sven Marnach
是的,Sven))) 谢谢您的评论。当我开始编写这个脚本时,它有另一个基本的想法)) 最终我忘记了编辑它。 - selfnamed

1

使用列表推导式,假设您的代码中有三种情况之一成立:

l = ['a', 'b', 'c']
z = [n for n in l if n not in [x,y]].pop()

或者,就像接受的答案一样,利用元组来解包它,

z, = [n for n in l if n not in [x,y]]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接