やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【擬似実装コード有り】Pythonでswitch構文っぽいものを表現する方法について

かゆい所に手が届くswitch構文

別にswitch構文がなくても、おそらくその処理は工夫次第で記述することが可能だろう。しかし、golangjavascriptに見られるswitch構文を用いることで少なくとも可読性は上がるし、拡張性もif elseの組み合わせよりは良いはず。それにif elseの組み合わせで複雑な多条件を表現すると階層化されて条件がネストされるため視認性が下がるので、好きではない。switch構文なら見かけ上ではcaseA, caseB, caseC...は対等に見えるので視認性はこちらの方が良い(実際の内部処理は上から辿っている模様)

別にswitchがなくても処理は書ける
golang

func FizzBuzz(num int) string {
  if num % 15 == 0 {
    return "fizzbuzz"
  } else if num % 3 == 0 {
    return "fizz"
  } else if num % 5 == 0 {
    return "buzz"
  }
  return "no match"
}

switch構文を使えば可読性と拡張性が増す
golang

func FizzBuzz(num int) string {
  // switchに渡す条件をtrueにしておけば条件式を記述出来る
  switch true {
    case num % 15 == 0:
      return "fizzbuzz"
    case num % 3 == 0:
      return "fizz"
    case num % 5 == 0:
      return "buzz"
  }
  return "no match"
}

仮に「七の倍数の時はラッキーって出力しておいて〜」という仕様が急に決まったとしても以下のようにするだけで改修は終わる(ただし、評価順は上から順なので注意)

// 7の倍数という条件の方を優位にさせるため、 num % 3 == 0の上に記述(最初に21で被る)
func FizzBuzz(num int) string {
  // switchに渡す条件をtrueにしておけば条件式を記述出来る
  switch true {
    case num % 15 == 0:
      return "fizzbuzz"
    case num % 7 == 0:
      return "ラッキー"
    case num % 3 == 0:
      return "fizz"
    case num % 5 == 0:
      return "buzz"
  }
  return "no match"
}

「お、こんな便利な構文使わん理由ないやん、よっしゃPythonでも書いたろ」と思っても残念。Pythonにはswitch構文は実装されていない。理由は以下の公式ドキュメントのQAにて書かれている通り

docs.python.org

if... elif... elif... else の繰り返しで簡単に同じことができます。switch 文の構文に関する提案がいくつかありましたが、範囲判定をするべきか、あるいはどのようにするべきかについての合意は (まだ) 得られていません。

一言で言えば、他に書く方法あるから、それで何とかしてね。って感じかな。おっしゃる通りだけど、あってもいい気はする。で、このPythonswitch構文を導入するかどうかは2001年頃から議論されており、拒否されているよう。
www.python.org

それでもそれっぽいswitch構文が使いたい

です。なので、それっぽいのを記述する。すでに多くの先駆者がif else使ったり、dictionaryにkeyをセットして関数をvalueに持たせたりと既出のものが多いので自分が調べた限り、この書き方は確認出来なかったので載せておく。正直な所はO(1)で高速にアクセス可能なdictionaryのkeyとvalueを使った複数条件処理を推したいところではある

if false: でネストさせるやつ

どういうこと。とりあえずコードを見せる

def fizzbuzz(num):
  """
    num -> int
    return -> int
  """
  if False:
    pass
  elif num % 15 == 0:
    print("fizzbuzz")
  elif num % 3 == 0:
    print("fizz")
  elif num % 5 == 0:
    print("buzz")

なぜif False:と一番上の条件式に記述をしているかというと、先程話した通り、条件式が見た目上、ネストするのを回避するためと後の拡張性を確保したいからだ。処理としては一判定無駄になってしまうが、前者の理由を優先した。あとはswitch構文同様にcaseと記述して条件式を書くもの、ifswitch構文っぽく記述するなら全てelifという様に記述できるように意識した。正直、好みの問題だし下記の記述がdefaultだろう

def fizzbuzz(num):
  """
    num -> int
    return -> None
  """
  if num % 15 == 0:
    print(num, " is fizzbuzz")
  elif num % 3 == 0:
    print(num, " is fizz")
  elif num % 5 == 0:
    print(num, " is buzz")

