题 Python中switch语句的替换?


我想在Python中编写一个函数,它根据输入索引的值返回不同的固定值。

在其他语言中我会使用 switch 要么 case 声明,但Python似乎没有 switch 声明。在这种情况下,推荐的Python解决方案是什么?


1445
2017-09-13 00:36


起源


相关的PEP,由Guido自己撰写: PEP 3103 - chb
@chb在那个PEP中,Guido没有提到if / elif链也是一个经典的错误来源。这是一个非常脆弱的结构。 - itsbruce
这里缺少所有解决方案就是检测到 重复的案例值。作为一种快速失败的原则,这可能是比性能或突破性功能更重要的损失。 - Bob Stein
所有提供的解决方案都非常难看......开关盒在阅读时非常干净。无法理解为什么它没有在Python中实现。 - jmcollin92
switch 实际上比基于输入索引的值返回不同固定值的东西更“通用”。它允许执行不同的代码片段。它实际上甚至不需要返回值。我想知道这里的一些答案是不是一般的替代品 switch 声明,或仅适用于返回值且不可能执行一般代码的情况。 - sancho.s


答案:


你可以使用字典:

def f(x):
    return {
        'a': 1,
        'b': 2,
    }[x]

1128
2017-09-13 00:38



如果找不到x会怎么样? - Nick
@nick:你可以使用defaultdict - Eli Bendersky
这不是真正的开关/案例......请参阅下面的回复 - daniel
如果性能是一个问题,我建议将dict放在函数之外,因此它不会在每个函数调用上重新构建dict - Claudiu
@EliBendersky,使用 get 方法可能比使用a更正常 collections.defaultdict 在这种情况下。 - Mike Graham


如果你想要默认值,你可以使用字典 get(key[, default]) 方法:

def f(x):
    return {
        'a': 1,
        'b': 2
    }.get(x, 9)    # 9 is default if x not found

1110
2017-09-19 15:45



如果'a'和'b'匹配1,'c'和'd'匹配2怎么办? - John Mee
@JM:嗯,显然字典查找不支持堕落。你可以做双字典查找。即'a'和'b'指向answer1,'c'和'd'指向answer2,它们包含在第二个字典中。 - Nick
最好传递一个默认值 - HaTiMSuM
这种方法存在一些问题,首先你每次调用f你将再次创建dict,如果你有更复杂的值,你可以得到例外。如果x是一个元组,我们想要做这样的事情x =('a')def f(x):return {'a':x [0],'b':x [1]} .get( x [0],9)这将引发IndexError - Idan Haim Shalom
@Idan:问题是复制开关。如果我尝试输入奇数值,我相信我也可以破坏这段代码。是的,它会重新创建,但修复起来很简单。 - Nick


我总是喜欢这样做

result = {
  'a': lambda x: x * 5,
  'b': lambda x: x + 7,
  'c': lambda x: x - 2
}[value](x)

从这里


294
2017-09-13 00:41



很棒的方法,结合get()来处理默认值也是我最好的选择 - drAlberT
在这种情况下使用lambda可能不是一个好主意,因为每次构建字典时都会调用lambda。 - htmlfarmer
可悲的是,这是最接近人们的。使用的方法 .get() (就像目前最高的答案一样)需要在派遣之前急切地评估所有可能性,因此不仅(不仅非常)非常低效,而且不会产生副作用;这个答案解决了这个问题,但更冗长。我只想使用if / elif / else,甚至那些和'case'一样长。 - ninjagecko
在所有情况下,每次都不会评估所有函数/ lambdas,即使它只返回其中一个结果? - slf
@slf不,当控制流到达那段代码时,它将构建3个函数(通过使用3个lambdas)然后构建一个字典,将这3个函数作为值,但它们仍未被调用(评估 起初在这种情况下略微含糊不清。然后字典被索引通过 [value],它将只返回3个函数中的一个(假设 value 是3个键之一)。此时尚未调用该函数。然后 (x) 使用调用刚刚返回的函数 x 作为论点(结果如下) result)。其他2个函数将不会被调用。 - blubberdiblub


除了字典方法(我非常喜欢BTW)之外,您还可以使用if-elif-else来获取switch / case / default功能:

if x == 'a':
    # Do the thing
elif x == 'b':
    # Do the other thing
if x in 'bc':
    # Fall-through by not using elif, but now the default case includes case 'a'!
elif x in 'xyz':
    # Do yet another thing
else:
    # Do the default

这当然与开关/箱子不一样 - 你不能像离开休息那样容易穿透;声明,但你可以进行更复杂的测试。它的格式比一系列嵌套ifs更好,即使功能上它更接近它。


248
2017-09-13 01:10



