ITと哲学と

IT系エンジニアによる技術と哲学のお話。

Hello Python3.10

Python3.10が2021/10/4に正式リリースされる予定です。

Python3.10で新たに入る機能はすでにアナウンスされていますが、この中でいくつか気になったものがあるので、紹介しようと思います。

なお、本情報は2021/8/15現在の情報です。

スケジュール

Python3.10のリリーススケジュールは以下のPEPの通りです。

www.python.org

現在は3.10.0 candidate 1がリリースされています。

動作サンプル

この記事のコードはこちら。

github.com

Python3.10rc1で動作します。

本記事で紹介する新機能

個人的なお気に入りは以下の3つなので、それぞれ紹介します。

  • ErrorMessageのエンハンス
  • PEP 604, Allow writing union types as X | Y
  • PEP 634, PEP 635, PEP 636, Structural Pattern Matching

ErrorMessageのエンハンス

コード書くとき、よく触れるエラーメッセージですが、Python3.10からはちょっと気の利いたメッセージの出し方に変わります。 例を見てみましょう。

items = {
    "x":1,
    "y":2    #,がない
    "x":3,
}

python3.9では、これを実行しようとすると、以下のようなエラーメッセージが表示されます。

root@d0b73014baa3:/src# python sample.py 
  File "/src/sample.py", line 5
    "x":3,
    ^
SyntaxError: invalid syntax

これが、3.10では以下のようにユーザフレンドリーなエラーメッセージの表示に変わります。

root@3ffc6908fe04:/src# python sample.py 
  File "/src/sample.py", line 4
    "y":2 
        ^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?

該当行をちゃんと指定した上で、comma忘れてね?と教えてくれるわけです。超便利。 長時間作業して集中力落ちてきた時にハマって絶望する人類が減るので大変良いです。

さらに、タイポした変数を検知して修正を提案してくれたりもします。僕は普段タイポの修正に70%ほどの生産性を費やしているので、これがAIに仕事を奪われるということか、と時代の流れを感じています。

ultimate_answer = 42 
print(ultimat_answer) # typoしてる

python3.9では以下の通りで、エラーメッセージ見てもよくわからないですよね。

root@d0b73014baa3:/src# python sample.py 
Traceback (most recent call last):
  File "/src/sample.py", line 2, in <module>
    print(ultimat_answer) 
NameError: name 'ultimat_answer' is not defined

お酒飲みながら作業してたりすると見逃して、はまってしまいますよね。

これ、python3.10ではこんな感じになります。

root@3ffc6908fe04:/src# python sample.py 
Traceback (most recent call last):
  File "/src/sample.py", line 11, in <module>
    print(ultimat_answer)     
NameError: name 'ultimat_answer' is not defined. Did you mean: 'ultimate_answer'?

ピャ------!!賢い!Python3.10タン賢い! 積極的にタイポしてPython3.10タンの優しさに包まれたい!!! と、なりますよね。

UnionType

型の表現の際に、"or"を表現するには、Python3.9までは以下のようにUnion型を使って表現していました。

def is_dog_or_cat(animal: Union[Dog, Cat]):
    pass

これが、Pyhthon3.10からは"|"を使って以下のように表現できます。他の言語でもよくみるような記法ですよね。

def can_pet(animal: Dog | Cat):
    pass

この記法ができることになったことで、後述のパターンマッチングがより綺麗に書けるようになります。

パターンマッチング

初歩

ここからが真打のパターンマッチングの紹介です。

これまでのpythonでは以下のように書いていたものを

if isinstance(x, tuple) and len(x) == 2: 
    host, port = x 
    mode = "http" 
elif isinstance(x, tuple) and len(x) == 3: 
    host, port, mode = x 
# Etc.

パターンマッチングを使うと以下のように記載できます。

match x: 
    case host, port: 
        mode = "http" 
    case host, port, mode: 
        pass 
    # Etc.

一般化して記載するとこんな感じです。

match 式: 
    case パターン1: 
        … 
    case パターン2: 
        …

式の値を評価して、合致するパターンの文に飛ばすといった機能です。 この時、パターンは上からチェックされ、一番初めに合致した条件に飛ばします。

パターンのバリエーション

パターンの書き方が色々とあり、かなり細かく表現することができます。

数値リテラルのパターン

まずは基本的なパターンである、数値でのパターンの記載です。