無名関数のリストを作るやつ

無名関数を使ってこんなことが出来る

lst = [lambda x: x > 5, lambda x: x < 3]

for i, func in enumerate(lst):
  print(i, "->", func(i))

実行結果

0 -> False # 0 > 5
1 -> True # 1 < 3

この様に無名関数をリストに保持させておくことで実行順序を保証できて、条件式を実行時評価状態にしておくことが出来るので、複雑な条件式を表現可能ということでswitch構文っぽいものが記述出来る。dictionaryを使う場合にはkeyをa, b, cのようにsortされても問題ないように順序を意識してkeyを作成する必要があるため、うーんとなり、この方法が思いついた

# 条件式を内包する無名関数を作成(実行時に評価される)
# Falseを返しているのは実行時に評価式がFalse判定されたことを呼び出し元に伝えるため
conditions = [
  lambda x: "fizzbuzz" if x % 15 == 0 else False,
  lambda x: "fizz" if x % 3 == 0 else False,
  lambda x: "buzz" if x % 5 == 0 else False
]

# 擬似switch構文(値と条件式を含む無名関数のリストを受け取る)
def switch(val, judge_lst, default_res=None):
  """
    # val -> any
    # judge_lst -> list[func]
    # default_res(None) -> any
    # return -> any
  """
  # リストに含まれている関数を順に実行(リストなので)
  for func in judge_lst:
    res = func(val)
    # Flaseが返ってきていない(評価式がtrueとなった)ならbreak
    # この処理を削除すればフォールスルーになる(breakをしないと次の評価式に移るやつ)
    if res:
      return res
  return default_res

実行結果

for i in range(1, 31):
  print(i, " -> ", switch(i, conditions, "no match"))

# 1  ->  no match
# 2  ->  no match
# 3  ->  fizz
# 4  ->  no match
# 5  ->  buzz
# 6  ->  fizz
# 7  ->  no match
# 8  ->  no match
# 9  ->  fizz
# 10  ->  buzz
# 11  ->  no match
# 12  ->  fizz
# 13  ->  no match
# 14  ->  no match
# 15  ->  fizzbuzz
# 16  ->  no match
#  :
# 30  ->  fizzbuzz

今回はサンプルのために、ただ文字列を返すという無名関数を作成したが、無名関数内から定義済み関数をcallしたり、クラスのinstanceを返したりと割とやれることは多いはず。ただ、if else...の実装に比べると明らかに重いので、上手く共通化出来る時ぐらいしか出番はないだろう

conditions = [
  lambda x: str_slicer("fizzbuzz") if x % 15 == 0 else False,
  lambda x: str_slicer("fizz") if x % 3 == 0 else False,
  lambda x: str_slicer("buzz") if x % 5 == 0 else False
]

def str_slicer(str_):
  """
    str_ -> string
    return -> list[string]
  """
  res = list()
  append = res.append
  for s in str_:
    append(s)
  return res

実行結果

for i in range(1, 31):
  print(i, " -> ", switch(i, conditions, "no match"))

# 1  ->  no match
# 2  ->  no match
# 3  ->  ['f', 'i', 'z', 'z']
# 4  ->  no match
# 5  ->  ['b', 'u', 'z', 'z']
# 6  ->  ['f', 'i', 'z', 'z']
# 7  ->  no match
# :
# 29  ->  no match
# 30  ->  ['f', 'i', 'z', 'z', 'b', 'u', 'z', 'z']

総評

すでにPythonswitch構文の代役に関する記事はそこそこあるが、個人的な興味と久しぶりにPythonを記述機会があったので遊んでみた。if else...を淡々と書き続けていると頭がおかしくなりそうなので、最近は無名関数に条件式埋め込んでってことをよくやる。記事の主題とは関係ないが、Pythonswitch構文がない理由を調べる中で、「デザインと歴史」というページにたどり着いて、少し読んでみたが面白かった。興味のある項目があったら読んでみるのも良いかもしれない

参考文献