Python中替代switch语句的方法是什么?

1717

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

在其他语言中,我会使用switchcase语句,但Python似乎没有switch语句。在这种情况下,有哪些推荐的Python解决方案?


77
相关PEP,由Guido本人撰写:PEP 3103 - chb
28
在那个PEP中,Guido没有提到if/elif链也是一个经典的错误来源。这种结构非常脆弱。 - itsbruce
15
所有这里提供的解决方案都没有检测重复案例值。作为一种快速失败原则,这可能比性能或fallthrough功能更重要的遗漏。 - Bob Stein
6
“switch”语句其实比基于输入索引值返回不同固定值的语句更加“通用”。它允许执行不同的代码块,甚至不需要返回值。我想知道这里的某些答案是否是“switch”语句的良好替代品,还是只适用于返回值而没有执行一般代码块的情况。 - sancho.s ReinstateMonicaCellio
3
类似于 Ruby 的 case...when...(或 Scala 的 match、Haskell 的 case、Perl 的 given/when)这样的语法满足了常见用例并提供了强大的抽象。if...elif... 是一个较差的替代品。 - itsbruce
显示剩余2条评论
44个回答

2170
Python 3.10(2021年)引入了match-case语句,为Python提供了一种一流的实现方式,用于实现"switch"。例如:
def f(x):
    match x:
        case 'a':
            return 1
        case 'b':
            return 2
        case _:
            return 0   # 0 is the default case if x is not found
match-case语句比这个简单的例子要强大得多。
如果您需要支持Python ≤ 3.9,请使用字典代替:
def f(x):
    return {
        'a': 1,
        'b': 2,
    }[x]

137
如果找不到 x,会发生什么? - Nick
65
可以使用Python中的defaultdict。 - Eli Bendersky
456
如有性能问题,建议将字典放在函数外部,这样就不会在每次函数调用时重新构建字典。 - Claudiu
76
在这种情况下,使用get方法可能比使用collections.defaultdict更常见。 - Mike Graham
44
@Nick,如果需要一个默认值,当遇到异常时,请使用}.get(x, default)语句。请注意:这比在switch语句中没有指定默认值要好得多! - Mike Graham
显示剩余21条评论

1584
如果您想使用默认值,可以使用字典get(key[, default])函数:
def f(x):
    return {
        'a': 1,
        'b': 2
    }.get(x, 9)    # 9 will be returned default if x is not found

13
如果'a'和'b'匹配为1,'c'和'd'匹配为2,会发生什么? - John Mee
14
@JM:显然,字典查找不支持下落。您可以进行双重字典查找。即“a”和“b”指向answer1,“c”和“d”指向answer2,它们包含在第二个字典中。 - Nick
3
最好传递一个默认值。 - HaTiMSuM
1
这种方法存在问题,首先每次调用f函数都会重新创建字典;其次,如果你有更复杂的值,可能会出现异常。例如,如果x是一个元组,并且我们想要像这样做:x = ('a') def f(x): return { 'a': x[0], 'b': x[1] }.get(x[0], 9),那么就会引发IndexError异常。 - Idan Haim Shalom
4
@Idan:这个问题是要复制开关。如果我试图输入奇数值,我肯定也能破坏这段代码。是的,它会重新创建,但修复它很简单。 - Nick
在 Python 3.10 之前,这应该是最受赞同的答案。 - FLAK-ZOSO

487

我一直喜欢用这种方式做

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

从这里开始


