SANDFISH FACTORY

技術ブログです。python・vuejsを愛でる日々について綴ります

toioをpythonで動かす:番外編(型で学んだこと)

はじめに

前回は「toioをpythonで動かす:BLE操作」を行いました。
今回はちょっと番外編です。

これまでtoio.jsのコードを読みながら、これってpythonで書くとどうなるんだ?と考えながら書いていました。
私自身pythonAPIドキュメントを全て読んでいたわけではなかったので、割と新しい発見がありました。

型で学んだこと

Union型/Optional型

toio.jsのコードはtypescriptで書かれています。
その中で以下のようなコードが出てきました。

export type DataType =
  | {
      buffer: Uint8Array
      data: PositionIdInfo
      dataType: 'id:position-id'
    }
  | {
      buffer: Uint8Array
      data: StandardIdInfo
      dataType: 'id:standard-id'
    }
  | {
      buffer: Uint8Array
      dataType: 'id:position-id-missed'
    }
  | {
      buffer: Uint8Array
      dataType: 'id:standard-id-missed'
    }

これは { buffer: Uint8Array … } のそれぞれを型とし、DataTypeにはいずれかの型が入ることを示しています。
これをpythonで実現しようとするとtype hintのUnion型で対応することになります。

typing --- 型ヒントのサポート — Python 3.8.3 ドキュメント

pythonでも匿名クラスはありますが、ここで匿名クラスにしてコードが読みづらかったので、別途クラスを定義しました。

from typing import Union
from toiopy.data import (
  PositionIdType,
  StandardIdType,
  IdMissedType
)

class Hoge:
  DataType: Union[
    PositionIdType,
    StandardIdType,
    IdMissedType
  ]

これでPositionIdType、StandardIdType、IdMissedTypeのいずれかの型が入るDataTypeとなります。

また、似たケースでNullを許容する場合もあります。

private timer: NodeJS.Timer | null = null

この場合、Union型ではなく、Optional型を利用することで同様のことが実現できます。(pythonなのでNullではなく、Noneですが)

typing --- 型ヒントのサポート — Python 3.8.3 ドキュメント

from typing import Optional
from toiopy.data import TImer

class Fuga:
  timer: Optional[TImer] = None

typescriptにはinterfaceという概念があり、完全に同じ振る舞いに書き換えることはできないですが、代替することができました。

Enumの機能API

export enum Note {
  C0 = 0, CS0,  D0,  DS0,  E0,  F0,  FS0,  G0, GS0, A0, AS0, B0,
  C1,     CS1,  D1,  DS1,  E1,  F1,  FS1,  G1, GS1, A1, AS1, B1,
  C2,     CS2,  D2,  DS2,  E2,  F2,  FS2,  G2, GS2, A2, AS2, B2,
  C3,     CS3,  D3,  DS3,  E3,  F3,  FS3,  G3, GS3, A3, AS3, B3,
  C4,     CS4,  D4,  DS4,  E4,  F4,  FS4,  G4, GS4, A4, AS4, B4,
  C5,     CS5,  D5,  DS5,  E5,  F5,  FS5,  G5, GS5, A5, AS5, B5,
  C6,     CS6,  D6,  DS6,  E6,  F6,  FS6,  G6, GS6, A6, AS6, B6,
  C7,     CS7,  D7,  DS7,  E7,  F7,  FS7,  G7, GS7, A7, AS7, B7,
  C8,     CS8,  D8,  DS8,  E8,  F8,  FS8,  G8, GS8, A8, AS8, B8,
  C9,     CS9,  D9,  DS9,  E9,  F9,  FS9,  G9, GS9, A9, AS9, B9,
  C10,    CS10, D10, DS10, E10, F10, FS10, G10,
  NO_SOUND
}

このコードをみて、OH...ってなりました。
pythonだとインデントが必要になるので、これ全部変換するのか。。。。

ありました。解決する方法が。

Enumに機能APIがあり、文字列からEnum型を定義することが可能です。
enum --- 列挙型のサポート — Python 3.8.3 ドキュメント

Animal = Enum('Animal', 'ANT BEE CAT DOG')

class Animal(Enum):
     ANT
     BEE
     CAT
     DOG

は同義になります。
また、C0=0となっているので、同様に開始する要素の値を設定することもできます。(defaultは1になります)

これらを踏まえてtoio.jsで定義されているEnumpythonで実現した内容は以下の通りです。

