Des remplacements pour l'instruction switch en Python?

1718

Je veux écrire une fonction en Python qui retourne différentes valeurs fixes basées sur la valeur d'un index d'entrée.

Dans d'autres langues, j'utiliserais une instruction switchor case, mais Python ne semble pas avoir de switchdéclaration. Quelles sont les solutions Python recommandées dans ce scénario?

Michael Schneider
la source
77
PEP connexe, rédigé par Guido lui-même: PEP 3103
chb
28
@chb Dans ce PEP, Guido ne mentionne pas que les chaînes if / elif sont également une source d'erreur classique. C'est une construction très fragile.
itsbruce
15
Il manque ici toutes les solutions est la détection des valeurs de cas en double . En tant que principe d'échec rapide, cela peut être une perte plus importante que les performances ou la fonctionnalité de remplacement.
Bob Stein
6
switchest en fait plus "polyvalent" que quelque chose renvoyant des valeurs fixes différentes en fonction de la valeur d'un index d'entrée. Il permet d'exécuter différents morceaux de code. Il n'a même pas besoin de renvoyer de valeur. Je me demande si certaines des réponses ici sont de bons remplacements pour une switchdéclaration générale , ou seulement pour le cas de renvoyer des valeurs sans possibilité d'exécuter des morceaux de code généraux.
sancho.s ReinstateMonicaCellio
3
@ MalikA.Rumi Construction fragile, tout comme une boucle while est une construction fragile si vous essayez de l'utiliser pour faire quoi pour ... dans ... fait. Allez-vous appeler des programmeurs faibles pour l'utilisation des boucles for? Les boucles sont tout ce dont elles ont réellement besoin. Mais pour les boucles, affichez une intention claire, enregistrez le passe-partout inutile et donnez la possibilité de créer de puissantes abstractions.
itsbruce

Réponses:

1486

Vous pouvez utiliser un dictionnaire:

def f(x):
    return {
        'a': 1,
        'b': 2,
    }[x]
Greg Hewgill
la source
100
Que se passe-t-il si x n'est pas trouvé?
Nick
46
@nick: vous pouvez utiliser defaultdict
Eli Bendersky
385
Je recommanderais de mettre le dict en dehors de la fonction si les performances sont un problème, afin qu'il ne reconstruise pas le dict à chaque appel de fonction
Claudiu
56
@EliBendersky, Utiliser la getméthode serait probablement plus normal que d'utiliser un collections.defaultdictdans ce cas.
Mike Graham
27
@Nick, Une exception est levée - faites à la }.get(x, default)place s'il doit y avoir une valeur par défaut. (Remarque: c'est beaucoup plus agréable que ce qui se passe si vous laissez la valeur par défaut sur une instruction switch!)
Mike Graham
1374

Si vous souhaitez des valeurs par défaut, vous pouvez utiliser la get(key[, default])méthode du dictionnaire :

def f(x):
    return {
        'a': 1,
        'b': 2
    }.get(x, 9)    # 9 is default if x not found
pseudo
la source
11
Que faire si «a» et «b» correspondent à 1, et «c» et «d» correspondent à 2?
John Mee
13
@JM: De toute évidence, les recherches de dictionnaire ne prennent pas en charge les raccourcis. Vous pouvez effectuer une double recherche dans le dictionnaire. C'est-à-dire 'a' et 'b' pointent vers la réponse1 et 'c' et 'd' pointent vers la réponse2, qui sont contenus dans un second dictionnaire.
Nick
3
il vaut mieux passer une valeur par défaut
HaTiMSuM
Il y a un problème avec cette approche, d'abord à chaque fois que vous appelez f, vous allez recréer le dict en second lieu si vous avez une valeur plus complexe, vous pouvez obtenir une exception ex. si x est un tuple et que nous voulons faire quelque chose comme ceci x = ('a') def f (x): return {'a': x [0], 'b': x [1]} .get ( x [0], 9) Cela soulèvera IndexError
Idan Haim Shalom
2
@Idan: La question était de répliquer le commutateur. Je suis sûr que je pourrais aussi casser ce code si j'essayais de mettre des valeurs impaires. Oui, il se recréera, mais il est simple à corriger.
Nick
394

J'ai toujours aimé le faire de cette façon

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

D'ici

Mark Biek
la source
16
une excellente méthode, combinée avec get () pour gérer les
valeurs
27
ce n'est peut-être pas une bonne idée d'utiliser lambda dans ce cas car lambda est en fait appelé à chaque fois que le dictionnaire est construit.
Asher
13
Malheureusement, ce sont les personnes les plus proches qui seront. Les méthodes qui utilisent .get()(comme les réponses les plus élevées actuelles) devront évaluer avec impatience toutes les possibilités avant la répartition, et donc non seulement sont (pas seulement très mais) extrêmement inefficaces et ne peuvent pas non plus avoir d'effets secondaires; cette réponse contourne ce problème, mais est plus détaillée. J'utiliserais simplement if / elif / else, et même ceux-ci prennent autant de temps à écrire que 'case'.
ninjagecko
13
cela n'évaluerait-il pas toutes les fonctions / lambdas à chaque fois dans tous les cas, même s'il ne renvoie qu'un des résultats?
slf
23
@slf Non, lorsque le flux de contrôle atteint ce morceau de code, il crée 3 fonctions (grâce à l'utilisation des 3 lambdas), puis crée un dictionnaire avec ces 3 fonctions comme valeurs, mais elles restent non appelées (l' évaluation est légèrement ambiguë dans ce contexte) dans un premier temps. Ensuite, le dictionnaire est indexé via [value], qui ne renverra qu'une seule des 3 fonctions (en supposant qu'il values'agit de l'une des 3 clés). La fonction n'a pas encore été appelée à ce stade. Appelle ensuite (x)la fonction qui vient d'être renvoyée avec xcomme argument (et le résultat passe à result). Les 2 autres fonctions ne seront pas appelées.
blubberdiblub
354