我真的更喜欢这个,它使用一个标准的语言结构,如果没有找到匹配的情况,就不会抛出KeyError - martyglaubitz
我想到了字典/ get 方式,但标准方式更具可读性。 - Martin Thoma
不完全像switch语句那样操作但非常接近。在我看来最接近的事情 - Arijoon
这是最干净的解决方案。每个都是最常见的 switch 有一个 break 我看到的最常见的使用方法是匹配多个元素,如 if x in 'bc':。 - bmacnaughton
不错,“不使用elif的堕落”有点令人困惑。那怎么样:忘记“堕落”并接受它作为两个 if/elif/else的? - Alois Mahdal


我最喜欢的用于开关/案例的Python配方是:

choices = {'a': 1, 'b': 2}
result = choices.get(key, 'default')

简单场景简短而简单。

比较11行以上的C代码:

// C Language version of a simple 'switch/case'.
switch( key ) 
{
    case 'a' :
        result = 1;
        break;
    case 'b' :
        result = 2;
        break;
    default :
        result = -1;
}

您甚至可以使用元组分配多个变量:

choices = {'a': (1, 2, 3), 'b': (4, 5, 6)}
(result1, result2, result3) = choices.get(key, ('default1', 'default2', 'default3'))

122
2018-06-17 02:25



我发现这是一个比接受的更强大的答案。 - cerd
@some user:C要求返回值对于所有情况都是相同的类型。 Python没有。我希望强调Python的这种灵活性,以防有人出现这种情况需要保证这种情况。 - ChaimG
@some用户:我个人认为{} .get(,)是可读的。为了获得Python初学者的额外可读性,您可能需要使用 default = -1; result = choices.get(key, default)。 - ChaimG
与1行c ++进行比较 result=key=='a'?1:key==b?2:-1 - Jasen
@Jasen可以说你可以在一行Python中做到这一点: result = 1 if key == 'a' else (2 if key == 'b' else 'default')。但是单行内容是否可读? - ChaimG


class switch(object):
    value = None
    def __new__(class_, value):
        class_.value = value
        return True

def case(*args):
    return any((arg == switch.value for arg in args))

用法:

while switch(n):
    if case(0):
        print "You typed zero."
        break
    if case(1, 4, 9):
        print "n is a perfect square."
        break
    if case(2):
        print "n is an even number."
    if case(2, 3, 5, 7):
        print "n is a prime number."
        break
    if case(6, 8):
        print "n is an even number."
        break
    print "Only single-digit numbers are allowed."
    break

测试:

n = 2
#Result:
#n is an even number.
#n is a prime number.
n = 11
#Result:
#Only single-digit numbers are allowed.

86
2017-07-07 06:09



这不是威胁安全的。如果同时击中多个开关,则所有开关都采用最后一个开关的值。 - francescortiz
虽然@francescortiz可能意味着线程安全,但它也不是威胁安全的。它威胁着变量的价值! - Zizouz212
线程安全问题可能会通过使用来解决 线程本地存储。或者可以通过返回实例并使用该实例进行案例比较来完全避免它。 - blubberdiblub
@blubberdiblub然而,使用标准并不是更有效率 if 声明? - wizzwizz4
如果在多个功能中使用,这也是不安全的。在给出的例子中,如果 case(2) block调用另一个使用switch()的函数,然后执行 case(2, 3, 5, 7) 等等要查找下一个要执行的情况,它将使用由其他函数设置的开关值,而不是当前switch语句设置的值。 - user9876


我从Twisted Python代码中学到了一种模式。

class SMTP:
    def lookupMethod(self, command):
        return getattr(self, 'do_' + command.upper(), None)
    def do_HELO(self, rest):
        return 'Howdy ' + rest
    def do_QUIT(self, rest):
        return 'Bye'

SMTP().lookupMethod('HELO')('foo.bar.com') # => 'Howdy foo.bar.com'
SMTP().lookupMethod('QUIT')('') # => 'Bye'

您可以在需要分派令牌并执行扩展代码时随时使用它。在你可以拥有的状态机中 state_ 方法和派遣 self.state。通过继承基类并定义自己的开关,可以干净地扩展此开关 do_ 方法。很多时候你甚至都没有 do_ 基类中的方法。

编辑:如何使用

如果是SMTP,您将收到 HELO 从电线。相关代码(来自 twisted/mail/smtp.py,为我们的案例修改)看起来像这样

class SMTP:
    # ...

    def do_UNKNOWN(self, rest):
        raise NotImplementedError, 'received unknown command'

    def state_COMMAND(self, line):
        line = line.strip()
        parts = line.split(None, 1)
        if parts:
            method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
            if len(parts) == 2:
                return method(parts[1])
            else:
                return method('')
        else:
            raise SyntaxError, 'bad syntax'

SMTP().state_COMMAND('   HELO   foo.bar.com  ') # => Howdy foo.bar.com

你会收到的 ' HELO foo.bar.com ' (或者你可能会得到 'QUIT' 要么 'RCPT TO: foo')。这被标记为 parts 如 ['HELO', 'foo.bar.com']。实际的方法查找名称取自 parts[0]