names = """
C0,CS0,D0,DS0,E0,F0,FS0,G0,GS0,A0,AS0,B0,
C1,CS1,D1,DS1,E1,F1,FS1,G1,GS1,A1,AS1,B1,
C2,CS2,D2,DS2,E2,F2,FS2,G2,GS2,A2,AS2,B2,
C3,CS3,D3,DS3,E3,F3,FS3,G3,GS3,A3,AS3,B3,
C4,CS4,D4,DS4,E4,F4,FS4,G4,GS4,A4,AS4,B4,
C5,CS5,D5,DS5,E5,F5,FS5,G5,GS5,A5,AS5,B5,
C6,CS6,D6,DS6,E6,F6,FS6,G6,GS6,A6,AS6,B6,
C7,CS7,D7,DS7,E7,F7,FS7,G7,GS7,A7,AS7,B7,
C8,CS8,D8,DS8,E8,F8,FS8,G8,GS8,A8,AS8,B8,
C9,CS9,D9,DS9,E9,F9,FS9,G9,GS9,A9,AS9,B9,
C10,CS10,D10,DS10,E10,F10,FS10,G10,
NO_SOUND
"""
Note = Enum(
    "Note",
    names,
    module=__name__,
    qualname="toiopy.data.Note",
    start=0,
)

Enumの機能APIの定義は以下の通りです。

Enum(
  value='NewEnumName',
  names=<...>, *, 
  module='...', 
  qualname='...', 
  type=<mixed-in class>, 
  start=1
)

namesの指定の仕方はいくつかあり、空白またはカンマで区切った文字列でも構いません。

'RED GREEN BLUE' | 'RED,GREEN,BLUE' | 'RED, GREEN, BLUE'

または名前のイテレータで指定もできます:

['RED', 'GREEN', 'BLUE']

または (名前, 値) のペアのイテレータでも指定できます:

[('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]

またはマッピングでも指定できます:

{'CHARTREUSE': 7, 'SEA_GREEN': 11, 'ROSEMARY': 42}

利用シーンは限定的かもしれませんが、Enumには機能APIがあると頭の片隅に置いておいて良さそうです。

mypy

ここまで、typescriptの型の表現をpythonで実現するにはということで話してきましたが、type hintは、あくまでヒントであって実行時には影響を与えません。(文法エラーで失敗するわけではない)
ここで、型のチェックはmypyを使って型の状態を静的にチェックします。

インストール

導入するのは簡単です。 こちらのコマンドを実行してください。
これまでと同様にvenvをactivateしてから実行してください。

pip install mypy

型チェックする

これで型チェックが可能となります。

mypy <ディレクトリ名>

実際に私自身が作成したクラスなどは問題なく型チェックができたのですが、pipなどでinstallしたモジュールでエラーが出る場合があります。
それは型定義ファイルがないから発生するのですが、その場合はmypy.iniを作成することで対応可能です。

[mypy-Adafruit_BluefruitLE.*]
ignore_missing_imports = True

この場合は、BLE操作でinstallしたAdafruit_BluefruitLEに対する型チェックを行わないようにしています。

また、一部だけ型チェックを無効化にする方法もあります。
無視したいコードの行末尾に#type: ignoreのコメントをつけます。

from typing import Optional
from toiopy.data import TImer

class Fuga:
  timer = None #type: ignore

本来はこういった使い方はあまり良くないとは思いますが、必要になったケースもありました。
この投稿にも書いたEnumの機能APIです。
私が使っていたバージョンでは機能APIを用いるとうまく対応されておらず、エラーになるので、このような対応をしました。

# mypyの不具合で機能APIのEnumはtype ignoreとする
Note = Enum(  # type: ignore
    "Note",
    names,
    module=__name__,
    qualname="toiopy.data.Note",
    start=0,
)

ちなみに、まだ試していないですが、mypyのEnum機能APIの対応を行うIssueはclosedに変わりました。

Support functional API for Enum creation · Issue #2306 · python/mypy · GitHub

mypyのバージョンをあげたら解決するかもしれません。

まとめ

typescriptからpythonに置き換えることで型について学ぶことが多かったです。

  • Union型/Optional型
  • 文字列やリストなどから列挙型のEnumを定義する機能API
  • mypyを使った型チェックと例外対応の仕方

エンジニアとしての始まりがJavaなどの静的型言語から始めた人間としては、型がある方が安心してしまうのは習性のようなものかなと思いました。