はじめに
よく、Pythonの関数の引数へ型アノテーションをするとき、tuple[str, ...]
でもlist[str]
でも受け取れるようにしたいということがあると思います。
そのときに
def foo(a: list[str] | tuple[str]): ...
のようにUnion
を使って書くと冗長感があるし、他のシーケンス型を受け入れられなくなるということがあります。
しかしSequence[str]
を使ってアノテーションしようとすると、下記issueにあるように、「Sequence[str]
はstr
」であるので、ただの文字列型も受け入れられてしまうという欠点があります。
from collections.abc import Sequence
def foo(a: Sequence[str]):
"""`('hoge',)`や`['hoge', 'fuga']`を渡して欲しいが、`'hoge'`のような単なる文字列は渡してほしくない"""
...
foo(("",)) # Valid
foo(()) # Valid # 空tupleは長さ0のT型シーケンスと判断されるため
foo([""]) # Valid
foo([]) # Valid # 空tupleと同じ理由
foo("") # Validになってしまう。本来咎めてほしい
その解決策として提案されたのが、useful_types.SequenceNotStr
です。
useful_types
について
typing
、typeshed
やmypy
のコアコントリビューターが参画し、「痒いところに手が届く」型ヒントシンボルを提供しているリポジトリです。
SequenceNotStr
の使用例
useful_types
をインストールしてimportして、引数にアノテーションするとこのようになります。
from useful_types import SequenceNotStr
def foo(a: SequenceNotStr[str]): ...
foo("") # Invalid
foo(("",)) # Valid
foo(()) # Valid
foo([""]) # Valid
foo([]) # Valid
foo({""}) # Invalid
foo({"": ""}) # Invalid
foo(iter([""])) # Invalid
SequenceNotStr[str]
と型付けされた変数に、ただの文字列を渡すと静的型チェッカーはエラーを表示します。
しかし文字列のリストやタプルであれば、エラーを表示しません
また、「配列」であっても
-
set[str]
のように順番を考慮せずindex
がないもの -
dict[str, Any]
のように__getitem__
にスライスを受け取れないもの -
Iterator[str]
などのイテレータ(Sequence
の基底クラス)
がアサインされると、静的型チェッカーはエラーを表示します。
ただし、コーダーが直接インスタンス化するイテラブルはたいていlist
やtuple
で、これらは組み込み型(組み込み関数)なので、「キャストができない」ということで困ることはほとんどないでしょう。
実装
なぜこのようなことができるかは、ソースコード上にコメントがされています
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
class SequenceNotStr(Protocol[_T_co]):
...
def __contains__(self, value: object, /) -> bool: ...
...
str.__contains__
がobject
を受け入れられない(typeshed
でも実行時でも)ので、これは期待通りの働きをする
まとめ
長年(typing
にissueが投稿されたのは2016年7月)問題となっていた「Sequence[str]
はstr
」はuseful_types.SequenceNotStr
によって一つの解決策がもたらされました。
一方、現在ではtyping.Literal
もあるので、渡したい文字列種類が限定されるのであれば、Sequence[Literal[...]]
を使うことで、どんな文字列を使うべきかを型アノテーションで伝えることができるので、よりコードをロバストにできると考えています。