(也称为原始方法 state_COMMAND,因为它使用相同的模式来实现状态机,即 getattr(self, 'state_' + self.mode)


41
2017-09-13 01:26



我没有看到这种模式直接调用方法的好处:SMTP()。do_HELO('foo.bar.com')好的,lookupMethod中可以有公共代码,但是因为它也可以被覆盖我看不到你从间接获得的东西。 - Mr Shark
你不知道提前调用什么方法,也就是说'HELO'来自一个变量。我已经在原帖中添加了用法示例
我可以简单地建议:eval('SMTP()。do_'+命令)('foo.bar.com') - jforberg
另外,为什么要为每个方法调用实例化一个新的SMTP对象?这就是全球功能的用途。 - jforberg
EVAL?当真?而不是每次调用实例化一个方法,我们可以很好地实例化一次并在所有调用中使用它,前提是它没有内部状态。 - Mahesh


我最喜欢的是非常好的 食谱。你真的很喜欢它。它是我见过的最接近实际的switch case语句,特别是在功能方面。

这是一个例子:

# The following example is pretty much the exact use-case of a dictionary,
# but is included for its simplicity. Note that you can include statements
# in each suite.
v = 'ten'
for case in switch(v):
    if case('one'):
        print 1
        break
    if case('two'):
        print 2
        break
    if case('ten'):
        print 10
        break
    if case('eleven'):
        print 11
        break
    if case(): # default, could also just omit condition or 'if True'
        print "something else!"
        # No need to break here, it'll stop anyway

# break is used here to look as much like the real thing as possible, but
# elif is generally just as good and more concise.

# Empty suites are considered syntax errors, so intentional fall-throughs
# should contain 'pass'
c = 'z'
for case in switch(c):
    if case('a'): pass # only necessary if the rest of the suite is empty
    if case('b'): pass
    # ...
    if case('y'): pass
    if case('z'):
        print "c is lowercase!"
        break
    if case('A'): pass
    # ...
    if case('Z'):
        print "c is uppercase!"
        break
    if case(): # default
        print "I dunno what c was!"

# As suggested by Pierre Quentel, you can even expand upon the
# functionality of the classic 'case' statement by matching multiple
# cases in a single shot. This greatly benefits operations such as the
# uppercase/lowercase example above:
import string
c = 'A'
for case in switch(c):
    if case(*string.lowercase): # note the * for unpacking as arguments
        print "c is lowercase!"
        break
    if case(*string.uppercase):
        print "c is uppercase!"
        break
    if case('!', '?', '.'): # normal argument passing style also applies
        print "c is a sentence terminator!"
        break
    if case(): # default
        print "I dunno what c was!"

38
2017-07-07 06:12



我会替换 for case in switch() 同 with switch() as case更有意义,因为它只需要运行一次。 - Ski
@Skirmantas:请注意 with 不允许的 break 但是,所以即使是选项也会被取消。 - Jonas Wielicki
抱歉没有花更多精力来自己确定:上面的类似答案不是线程安全的。这是? - David Winiecki
@DavidWiniecki上面缺少的代码组件(可能是activestate的版权)似乎是线程安全的。 - Jasen
将是另一个版本的东西 if c in set(range(0,9)): print "digit" elif c in set(map(chr, range(ord('a'), ord('z')))): print "lowercase"? - mpag


class Switch:
    def __init__(self, value): self._val = value
    def __enter__(self): return self
    def __exit__(self, type, value, traceback): return False # Allows traceback to occur
    def __call__(self, *mconds): return self._val in mconds

from datetime import datetime
with Switch(datetime.today().weekday()) as case:
    if case(0):
        # Basic usage of switch
        print("I hate mondays so much.")
        # Note there is no break needed here
    elif case(1,2):
        # This switch also supports multiple conditions (in one line)
        print("When is the weekend going to be here?")
    elif case(3,4): print("The weekend is near.")
    else:
        # Default would occur here
        print("Let's go have fun!") # Didn't use case for example purposes

28
2018-05-03 09:05



使用上下文管理器是一个很好的创造性解我建议添加一些解释,并可能链接到关于上下文管理器的一些信息,以给这篇文章一些,好的,上下文;) - Will
我不喜欢if / elif很多,但这是我用Python现有语法看到的所有解决方案中最有创意和最实用的。 - itsbruce


假设你不想只返回一个值,但想要使用改变对象上某些东西的方法。使用此处说明的方法将是:

result = {
  'a': obj.increment(x),
  'b': obj.decrement(x)
}.get(value, obj.default(x))

这里发生的是python评估字典中的所有方法。 因此,即使您的值为'a',对象也会增加  减去x。

解:

func, args = {
  'a' : (obj.increment, (x,)),
  'b' : (obj.decrement, (x,)),
}.get(value, (obj.default, (x,)))

result = func(*args)

所以你得到一个包含函数及其参数的列表。这样,只返回函数指针和参数列表,  评估。 'result'然后计算返回的函数调用。


22
2017-09-30 08:31