toioをpythonで動かす:番外編(型で学んだこと)
はじめに
前回は「toioをpythonで動かす:BLE操作」を行いました。
今回はちょっと番外編です。
これまでtoio.jsのコードを読みながら、これってpythonで書くとどうなるんだ?と考えながら書いていました。
私自身pythonのAPIドキュメントを全て読んでいたわけではなかったので、割と新しい発見がありました。
型で学んだこと
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で定義されているEnumをpythonで実現した内容は以下の通りです。
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( 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に置き換えることで型について学ぶことが多かったです。
エンジニアとしての始まりがJavaなどの静的型言語から始めた人間としては、型がある方が安心してしまうのは習性のようなものかなと思いました。