En plus des méthodes de dictionnaire (que j'aime beaucoup, BTW), vous pouvez également utiliser if- elif- elsepour obtenir la fonctionnalité 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

Bien sûr, ce n'est pas identique à switch / case - vous ne pouvez pas avoir une transition aussi facilement que de laisser la breakdéclaration, mais vous pouvez avoir un test plus compliqué. Son formatage est plus agréable qu'une série de ifs imbriqués , même si fonctionnellement c'est ce dont il est le plus proche.

Matthew Schinckel
la source
51
je préfère vraiment cela, il utilise une construction de langage standard et ne lance pas de KeyError si aucun cas correspondant n'est trouvé
martyglaubitz
7
J'ai pensé au dictionnaire / getméthode, mais la méthode standard est tout simplement plus lisible.
Martin Thoma
2
@someuser mais le fait qu'ils puissent se "chevaucher" est une fonctionnalité. Vous vous assurez simplement que l'ordre est la priorité dans laquelle les correspondances doivent avoir lieu. Quant aux x répétés: faites juste un x = the.other.thingavant. En règle générale, vous auriez un seul if, plusieurs elif et un seul autre, car c'est plus facile à comprendre.
Matthew Schinckel
7
Bien, le "Fall-through en n'utilisant pas elif" est un peu déroutant, cependant. Qu'en est-il de cela: oublier "tomber" et l'accepter comme deux if/elif/else?
Alois Mahdal
7
Il convient également de mentionner, lorsque vous utilisez des choses comme x in 'bc', gardez à l'esprit que "" in "bc"c'est True.
Lohmar ASHAR
185

Ma recette Python préférée pour commutateur / boîtier est:

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

Court et simple pour des scénarios simples.

Comparez à plus de 11 lignes de code C:

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

Vous pouvez même affecter plusieurs variables en utilisant des tuples:

choices = {'a': (1, 2, 3), 'b': (4, 5, 6)}
(result1, result2, result3) = choices.get(key, ('default1', 'default2', 'default3'))
ChaimG
la source
16
Je trouve que c'est une réponse plus solide que celle acceptée.
cerd
3
@some user: C requiert que la valeur de retour soit du même type pour tous les cas. Python ne fonctionne pas. Je voulais souligner cette flexibilité de Python au cas où quelqu'un aurait une situation qui justifierait une telle utilisation.
ChaimG
3
@some user: Personnellement, je trouve {} .get (,) lisible. Pour plus de lisibilité pour les débutants Python, vous voudrez peut-être utiliser default = -1; result = choices.get(key, default).
ChaimG
4
comparer avec 1 ligne de c ++result=key=='a'?1:key==b?2:-1
Jasen
4
@Jasen on peut dire que vous pouvez le faire en une ligne de Python ainsi: result = 1 if key == 'a' else (2 if key == 'b' else 'default'). mais le liner est-il lisible?
ChaimG
101
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))

Usage:

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

Tests:

n = 2
#Result:
#n is an even number.
#n is a prime number.
n = 11
#Result:
#Only single-digit numbers are allowed.
Adam
la source
64
Ce n'est pas sans danger. Si plusieurs commutateurs sont touchés en même temps, tous les commutateurs prennent la valeur du dernier commutateur.
francescortiz
48
Bien que @francescortiz signifie probablement thread-safe, il n'est pas non plus sûr contre les menaces. Il menace les valeurs des variables!
Zizouz212
7
Le problème de sécurité des threads pourrait probablement être résolu en utilisant le stockage local des threads . Ou cela pourrait être évité en retournant une instance et en utilisant cette instance pour les comparaisons de cas.
blubberdiblub
6
@blubberdiblub Mais n'est-il pas simplement plus efficace d'utiliser une ifinstruction standard ?
wizzwizz4
9
Ce n'est également pas sûr s'il est utilisé dans plusieurs fonctions. Dans l'exemple donné, si le case(2)bloc a appelé une autre fonction qui utilise switch (), lors case(2, 3, 5, 7)de l'exécution de etc pour rechercher le prochain cas à exécuter, il utilisera la valeur de commutateur définie par l'autre fonction et non celle définie par l'instruction switch actuelle .
user9876
52

Ma recette préférée est une très bonne recette . Vous l'aimerez vraiment. C'est le plus proche que j'ai vu des déclarations de cas de commutation réelles, en particulier dans les fonctionnalités.

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

Voici un exemple:

