Techブログ - MNTSQ, Ltd.

リーガルテック・カンパニー「MNTSQ(モンテスキュー)」のTechブログです。

python3.8 から新しく導入された Literal types について

MNTSQ Tech Blog TOP > 記事一覧 > python3.8 から新しく導入された Literal types について

f:id:myatsdqn:20201225151816p:plain

はじめに

pythonの関数に与える引数として特定の値のみを許容したいときはないでしょうか?

そのようなときに、動的に引数の値をチェックして範囲外のものを除外するアサーションや、Enumを用いてとりうる値を絞ることが考えられます。

ですが、前者は動的な値検査しか行えず、後者についてはAPIの引数の型をEnumに変更する必要があり、Enumが既存のAPIオーバーロードしたときの引数の型として使えるとは限らないです。1

このような引数の値のチェックに使える手段として、python3.8から使えるようになっているLiteral typesがあります。

Literal types は型として宣言することで、関数の引数等に対して特定の型を要求するのと同時に、特定の値を持つことも要求できます。

次のようなコードの例を考えてみます。

target_fruit = ['apple', 'banana']

def print_fruit(fruit: str):
    assert fruit in target_fruit
    print(fruit)
    
print_fruit('orange') # 動作時にアサーションエラー

ここではtype hintはprint_fruit関数の引数がstrであることまではチェックしてくれますが、中身の値が applebanana のどちらであるかまではチェックしないです。想定しない文字列に対する処理を関数に書く必要があり、動作時のアサーションのみによってfruitの値が検証されます。

Literal types を用いた型宣言に書き換えると次のようになります。

Fruit = Literal['apple', 'banana']

def print_fruit(fruit: Fruit):
    print(fruit)
    
print_fruit('orange') # type hintingの型チェックが入る

この場合、pycharmがIDEの場合には次のようなwarningが表示されます。

f:id:S_heiya:20201216211744p:plain

Literal typeの型チェックはコード実行時ではなく、静的に判定されるため、今まで引数のアサーション等を行っていた場所に適用することで、Type hintingの恩恵が得られそうです。

Literal周りのルール

Literal記述時のルールです。

  • Literal types の宣言方法は Literal[{許容したい値のリスト}] です。
  • 許容したい値のリストの中身が複数である時、そのLiteral type は値のユニオンと同等です

    • 例えば Literal[v1, v2, v3]Union[Literal[v1], Literal[v2], Literal[v3]] と同等です。
  • Literalに入れる値に計算式や動的な計算結果は含められません。

    • 例えば Literal[7] という書き方はできますが、Literal[3 + 4] という書き方はできません。
    • 直前に some_result = 1 + 3 と計算した直後に Literal[some_result] と書くこともできません。
  • Literalの中に入れられる値は以下のような動的でない値です。

行列型の宣言例

Literal types とジェネリック型と組み合わせればサイズの情報を持つ行列型を定義することが可能です

from __future__ import annotations

A = TypeVar('A', bound=int)
B = TypeVar('B', bound=int)
C = TypeVar('C', bound=int)

class Matrix(Generic[A, B]):
    def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
    def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
    def transpose(self) -> Matrix[B, A]: ...

foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)

baz = foo @ bar

pycharm環境でbaz変数にマウスオーバーするとサイズが計算された型情報が表示されます。

f:id:S_heiya:20201216211812p:plain

しかし、このような整数リテラルを用いた型サイズの計算は、静的に宣言可能なジェネリック型の順序入れ替え等で可能な範囲に絞られます。

例えば2つのベクトルをconcatする操作を型で表現しようとすると、Literal types 同士を足し算する必要があり、この機能では実現できません。

class Vector(Generic[N, T]): ...

def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
    # ...snip...

おわりに

型として制約をかける値同士の動的な計算はできないという制約はあるものの、新しく実装するクラスの厳密なインターフェイスを設計する時や、既存のライブラリの動的な値エラーをなくすために Literal types は有用そうです。python3.8以降を用いているプロジェクトについて導入を検討してみてはどうでしょうか。

参考リンク

この記事を書いた人

f:id:mntsq:20201223123239j:plain

yad

ビリヤニ食べたい


  1. pythonでは @overload デコレータを用いて、引数の型の複数の組み合わせをサポートする関数やメソッドを書けるようになります。typing --- 型ヒントのサポート — Python 3.9.1 ドキュメント