7
他要求固定的值。当需要查找时,为什么要生成一个计算某些东西的函数?不过这对其他问题是一个有趣的解决方案。 - Nick
37
在这种情况下使用lambda可能不是一个好主意,因为lambda实际上在构建字典时被调用了每次。 - Asher
16
遗憾的是这已经是人们能得到的最接近的答案了。像目前最高票的答案那样使用.get()方法的方式需要急切地评估所有可能性才能进行分派,因此不仅效率极低(不仅仅是非常低),而且也不能有副作用;该答案解决了这个问题,但更加冗长。我只会使用if/elif/else,即使这些也需要编写与'case'一样长的代码。 - ninjagecko
14
这样做会导致所有函数/lambda在任何情况下每次都被评估,即使它只返回其中一个结果,你觉得这样可以吗? - slf
27
@slf 不是的,当控制流程达到那段代码时,它会通过使用3个lambda函数来建立3个函数,然后建立一个包含这3个函数作为值的字典,但它们最初仍未被调用(在那种情况下,“_evaluate_”略有歧义)。然后通过[value]对字典进行索引,它将仅返回3个函数中的一个(假设value是其中3个键之一)。此时还没有调用该函数。然后(x)使用x作为参数调用刚刚返回的函数(结果存储在result中)。其他两个函数不会被调用。 - blubberdiblub
显示剩余10条评论

461

除了字典方法外(我真的很喜欢,顺便说一句),您还可以使用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

当然,这并不完全等同于switch/case - 你不能像省略break语句那样轻松地实现贯穿效果,但你可以进行更复杂的测试。尽管在功能上它更接近一系列嵌套的if语句,但其格式比后者更好。


81
我真的很喜欢这个,它使用标准语言结构,如果没有找到匹配的情况,也不会抛出 KeyError。 - martyglaubitz
9
我考虑过使用字典的方法,但标准方法更易读。get - Martin Thoma
2
@someuser 但是它们可以“重叠”这一事实是一个特性。您只需确保顺序是匹配应发生的优先级。至于重复的x:在之前执行x = the.other.thing即可。通常,您会有一个if,多个elif和一个else,因为这更容易理解。 - Matthew Schinckel
8
好的,"不使用elif语句时的Fall-through"有点令人困惑。那这样怎么样?不要考虑"Fall-through",只将其视为两个if/elif/else语句? - Alois Mahdal
10
值得一提的是,在使用类似 x in 'bc' 的表达式时,需要注意 "" in "bc" 的结果为 True - Lohmar ASHAR
显示剩余13条评论

380

Python >= 3.10

哇,Python 3.10+ 现在有一个match/case语法,类似于switch/case等等!

PEP 634 -- 结构化模式匹配

match/case的选择性特点

1 - 匹配值:

与其他语言中的简单switch/case类似,可以匹配值:

match something:
    case 1 | 2 | 3:
        # Match 1-3.
    case _:
        # Anything else.
        # 
        # If `case _:` is omitted, an error will be thrown
        # if `something` doesn't match any of the patterns.

2-匹配结构模式:

match something:
    case str() | bytes():  
        # Match a string like object.
    case [str(), int()]:
        # Match a `str` and an `int` sequence 
        # (A sequence can be a `list` or a `tuple` but not a `set` or an iterator). 
    case [_, _]:
        # Match a sequence of 2 variables.
        # To prevent a common mistake, sequence patterns don’t match strings.
    case {"bandwidth": 100, "latency": 300}:
        # Match this dict. Extra keys are ignored.

3 - 捕获变量

解析一个对象,并将其保存为变量:

match something:
    case [name, count]
        # Match a sequence of any two objects and parse them into the two variables.
    case [x, y, *rest]:
        # Match a sequence of two or more objects, 
        # binding object #3 and on into the rest variable.
    case bytes() | str() as text:
        # Match any string like object and save it to the text variable.

捕获变量在解析数据(如JSON或HTML)时非常有用,因为数据可能具有多种不同的模式。
捕获变量是一项功能。但这也意味着您只能使用点常量(例如:COLOR.RED)。否则,该常量将被视为捕获变量并被覆盖。 更多示例用法
match something:
    case 0 | 1 | 2:
        # Matches 0, 1 or 2 (value).
        print("Small number")
    case [] | [_]:
        # Matches an empty or single value sequence (structure).
        # Matches lists and tuples but not sets.
        print("A short sequence")
    case str() | bytes():
        # Something of `str` or `bytes` type (data type).
        print("Something string-like")
    case _:
        # Anything not matched by the above.
        print("Something else")