# 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!"
John Doe
la source
3
Je remplacerais for case in switch()par with switch() as case, a plus de sens, car il ne doit s'exécuter qu'une seule fois.
Ski
4
@Skirmantas: Notez que withcela ne le permet pas break, donc l'option de remplacement est supprimée.
Jonas Schäfer
5
Toutes mes excuses pour ne pas avoir fait plus d'efforts pour le déterminer moi-même: une réponse similaire ci-dessus n'est pas sûre pour les threads. Est-ce?
David Winiecki
1
@DavidWiniecki Les composants de code manquants dans ce qui précède (et éventuellement les droits d'auteur par activestate) semblent être thread-safe.
Jasen
une autre version serait-elle quelque chose comme ça if c in set(range(0,9)): print "digit" elif c in set(map(chr, range(ord('a'), ord('z')))): print "lowercase"?
mpag
51
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
Ian Bell
la source
9
L'utilisation de gestionnaires de contexte est une bonne solution créative. Je recommanderais d'ajouter un peu d'explication et peut-être un lien vers des informations sur les gestionnaires de contexte pour donner à ce message un peu de contexte;)
Will
2
Je n'aime pas beaucoup les chaînes if / elif mais c'est à la fois la plus créative et la plus pratique de toutes les solutions que j'ai vues en utilisant la syntaxe existante de Python.
itsbruce
2
C'est vraiment sympa. Une amélioration suggérée consiste à ajouter une valuepropriété (publique) à la classe Switch afin que vous puissiez référencer le case.valuedans l'instruction.
Peter
48

Il y a un modèle que j'ai appris du code 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'

Vous pouvez l'utiliser à tout moment pour envoyer un jeton et exécuter un morceau de code étendu. Dans une machine d'état, vous auriez des state_méthodes et vous enverriez self.state. Ce commutateur peut être proprement étendu en héritant de la classe de base et en définissant vos propres do_méthodes. Souvent, vous n'aurez même pasdo_ méthodes dans la classe de base.

Edit: comment est-ce exactement utilisé

En cas de SMTP, vous recevrez HELOdu fil. Le code pertinent (de twisted/mail/smtp.py, modifié pour notre cas) ressemble à ceci

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

Vous recevrez ' HELO foo.bar.com '(ou vous pourriez obtenir 'QUIT'ou 'RCPT TO: foo'). Ceci est symbolisé en partsas ['HELO', 'foo.bar.com']. Le nom de recherche de méthode réel est tiré departs[0] .

(La méthode d'origine est également appelée state_COMMAND, car elle utilise le même modèle pour implémenter une machine à états, c.-à-d. getattr(self, 'state_' + self.mode))


la source
4
Je ne vois pas l'avantage de ce modèle sur le simple fait d'appeler directement les méthodes: SMTP (). Do_HELO ('foo.bar.com') OK, il peut y avoir du code commun dans lookupMethod, mais puisque cela peut également être écrasé par la sous-classe, je ne vois pas ce que vous gagnez de l'indirection.
M. Shark
1
Vous ne sauriez pas quelle méthode appeler à l'avance, c'est-à-dire que 'HELO' vient d'une variable. J'ai ajouté un exemple d'utilisation au message d'origine
Puis-je suggérer simplement: eval ('SMTP (). Do_' + commande) ('foo.bar.com')
jforberg
8
eval? sérieusement? et au lieu d'instancier une méthode par appel, nous pouvons très bien instancier une fois et l'utiliser dans tous les appels à condition qu'elle n'ait pas d'état interne.
Mahesh
1
IMO la vraie clé ici est la répartition en utilisant getattr pour spécifier une fonction à exécuter. Si les méthodes étaient dans un module, vous pouvez faire getattr (locals (), func_name) pour l'obtenir. La partie 'do_' est bonne pour la sécurité / erreurs, donc seuls les funcs avec le préfixe peuvent être appelés. SMTP lui-même appelle lookupMethod. Idéalement, l'extérieur ne sait rien de tout cela. Cela n'a pas vraiment de sens de faire SMTP (). LookupMethod (name) (data). Étant donné que la commande et les données sont dans une seule chaîne et que SMTP les analyse, cela a plus de sens. Enfin, SMTP a probablement un autre état partagé qui justifie qu'il soit une classe.
ShawnFumo
27

Disons que vous ne voulez pas simplement renvoyer une valeur, mais que vous souhaitez utiliser des méthodes qui changent quelque chose sur un objet. Utiliser l'approche indiquée ici serait:

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

Ce qui se passe ici, c'est que python évalue toutes les méthodes du dictionnaire. Donc, même si votre valeur est «a», l'objet sera incrémenté et décrémenté de x.

Solution:

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

result = func(*args)

Vous obtenez donc une liste contenant une fonction et ses arguments. De cette façon, seuls le pointeur de fonction et la liste d'arguments sont retournés, non évalués. 'result' évalue ensuite l'appel de fonction retourné.

GeeF
la source
23

Je vais juste déposer mes deux cents ici. La raison pour laquelle il n'y a pas d'instruction case / switch en Python est parce que Python suit le principe de «Il n'y a qu'une seule bonne façon de faire quelque chose». Donc, évidemment, vous pouvez trouver différentes façons de recréer la fonctionnalité commutateur / boîtier, mais la façon Pythonique d'accomplir cela est la construction if / elif. c'est à dire

if something:
    return "first thing"
elif somethingelse:
    return "second thing"
elif yetanotherthing:
    return "third thing"
else:
    return "default thing"

Je pensais juste que PEP 8 méritait un signe de tête ici. L'une des belles choses à propos de Python est sa simplicité et son élégance. Cela découle en grande partie des principes énoncés dans le PEP 8, notamment: «Il n'y a qu'une seule bonne façon de faire quelque chose»

user2233949
la source
6
Alors pourquoi Python a-t-il des boucles for et des boucles while? Tout ce que vous pouvez faire avec une boucle for, vous pouvez l'implémenter avec une boucle while.
itsbruce
1
Vrai. Les commutateurs / cas sont trop souvent abusés par les programmeurs débutants. Ce qu'ils veulent vraiment, c'est le modèle de stratégie .
user228395
On dirait que Python souhaite que ce soit Clojure
TWR Cole
1
@TWRCole Je ne pense pas, Python le faisait en premier. Python existe depuis 1990 et Clojure depuis 2007.
Taylor
Il n'y a qu'une seule bonne façon de faire quelque chose. Python 2.7 ou Python 3? Lol.
TWR Cole
17

élargir l'idée «dict as switch». si vous souhaitez utiliser une valeur par défaut pour votre commutateur:

def f(x):
    try:
        return {
            'a': 1,
            'b': 2,
        }[x]
    except KeyError:
        return 'default'
Jeremy Cantrell
la source
14
Je pense qu'il est plus clair d'utiliser .get () sur le dict avec la valeur par défaut spécifiée. Je préfère laisser des exceptions pour des circonstances exceptionnelles, et il coupe trois lignes de code et un niveau d'indentation sans être obscur.
Chris B.
10
Il s'agit d' une circonstance exceptionnelle. Cela peut ou non être une circonstance rare selon l'utilité, mais c'est certainement une exception (se replier 'default') sur la règle (obtenir quelque chose de ce dict). De par leur conception, les programmes Python utilisent des exceptions à la baisse d'un chapeau. Cela étant dit, l'utilisation getpourrait potentiellement rendre le code un peu plus agréable.
Mike Graham
16

Si vous avez un bloc de cas compliqué, vous pouvez envisager d'utiliser une table de recherche de dictionnaire de fonctions ...

Si vous ne l'avez pas fait auparavant, c'est une bonne idée d'entrer dans votre débogueur et de voir exactement comment le dictionnaire recherche chaque fonction.

REMARQUE: n'utilisez pas "()" dans la recherche de cas / dictionnaire ou cela appellera chacune de vos fonctions lors de la création du bloc dictionnaire / cas. N'oubliez pas ceci car vous ne souhaitez appeler chaque fonction qu'une seule fois à l'aide d'une recherche de style de hachage.

def first_case():
    print "first"

def second_case():
    print "second"

def third_case():
    print "third"

mycase = {
'first': first_case, #do not use ()
'second': second_case, #do not use ()
'third': third_case #do not use ()
}
myfunc = mycase['first']
myfunc()
Asher
la source
J'aime ta solution. Mais que se passe-t-il si j'ai juste besoin de passer des variables ou des objets?
Tedo Vrbanec
Cela ne fonctionnera pas si la méthode attend des paramètres.
Kulasangar
16

Si vous recherchez une extra-instruction, comme "switch", j'ai construit un module python qui étend Python. Ça s'appelle ESPY tant que «Structure améliorée pour Python» et il est disponible pour Python 2.x et Python 3.x.

Par exemple, dans ce cas, une instruction switch peut être exécutée par le code suivant:

macro switch(arg1):
    while True:
        cont=False
        val=%arg1%
        socket case(arg2):
            if val==%arg2% or cont:
                cont=True
                socket
        socket else:
            socket
        break

qui peut être utilisé comme ceci:

a=3
switch(a):
    case(0):
        print("Zero")
    case(1):
        print("Smaller than 2"):
        break
    else:
        print ("greater than 1")

alors espy le traduit en Python comme:

a=3
while True:
    cont=False
    if a==0 or cont:
        cont=True
        print ("Zero")
    if a==1 or cont:
        cont=True
        print ("Smaller than 2")
        break
    print ("greater than 1")
    break
elp
la source
Très cool, mais quel est l'intérêt de while True:la partie supérieure du code Python généré? Cela frappera inévitablement breakle bas du code Python généré, il me semble donc que le while True:et breakpourraient être supprimés. De plus, ESPY est-il assez intelligent pour changer le nom de contsi l'utilisateur utilise ce même nom dans son propre code? Dans tous les cas, je veux utiliser Python vanille donc je ne vais pas l'utiliser, mais c'est quand même cool. +1 pour la fraîcheur pure.
ArtOfWarfare
@ArtOfWarfare La raison pour laquelle while True:et breaks est d'autoriser, mais pas d'exiger, une interruption .
Solomon Ucko
Ce module est-il toujours disponible?
Solomon Ucko
15

J'ai trouvé qu'une structure de commutateur commune:

switch ...parameter...
case p1: v1; break;
case p2: v2; break;
default: v3;

peut être exprimé en Python comme suit:

(lambda x: v1 if p1(x) else v2 if p2(x) else v3)

ou formaté de manière plus claire:

(lambda x:
     v1 if p1(x) else
     v2 if p2(x) else
     v3)

Au lieu d'être une instruction, la version python est une expression qui évalue une valeur.

Leo
la source
Aussi au lieu de ... paramètre ... et p1 (x) que diriez-vous parameteretp1==parameter
Bob Stein
@ BobStein-VisiBone Salut, voici un exemple qui fonctionne dans ma session python: f = lambda x: 'a' if x==0 else 'b' if x==1 else 'c'. Quand j'ai appelé plus tard f(2), j'ai reçu 'c'; f(1), 'b'; et f(0), 'a'. Quant à p1 (x), il désigne un prédicat; tant qu'il revient Trueou False, peu importe qu'il s'agisse d'un appel de fonction ou d'une expression, ça va.
leo
@ BobStein-VisiBone Oui, vous avez raison! Merci :) Pour que l'expression multiligne fonctionne, des parenthèses doivent être placées, soit comme dans votre suggestion, soit comme dans mon exemple modifié.
leo
Excellent. Je vais maintenant supprimer tous mes commentaires sur les parens.
Bob Stein
15

La plupart des réponses ici sont assez anciennes, et en particulier les réponses acceptées, il semble donc utile de les mettre à jour.

Tout d'abord, la FAQ Python officielle couvre cela et recommande la elifchaîne pour les cas simples et la chaîne pour les cas dictplus grands ou plus complexes. Il suggère également un ensemble de visit_méthodes (un style utilisé par de nombreux frameworks de serveurs) pour certains cas:

def dispatch(self, value):
    method_name = 'visit_' + str(value)
    method = getattr(self, method_name)
    method()

La FAQ mentionne également PEP 275 , qui a été écrit pour obtenir une décision officielle une fois pour toutes sur l'ajout d'instructions de commutateur de style C. Mais ce PEP a en fait été reporté à Python 3, et il n'a été officiellement rejeté qu'en tant que proposition distincte, PEP 3103 . La réponse était, bien sûr, non, mais les deux PPE ont des liens vers des informations supplémentaires si vous êtes intéressé par les raisons ou l'historique.


Une chose qui est revenue plusieurs fois (et peut être vue dans PEP 275, même si elle a été découpée comme une recommandation réelle) est que si vous êtes vraiment gêné d'avoir 8 lignes de code pour gérer 4 cas, contre les 6 lignes que vous auriez en C ou Bash, vous pouvez toujours écrire ceci:

if x == 1: print('first')
elif x == 2: print('second')
elif x == 3: print('third')
else: print('did not place')

Ce n'est pas exactement encouragé par PEP 8, mais c'est lisible et pas trop unidiomatique.


Plus d'une décennie après le rejet du PEP 3103, la question des déclarations de cas de style C, ou même la version légèrement plus puissante de Go, a été considérée comme morte; chaque fois que quelqu'un en parle sur python-ideas ou -dev, ils sont renvoyés à l'ancienne décision.

Cependant, l'idée d'une correspondance complète des modèles de style ML surgit toutes les quelques années, d'autant plus que des langues comme Swift et Rust l'ont adoptée. Le problème est qu'il est difficile d'utiliser beaucoup la correspondance de modèles sans types de données algébriques. Bien que Guido ait sympathisé avec l'idée, personne n'a proposé une proposition qui s'intègre très bien dans Python. (Vous pouvez lire mon modèle de 2014 pour un exemple.) Cela pourrait changer avec dataclassen 3.7 et certaines propositions sporadiques pour un plus puissant enumpour gérer les types de somme, ou avec diverses propositions pour différents types de liaisons instruction-locales (comme PEP 3150 , ou le ensemble de propositions en cours de discussion sur les idées). Mais jusqu'à présent, ce n'est pas le cas.

Il y a aussi parfois des propositions pour la correspondance de style Perl 6, qui est fondamentalement un méli-mélo de tout, de l' elifexpression régulière à la commutation de type à répartition unique.

abarnert
la source
15

Solution pour exécuter des fonctions:

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

où foo1 (), foo2 (), foo3 () et default () sont des fonctions

Alejandro Quintanar
la source
1
Oui, par exemple si votre option de variable == "case2" votre résultat = foo2 ()
Alejandro Quintanar
et ainsi de suite.
Alejandro Quintanar
Oui, je comprends le but. Mais ma préoccupation est que si vous ne voulez foo2(), les foo1(), foo3()et les default()fonctions sont tout aussi vont courir, ce qui signifie des choses pourrait prendre beaucoup de temps
Brian Underwood
1
omettez le () dans le dictionnaire. utiliser get(option)(). problème résolu.
timgeb du
1
Excellent l'utilisation de () est une solution de grille, j'ai fait un essai pour le tester gist.github.com/aquintanar/01e9920d8341c5c6252d507669758fe5
Alejandro Quintanar
13

Je n'ai trouvé la réponse simple que je cherchais nulle part dans la recherche Google. Mais je l'ai quand même compris. C'est vraiment assez simple. Décidé de le poster, et peut-être d'éviter quelques rayures de moins sur la tête de quelqu'un d'autre. La clé est simplement "in" et tuples. Voici le comportement de l'instruction switch avec fall-through, y compris RANDOM fall-through.

l = ['Dog', 'Cat', 'Bird', 'Bigfoot',
     'Dragonfly', 'Snake', 'Bat', 'Loch Ness Monster']

for x in l:
    if x in ('Dog', 'Cat'):
        x += " has four legs"
    elif x in ('Bat', 'Bird', 'Dragonfly'):
        x += " has wings."
    elif x in ('Snake',):
        x += " has a forked tongue."
    else:
        x += " is a big mystery by default."
    print(x)

print()

for x in range(10):
    if x in (0, 1):
        x = "Values 0 and 1 caught here."
    elif x in (2,):
        x = "Value 2 caught here."
    elif x in (3, 7, 8):
        x = "Values 3, 7, 8 caught here."
    elif x in (4, 6):
        x = "Values 4 and 6 caught here"
    else:
        x = "Values 5 and 9 caught in default."
    print(x)

Fournit:

Dog has four legs
Cat has four legs
Bird has wings.
Bigfoot is a big mystery by default.
Dragonfly has wings.
Snake has a forked tongue.
Bat has wings.
Loch Ness Monster is a big mystery by default.

Values 0 and 1 caught here.
Values 0 and 1 caught here.
Value 2 caught here.
Values 3, 7, 8 caught here.
Values 4 and 6 caught here
Values 5 and 9 caught in default.
Values 4 and 6 caught here
Values 3, 7, 8 caught here.
Values 3, 7, 8 caught here.
Values 5 and 9 caught in default.
JD Graham
la source
Où se trouve-t-il exactement ici?
Jonas Schäfer
Oops! Il y a une chute, mais je ne contribue plus à Stack Overflow. Je ne les aime pas du tout. J'aime les contributions des autres, mais pas Stackoverflow. Si vous utilisez Fall Through pour FUNCTIONALITY, vous souhaitez attraper certaines conditions dans une instruction all in one dans un commutateur (un catch all), jusqu'à ce que vous atteigniez une instruction break dans un commutateur.
JD Graham
2
Ici, les valeurs "Chien" et "Chat" TOMBENT et sont gérées par la même fonctionnalité, c'est-à-dire qu'elles sont définies comme ayant "quatre pattes". C'est un équivalent ABSTRACT de chute et différentes valeurs gérées par l'instruction SAME case où une rupture se produit.
JD Graham
@JDGraham Je pense que Jonas signifiait un autre aspect de la solution, qui se produit lorsque le programmeur oublie parfois d'écrire breakà la fin du code pour a case. Mais je pense que nous n'avons pas besoin d'une telle "solution" :)
Mikhail Batcer
12

Les solutions que j'utilise:

Une combinaison de 2 des solutions publiées ici, qui est relativement facile à lire et prend en charge les valeurs par défaut.

result = {
  'a': lambda x: x * 5,
  'b': lambda x: x + 7,
  'c': lambda x: x - 2
}.get(whatToUse, lambda x: x - 22)(value)

.get('c', lambda x: x - 22)(23)

regarde "lambda x: x - 2"dans le dict et l'utilise avecx=23

.get('xxx', lambda x: x - 22)(44)

ne le trouve pas dans le dict et utilise la valeur "lambda x: x - 22"par défaut avec x=44.

thomasf1
la source
10
# simple case alternative

some_value = 5.0

# this while loop block simulates a case block

# case
while True:

    # case 1
    if some_value > 5:
        print ('Greater than five')
        break

    # case 2
    if some_value == 5:
        print ('Equal to five')
        break

    # else case 3
    print ( 'Must be less than 5')
    break
user5224656
la source
10
def f(x):
    dictionary = {'a':1, 'b':2, 'c':3}
    return dictionary.get(x,'Not Found') 
##Returns the value for the letter x;returns 'Not Found' if x isn't a key in the dictionary
Vikhyat Agarwal
la source
Pensez à inclure une brève description de votre code et comment il résout la question affichée
Henry Woody
D'accord, j'ai ajouté un commentaire à ce sujet maintenant.
Vikhyat Agarwal
8

J'ai aimé la réponse de Mark Bies

Puisque la xvariable doit être utilisée deux fois, j'ai modifié les fonctions lambda en sans paramètre.

Je dois courir avec results[value](value)

In [2]: result = {
    ...:   'a': lambda x: 'A',
    ...:   'b': lambda x: 'B',
    ...:   'c': lambda x: 'C'
    ...: }
    ...: result['a']('a')
    ...: 
Out[2]: 'A'

In [3]: result = {
    ...:   'a': lambda : 'A',
    ...:   'b': lambda : 'B',
    ...:   'c': lambda : 'C',
    ...:   None: lambda : 'Nothing else matters'

    ...: }
    ...: result['a']()
    ...: 
Out[3]: 'A'

Edit: j'ai remarqué que je peux utiliser Nonetype avec des dictionnaires. Donc, cela imiteraitswitch ; case else

guneysus
la source
Le cas None n'émule-t-il pas simplement result[None]()?
Bob Stein
Oui, exactement. Je veux direresult = {'a': 100, None:5000}; result[None]
guneysus
4
Vérifier simplement que personne ne pense None:se comporte comme ça default:.
Bob Stein
7
def f(x):
     return 1 if x == 'a' else\
            2 if x in 'bcd' else\
            0 #default

Court et facile à lire, a une valeur par défaut et prend en charge les expressions dans les conditions et les valeurs de retour.

Cependant, il est moins efficace que la solution avec un dictionnaire. Par exemple, Python doit parcourir toutes les conditions avant de renvoyer la valeur par défaut.

émeu
la source
7

vous pouvez utiliser un dict expédié:

#!/usr/bin/env python


def case1():
    print("This is case 1")

def case2():
    print("This is case 2")

def case3():
    print("This is case 3")


token_dict = {
    "case1" : case1,
    "case2" : case2,
    "case3" : case3,
}


def main():
    cases = ("case1", "case3", "case2", "case1")
    for case in cases:
        token_dict[case]()


if __name__ == '__main__':
    main()

Production:

This is case 1
This is case 3
This is case 2
This is case 1
Felix Martinez
la source
6

Simple, non testé; chaque condition est évaluée indépendamment: il n'y a pas de transition, mais tous les cas sont évalués (bien que l'expression à activer n'est évaluée qu'une seule fois), sauf s'il existe une instruction break. Par exemple,