def http_error(status):
    match status:
        case 400:
            print("Bad request")
        case 404:
            print("Not found")
        case 418:
            print("I'm a teapot")

http_error(400) #->Bad request

この例だと、status=400なので、case 400のパターンに合致し、それに続く文が実行されます。 caseに合致するパターンが存在しない場合は、何も実行されません。

defaultのパターン

caseに特筆すべきパターンはないが、デフォルトでなんらかのパターンにマッチさせたいような場合は、以下の通りに記載します。

def http_error(status):
    match status:
        case 400:
            print("Bad request")
        case 404:
            print("Not found")
        case 418:
            print("I'm a teapot")
        case 200 | 201:
            print("OK")
        case _:
            print("something worng")

http_error(405) # ->something wrong

この、"_"がワイルドカードになっており、全てのパターンに合致するため、デフォルトでこれが実行されます。 なお、デフォルトパターンは全てのパターンが合致するため、その下にパターンをおいても到達できないコードになりますので、この場合は実行時に以下の通りエラーが出ます。

def http_error(status):
    match status:
        case 400:
            print("Bad request")
        case _: #->ここにおくと、ここ以下のパターンが到達不可能になる
            print("something worng")
        case 404:
            print("Not found")
        case 418:
            print("I'm a teapot")
        case 200 | 201:
            print("OK")

エラーメッセージは以下の通りです。

root@3ffc6908fe04:/src# python main.py lesson_3
  File "/src/main.py", line 46
    case _:
         ^
SyntaxError: wildcard makes remaining patterns unreachable

自作型のパターン

数値リテラルだけではなく、dataclassで作成した自作型を使ったパターンも記載できます。

以下の例では、引数に与えられた動物(animal)が、Dogか否かを判定します。

@dataclass 
class Dog:
    name:str
    breed:str

def is_dog(animal):
        match animal:
            case Dog():
                return True
            case _:
                return False

orの表現

次に、引数に与えられた動物(animal)がDogかCatの場合はペットにできると判定し、それ以外はペットにできないと判定するような関数を考えます。

UnionTypeの新しい記法を使うと、こんなふうに記載することが可能です。

def is_pet(animal):
    match animal:
        case Dog() | Cat():
            return True
        case _:
            return False

dog = Dog("pochi", "Chihuahua")
cat = Cat("tama", "white")
crow = Crow("car")

print(is_pet(dog))  #-> True
print(is_pet(cat))  #-> True
print(is_pet(crow)) #-> False

ここでUnion[Dog, Cat]とせずに、Dog|Catと書けるのは気持ちがいいですね。

メンバ変数でのパターン

さらに細かく、型のメンバ変数の値を評価するようなパターンを記載することも可能です。

引数に与えられた動物(animal)が、自分のペットか否かを判定する関数を書きます。 なお、自分のペットは名前がpochiであり、犬種がチワワであるとします。

この場合は、型だけでなく、メンバ変数の値をみて判定する必要がありますよね。そんな場合はこう記載できます。

def is_mypet(animal):
    match animal:
        case Dog(name="pochi", breed="Chihuahua"):
            return True
        case _:
            return False

mypet = Dog("pochi", "Chihuahua")
anotherDog = Dog("hanako", "Shiba")
crow = Crow("car")

print(is_mypet(mypet))  #->True
print(is_mypet(anotherDog))  #->False
print(is_mypet(crow)) #->False

メンバ変数パターンでのワイルドカード

さらに、メンバ変数パターンでワイルドカードを使うこともできます。 例えば、引数に与えられた動物(animal)がチワワかどうかを返す関数においては、犬の名前はどうでもよくて、breed=Chihuahuaかどうかがポイントです。

def is_Chihuahua(animal):
    match animal:
        case Dog(name=_, breed="Chihuahua"):
            return True
        case _:
            return False

chihuahua_1 = Dog("pochi", "Chihuahua")
shiba = Dog("hanako", "Shiba")
chihuahua_2 = Dog("taro", "Chihuahua")
crow = Crow("car")

print(is_Chihuahua(chihuahua_1)) #->True
print(is_Chihuahua(shiba)) #->False
print(is_Chihuahua(chihuahua_2)) #->True    
print(is_Chihuahua(crow)) #->False

こんな形で、さまざまにパターンを書いていくことができます。

バインディング

パターンマッチングのさらに強力な機能、バインディングを紹介します。 パターンに合致した値を、名前にバインディングするという機能です。 言葉で説明しても意味がわからないので、例をみてみましょう。