Python <= 3.9

我最喜欢的 Python switch/case 的方法是:

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'))

37
我认为这个答案比已被接受的答案更加健壮。 - cerd
3
C 要求在所有情况下返回值类型相同,但 Python 则没有这个限制。如果某人需要使用这种灵活性,我希望强调一下 Python 的这种特点。 - ChaimG
4
个人认为使用{}.get(,)可读性较好。如果想让初学Python的人更易懂,可以使用default=-1; result=choices.get(key, default)来增加可读性。 - ChaimG
9
与 C++ 中的 1 行代码比较:result = key=='a'?1:key==b?2:-1 - Jasen
5
有人可能会争论说,你也可以用一行Python代码完成:result = 1 if key == 'a' else (2 if key == 'b' else 'default')。但是这个一行式的代码可读性如何呢? - ChaimG
显示剩余8条评论

118
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.

70
不安全威胁。如果同时按下多个开关,所有开关都会采用最后一个开关的值。 - francescortiz
58
@francescortiz可能意味着线程安全,但它也不是威胁安全的。它会威胁到变量的值! - Zizouz212
7
可以通过使用线程本地存储来解决线程安全问题,或者完全避免该问题,方法是返回一个实例并在案例比较中使用该实例。 - blubberdiblub
6
那么,使用标准的“if”语句不是更有效吗? - wizzwizz4
10
如果在多个函数中使用,这也是不安全的。以给出的示例为例,如果case(2)块调用另一个使用switch()的函数,则在执行case(2,3,5,7)等查找下一个要执行的case时,它将使用由其他函数设置的switch值而不是当前switch语句设置的值。 - user9876
显示剩余7条评论

67

我最喜欢的一个是一个非常好的代码片段。它最接近实际的switch case语句,特别是在功能方面。

class switch(object):
    def __init__(self, value):
        self.value = value
        self.fall = False

    def __iter__(self):
        """Return the match method once, then stop"""
        yield self.match
        raise StopIteration
    
    def match(self, *args):
        """Indicate whether or not to enter a case suite"""
        if self.fall or not args:
            return True
        elif self.value in args: # changed for v1.5, see below
            self.fall = True
            return True
        else:
            return False

这里有一个示例:

# 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!"

一些评论指出,使用with foo as case而不是for case in foo的上下文管理器解决方案可能更加简洁,对于大型开关语句,线性而不是二次行为可能是一个不错的选择。此答案中for循环的价值部分在于具有break和fallthrough的能力,如果我们愿意稍微调整关键字的选择,我们也可以在上下文管理器中实现这一点:

class Switch:
    def __init__(self, value):
        self.value = value
        self._entered = False
        self._broken = False
        self._prev = None

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        return False # Allows a traceback to occur

    def __call__(self, *values):
        if self._broken:
            return False
        
        if not self._entered:
            if values and self.value not in values:
                return False
            self._entered, self._prev = True, values
            return True
        
        if self._prev is None:
            self._prev = values
            return True
        
        if self._prev != values:
            self._broken = True
            return False
        
        if self._prev == values:
            self._prev = None
            return False
    
    @property
    def default(self):
        return self()

这里有一个例子:

# Prints 'bar' then 'baz'.
with Switch(2) as case:
    while case(0):
        print('foo')
    while case(1, 2, 3):
        print('bar')
    while case(4, 5):
        print('baz')
        break
    while case.default:
        print('default')
        break

3
我会用 with switch() as case 替换 for case in switch(),因为它只需要运行一次,更有意义。 - Ski
5
注意,“with”语句不支持“break”,因此无法使用“fallthrough”选项。 - Jonas Schäfer
5
很抱歉我没有花更多的精力来确定这个问题,之前的一个类似回答不是线程安全的。这个呢? - David Winiecki
1
@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