for case in [expression]:
    if case == 1:
        print(end='Was 1. ')

    if case == 2:
        print(end='Was 2. ')
        break

    if case in (1, 2):
        print(end='Was 1 or 2. ')

    print(end='Was something. ')

imprime Was 1. Was 1 or 2. Was something. (Bon sang! Pourquoi ne puis-je pas avoir d'espaces de fin dans les blocs de code en ligne?) si expressionévalue à 1, Was 2.si expressionévalue à 2, ou Was something.si expressionévalue à autre chose.

Solomon Ucko
la source
1
Eh bien, la chute à travers les œuvres, mais seulement pour aller à do_default.
syockit le
5

Définition:

def switch1(value, options):
  if value in options:
    options[value]()

vous permet d'utiliser une syntaxe assez simple, avec les cas regroupés dans une carte:

def sample1(x):
  local = 'betty'
  switch1(x, {
    'a': lambda: print("hello"),
    'b': lambda: (
      print("goodbye," + local),
      print("!")),
    })

J'ai continué à essayer de redéfinir le commutateur d'une manière qui me permettrait de me débarrasser du "lambda:", mais j'ai abandonné. Ajuster la définition:

def switch(value, *maps):
  options = {}
  for m in maps:
    options.update(m)
  if value in options:
    options[value]()
  elif None in options:
    options[None]()

Me permet de mapper plusieurs cas au même code et de fournir une option par défaut:

def sample(x):
  switch(x, {
    _: lambda: print("other") 
    for _ in 'cdef'
    }, {
    'a': lambda: print("hello"),
    'b': lambda: (
      print("goodbye,"),
      print("!")),
    None: lambda: print("I dunno")
    })

Chaque cas répliqué doit être dans son propre dictionnaire; switch () consolide les dictionnaires avant de rechercher la valeur. C'est encore plus laid que je ne le souhaiterais, mais il a l'efficacité de base d'utiliser une recherche hachée sur l'expression, plutôt qu'une boucle à travers toutes les clés.

William H. Hooper
la source
5

Je pense que la meilleure façon est d' utiliser les idiomes du langage python pour garder votre code testable . Comme indiqué dans les réponses précédentes, j'utilise des dictionnaires pour tirer parti des structures et du langage python et garder le code "case" isolé dans différentes méthodes. Ci-dessous, il y a une classe, mais vous pouvez utiliser directement un module, des globaux et des fonctions. La classe a des méthodes qui peuvent être testées avec isolation . Selon vos besoins, vous pouvez également jouer avec des méthodes et des attributs statiques.

class ChoiceManager:

    def __init__(self):
        self.__choice_table = \
        {
            "CHOICE1" : self.my_func1,
            "CHOICE2" : self.my_func2,
        }

    def my_func1(self, data):
        pass

    def my_func2(self, data):
        pass

    def process(self, case, data):
        return self.__choice_table[case](data)

ChoiceManager().process("CHOICE1", my_data)

Il est possible de profiter de cette méthode en utilisant également des classes comme clés de "__choice_table". De cette façon, vous pouvez éviter la toxicomanie et garder tout propre et testable.

Supposons que vous deviez traiter un grand nombre de messages ou de paquets à partir du net ou de votre MQ. Chaque paquet a sa propre structure et son code de gestion (de manière générique). Avec le code ci-dessus, il est possible de faire quelque chose comme ceci:

class PacketManager:

    def __init__(self):
        self.__choice_table = \
        {
            ControlMessage : self.my_func1,
            DiagnosticMessage : self.my_func2,
        }

    def my_func1(self, data):
        # process the control message here
        pass

    def my_func2(self, data):
        # process the diagnostic message here
        pass

    def process(self, pkt):
        return self.__choice_table[pkt.__class__](pkt)

pkt = GetMyPacketFromNet()
PacketManager().process(pkt)


# isolated test or isolated usage example
def test_control_packet():
    p = ControlMessage()
    PacketManager().my_func1(p)

La complexité n'est donc pas répartie dans le flux de code, mais elle est rendue dans la structure de code .

J_Zar
la source
Vraiment moche ... le boîtier du commutateur est si propre lors de la lecture. Impossible de comprendre pourquoi il n'est pas implémenté en Python.
jmcollin92
@AndyClifton: Je suis désolé ... un exemple? Pensez à chaque fois que vous devez avoir un code de branchement à décisions multiples et vous pouvez appliquer cette méthode.
J_Zar
@ jmcollin92: l'instruction switch est confortable, je suis d'accord. Cependant, le programmeur a tendance à écrire de très longues instructions et du code qui n'est pas réutilisable. La façon dont j'ai décrit est plus propre à tester et plus réutilisable, à mon humble avis.
J_Zar
@J_Zar: re. Ma demande pour un exemple: oui, je l'obtiens, mais j'ai du mal à mettre cela dans le contexte d'un morceau de code plus grand. Pourriez-vous montrer comment je pourrais l'utiliser dans une situation réelle?
Andy Clifton
1
@AndyClifton: Je suis désolé, je suis en retard mais j'ai publié un exemple de cas.
J_Zar
5

Développant la réponse de Greg Hewgill - Nous pouvons encapsuler la solution de dictionnaire en utilisant un décorateur:

def case(callable):
    """switch-case decorator"""
    class case_class(object):
        def __init__(self, *args, **kwargs):
            self.args = args
            self.kwargs = kwargs

        def do_call(self):
            return callable(*self.args, **self.kwargs)

return case_class

def switch(key, cases, default=None):
    """switch-statement"""
    ret = None
    try:
        ret = case[key].do_call()
    except KeyError:
        if default:
            ret = default.do_call()
    finally:
        return ret

Cela peut ensuite être utilisé avec le @case-décorateur

@case
def case_1(arg1):
    print 'case_1: ', arg1

@case
def case_2(arg1, arg2):
    print 'case_2'
    return arg1, arg2

@case
def default_case(arg1, arg2, arg3):
    print 'default_case: ', arg1, arg2, arg3

ret = switch(somearg, {
    1: case_1('somestring'),
    2: case_2(13, 42)
}, default_case(123, 'astring', 3.14))

print ret

La bonne nouvelle est que cela a déjà été fait dans le module NeoPySwitch . Installez simplement en utilisant pip:

pip install NeoPySwitch
À M
la source
5

Une solution que j'ai tendance à utiliser et qui utilise également des dictionnaires est:

def decision_time( key, *args, **kwargs):
    def action1()
        """This function is a closure - and has access to all the arguments"""
        pass
    def action2()
        """This function is a closure - and has access to all the arguments"""
        pass
    def action3()
        """This function is a closure - and has access to all the arguments"""
        pass

   return {1:action1, 2:action2, 3:action3}.get(key,default)()

Cela a l'avantage de ne pas essayer d'évaluer les fonctions à chaque fois, et il suffit de s'assurer que la fonction externe obtient toutes les informations dont les fonctions internes ont besoin.

Tony Suffolk 66
la source
5

Jusqu'à présent, il y a eu beaucoup de réponses qui ont dit: "nous n'avons pas de commutateur en Python, faites-le de cette façon". Cependant, je voudrais souligner que l'instruction switch elle-même est une construction facilement exploitable qui peut et doit être évitée dans la plupart des cas car elle favorise la programmation paresseuse. Exemple concret:

def ToUpper(lcChar):
    if (lcChar == 'a' or lcChar == 'A'):
        return 'A'
    elif (lcChar == 'b' or lcChar == 'B'):
        return 'B'
    ...
    elif (lcChar == 'z' or lcChar == 'Z'):
        return 'Z'
    else:
        return None        # or something

Maintenant, vous pouvez le faire avec une instruction switch (si Python en proposait une) mais vous perdriez votre temps car il existe des méthodes qui le font très bien. Ou peut-être avez-vous quelque chose de moins évident:

def ConvertToReason(code):
    if (code == 200):
        return 'Okay'
    elif (code == 400):
        return 'Bad Request'
    elif (code == 404):
        return 'Not Found'
    else:
        return None

Cependant, ce type d'opération peut et doit être géré avec un dictionnaire car il sera plus rapide, moins complexe, moins sujet aux erreurs et plus compact.

Et la grande majorité des «cas d'utilisation» pour les instructions switch tomberont dans l'un de ces deux cas; il n'y a que très peu de raisons d'en utiliser un si vous avez bien réfléchi à votre problème.

Ainsi, plutôt que de demander "comment puis-je basculer en Python?", Nous devrions peut-être demander, "pourquoi est-ce que je veux passer en Python?" car c'est souvent la question la plus intéressante et elle expose souvent des défauts dans la conception de tout ce que vous construisez.

Maintenant, cela ne veut pas dire que les commutateurs ne devraient jamais être utilisés non plus. Les machines d'état, les lexers, les analyseurs et les automates les utilisent tous dans une certaine mesure et, en général, lorsque vous démarrez à partir d'une entrée symétrique et passez à une sortie asymétrique, ils peuvent être utiles; vous devez juste vous assurer que vous n'utilisez pas le commutateur comme un marteau car vous voyez un tas de clous dans votre code.

Woody1193
la source