def build_message(animal):
    match animal:
        case Dog("pochi", "Chihuahua"):
            return f"he is my pet!"
        case Dog(name = _ as dog_name, breed="Chihuahua"): # dog_nameという名前にバインディングしている
                return f"I love Chihuahua ! come on {dog_name}!"
        case Dog(_, "Shiba"):
            return f"Shiba is not so bad."
        case _:
            return "..."

chihuahua_2 = Dog("taro", "Chihuahua")

print(build_message(chihuahua_2)) #->I love Chihuahua ! come on taro!

ポイントは、case Dog(name = _ as dog_name, breed="Chihuahua"):の行です。 このas nameで、Dogのメンバ変数nameの値をdog_nameという名前にバインディングしています。 バインディグされた名前は、caseの中で参照可能で、returnする文字列として組み込まれています。

大きな注意点として、バインディングと代入は異なります。 バインディングはあくまでcaseコンテキストの中で値を名前に一時的にくっつけるだけのものであり、caseコンテキストの外からは参照できませんし、影響を及ぼしません。

上記の意味がわかる例を下に示します。

def build_message(animal):
    name="X" # nameという名前の変数を宣言する
    match animal:
        case Dog("pochi", "Chihuahua"):
            val= f"he is my pet!"
        case Dog(_ as name, "Chihuahua"): # nameを上書きしているようにみえるが?
            val= f"I love Chihuahua ! come on {name}!"
        case Dog(_, "Shiba"):
            val= f"Shiba is not so bad."
        case _:
            val= "..."
    print(name) # ここではXが出力される。名前がかぶっていても、もとのnameが予期せぬ上書きをされない
    return val

anotherDog = Dog("hanako", "Chihuahua")

print(build_message(anotherDog)) #->I love Chihuahua ! come on hanako!

caseの中で行われるバインディングは、あくまでcaseコンテキストの中での話であり、その外の世界には影響を及ぼさないことが見て取れると思います。

なお、無理矢理外の世界に影響を与えようとすると、実行時エラーが発生します。

def build_message(animal):
    hoge=[0]
    match animal:
        case Dog("pochi", "Chihuahua"):
            return f"he is my pet!"
        case Dog(_ as hoge[0], "Chihuahua"): # hogeの第一引数に代入しようとしているがNG
            return f"I love Chihuahua ! come on {name}!"
        case Dog(_, "Shiba"):
            return f"Shiba is not so bad."
        case _:
            return "..."

実行時のエラー

root@3ffc6908fe04:/src# python main.py lesson_7
  File "/src/main.py", line 155
    case Dog(_ as hoge[0], "Chihuahua"): # hogeの第一引数に代入しようとしているがNG
                      ^
SyntaxError: invalid syntax

さらにさらに、listに対して、こんな書き方ができます。 先頭と末尾の要素をfirstlastとして取り出して、それ以外の要素はmiddleに入れるといった書き方です。

def lesson_9():
    l = [1,2,3,4,5]
    match l:
        case [first, *middle, last]:
            print(f"first is {first}, last is {last},")
            print(f"size of middle is {len(middle)}")

関数型フロントエンド言語のElmの勉強をしていたときに、このような形でリストを綺麗に扱うことができる仕組みを学び、感動してElmが好きになったのですが、Pythonでもついにこれができるようになるということで、とても嬉しいです。

注意点

Python3.10の時点では、caseの細かい精的なチェックは行われません。 ワイルドカードを末尾以外に持っていった場合の到達不能コードについてはチェックしてくれますが、それ以外の方法で到達不能になるコードのチェックはしてくれないので、エラーは起きません。

以下の例では、colorはColorのEnumであり、case 1に到達することはないですが、これをチェックしてくれたりはしませんし、Color.BLUEの処理を記載していない件についてエラーで教えてくれたりもしません。

ここら辺が静的にチェックできるようになるともっと良いなと思います。

from enum import Enum 
class Color(Enum): 
    RED = 0 
    GREEN = 1 
    BLUE = 2 
def which_color(color: Color): 
    match color: 
        case Color.RED: 
            print("I see red!") 
        case Color.GREEN: 
            print("Grass is green") 
        case 1: 
            print("!!!") 
which_color(1)

予期せぬバグを仕込まないように、注意しなくてはいけませんね。

おわりに

Python3.10のリリース、とても待ち遠しいですね。 みなさんPython3.10使っていきましょう。