66
class Switch:
    def __init__(self, value):
        self.value = value

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        return False # Allows a traceback to occur

    def __call__(self, *values):
        return self.value in values


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

13
使用上下文管理器是一个好的创意解决方案。我建议加入一些解释,并可能添加一些关于上下文管理器的信息链接,以便为这篇文章增加一些背景。 - Will
2
我不太喜欢if/elif链,但这是我见过的使用Python现有语法最具创意和实用性的解决方案。 - itsbruce
2
这真的很好。建议改进是在Switch类中添加一个(公共)value属性,以便您可以在语句中引用case.value - Peter
这个答案提供了最接近 switch 的功能,同时又相当简单。使用 dict 的问题在于你只能检索数据,而无法运行函数/方法。 - moshevi

52

我从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)


4
我觉得通过这种方式调用方法并没有比直接调用方法更有益处: SMTP().do_HELO('foo.bar.com') 虽然在lookupMethod中可能会有公共代码,但由于子类也可以覆盖它,所以我认为这种间接调用并没有什么优势。 - Mr Shark
1
你事先不知道要调用哪个方法,也就是说“HELO”来自一个变量。我已经在原帖中添加了使用示例。 - user6205
我可以简单地建议:eval('SMTP().do_' + command)('foo.bar.com') - jforberg
8
真的要用eval吗?相比每次调用实例化一个方法,只要该方法没有内部状态,我们完全可以实例化一次并在所有调用中使用。 - Mahesh
1
在我看来,这里的关键是使用getattr进行调度以指定要运行的函数。如果方法在一个模块中,你可以使用getattr(locals(), func_name)来获取它。'do_'部分对于安全和错误处理很好,因此只能调用具有该前缀的函数。SMTP本身调用lookupMethod。理想情况下,外部不知道任何这些内容。SMTP().lookupMethod(name)(data)并没有太多意义,因为命令和数据在一个字符串中,SMTP解析它更有意义。最后,SMTP可能有其他共享状态,这使得它成为一个类的合理选择。 - ShawnFumo
显示剩余2条评论

31

运行函数的解决方案:

result = {
    'case1':     foo1, 
    'case2':     foo2,
    'case3':     foo3,
}.get(option)(parameters_optional)

其中foo1()、foo2()和foo3()是函数。

示例1(带参数):

option = number['type']
result = {
    'number':     value_of_int,  # result = value_of_int(number['value'])
    'text':       value_of_text, # result = value_of_text(number['value'])
    'binary':     value_of_bin,  # result = value_of_bin(number['value'])
}.get(option)(value['value'])

示例 2(无参数):

option = number['type']
result = {
    'number':     func_for_number, # result = func_for_number()
    'text':       func_for_text,   # result = func_for_text()
    'binary':     func_for_bin,    # result = func_for_bin()
}.get(option)()

示例 4(仅值):

option = number['type']
result = {
    'number':    lambda: 10,       # result = 10
    'text':      lambda: 'ten',    # result = 'ten'
    'binary':    lambda: 0b101111, # result = 47
}.get(option)()

2
是的,例如如果您的变量option=="case2",那么您的结果=foo2()。 - Alejandro Quintanar
等等,诸如此类。 - Alejandro Quintanar
是的,我理解这个目的。但我的担忧是,如果你只想要 foo2(),那么 foo1()foo3()default() 函数也会运行,这意味着事情可能需要很长时间。 - Brian Underwood
你是完全正确的,这个简单版本在目录创建时运行所有函数,你可以通过将“option”作为foo()函数参数来解决它,这里有一个可工作的示例:https://gist.github.com/anonymous/a5ed3953e28440b4368c338b1e19d294/ - Alejandro Quintanar
2
使用get(option)()即可解决问题。请省略字典内的括号。 - timgeb
1
优秀的使用()是一个很好的解决方案,我创建了一个gist来测试它 https://gist.github.com/aquintanar/01e9920d8341c5c6252d507669758fe5 - Alejandro Quintanar

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