マイケル・J・サリバン氏は、PEP 827について議論するために、言語サミットに参加しました。PEP 827は、Pythonの型注釈に関する多くの改善提案を含むものです。Python言語サミットにおいて、マイケル氏は特に、型注釈がどのように格納されるかという点に焦点を当てたいと考えていました。
マイケル氏は、PEP 827について説明する際に、「全く異なるプログラミング言語」の例を挙げました。彼は、TypeScriptで書かれたオブジェクト関係マッパー(ORM)ライブラリであるPrismaの例を取り上げました。Userモデルで`findMany`を呼び出すと、入力パラメータの値に基づいて戻り値の型が推論されます。例えば、フィールドのサブセットを選択する場合:
```typescript
const user = await prisma.user.findMany({
select: {
name: true,
email: true,
posts: true,
},
});
```
このクエリによって得られるTypeScriptの型は、次のようになります。
```typescript
{
email: string;
name: string | null;
posts: {
id: number;
title: string;
content: string | null;
authorId: number | null;
}[];
}[]
```
PEP 827によって、Pythonの型注釈も同様に機能する可能性があります。関数の入力パラメータの型から、戻り値の型が推論できるようになるかもしれません。
PEP 827のもう一つの活用例は、プログラム的に新しい型を生成することです。例えば、CRUD(Create、Read、Update、Delete)ウェブアプリケーションでは、通常、単一のデータベーステーブルに対して、各CRUD操作ごとに個別のORMモデル定義が必要です。Createでは、デフォルト値のないすべてのプロパティが必要であり、Readでは、モデルのすべての公開プロパティが含まれます。Updateでは、すべてのプロパティがオプションになります(PATCH HTTPメソッドをサポートします)。Deleteでは、通常、モデルは必要ありません。なぜなら、レコードを削除するには、プライマリキーだけが必要だからです。
PEP 827で示されている例では、`secret_name`とオプションの`age`パラメータを持つHeroモデルを使用し、さらに「公開取得」、create、およびupdate操作のための3つのモデルを作成しています。
class HeroBase ( SQLModel ):
name: str = Field( index = True )
age: int | None = Field( default = None , index = True )
class Hero ( HeroBase , table = True ):
id : int | None = Field( default = None , primary_key = True )
secret_name: str
class HeroPublic ( HeroBase ):
id : int
class HeroCreate ( HeroBase ):
secret_name: str
class HeroUpdate ( HeroBase ):
name: str | None = None
age: int | None = None
secret_name: str | None = None
これらのモデルとパラメータをデータベースモデルと常に最新の状態に保つには、多くの注意が必要であり、通常はプログラム的に行うことはできません。そこで、CRUD操作ごとに全体のモデルタイプを変換する型の中に複雑さを包含する方法があるとすればどうでしょうか? PEP 827を使用すると、以下のようなPythonの型を記述でき、その複雑さをPublic、Create、およびUpdateの注釈の中に含めることができます。
class Hero ( SQLModel , table = True ):
id : int | None = Field( default = None , primary_key = True )
name: str = Field( index = True )
age: int | None = Field( default = None , index = True )
secret_name: str = Field( hidden = True )
type HeroPublic = Public[Hero]
type HeroCreate = Create[Hero]
type HeroUpdate = Update[Hero]
PEP 827における型操作の基本的な機能
このアイデアの中核は、Pythonの型システムにいくつかの基本的な機能を追加することです。
条件付き型 ( true_type if bool_type else false_type )。
内包型 ( *[t for t in Iter[iter_t]] )。内包型は、通常の内包と同様に、条件付きのif句を持つことができます。
メンバー型 ( typing.Members[t] は、型のメンバーのイテラブルを返します)。member.nameとmember.typeは、メンバーのプロパティです。
このPEPは「TypeScriptに触発されたものですが、重要な点として[このPEP]はTypeScriptをモデルとしていません」。Michael氏は、PythonとJavaScriptは「非常に異なる言語」であるため、最終的にシステムは「かなり異なるものになった」と述べています。また、このPEPは、採用するために新しいキーワードを必要としないように設計されています。
PEP 827には、マイケルが時間内に説明しきれなかった、新しい演算子、呼び出し可能な特殊な構築、タプルのスライスと長さ、ユニオンの反復処理、属性の型取得、初期化子の検査、そして__init_subclass__デコレータの扱い方など、多くの提案された機能が含まれています。このPEPには、提案されているすべての機能に関する、mypyベースのプロトタイプが含まれています。
型アノテーションはどのように保存するのか?
PEPを紹介した後、マイケルは当初の動機に戻り、実行時に型アノテーションをどのように保存するかについて議論しました。PydanticやFastAPIのような人気のあるライブラリは、実行時に型アノテーションを使用して動作を制御しています。「これは、if、for、およびドット表記のような、Pythonらしい構文を使用したいという私たちの意向によって複雑になりました。」
具体的には、Python自体が実行時に型を評価します。PEP 827に記述されているすべての機能を実装するためには、Pythonが条件文や反復処理を実行した後でも、評価されていない型にアクセスする方法が必要です。現在のドラフトでは、「このPEPは、__annotate__()関数を呼び出すことによって動作する評価器をサポートするように、慎重に設計されています。」
すでに、annotationlibというモジュールがあり、文字列形式(Format.STRING)を持っています。理論的には、annotationlib.get_annotations(..., format=Format.STRING)を呼び出すと、評価されていない型アノテーションが返されるはずです。「しかし、この関数は現在、そのように実装されていません。」代わりに、この関数は「非常に巧妙な処理」を行い、すべてのダンダメソッドをオーバーロードするプロキシオブジェクトを使用します。
イモジェン・ハーゲスは、この問題に関連する新しい「AST」形式を提案するPEP 849の草案を作成しましたが、マイケルは、条件式でSTRING形式が機能するようにする方が、より小さく、シンプルであると考えました。「ここでは、時間、スペース、複雑さのトレードオフがある、驚くほど大きな設計の余地があります。」
PEPで検討された初期のアイデアの一つで、__annotate__()関数に組み込まれたものには、「単純に文字列を保存できないか」というものがありました。最も単純な方法は、__annotate__()関数がVALUEとVALUE_WITH_FAKE_GLOBALSに加えて、STRING型もサポートすることです。__annotate__()関数自体は少し大きくなりますが、もしSTRINGがフォーマットとして渡された場合、ライブラリが複雑な処理を行うことなく、文字列を直接返すことができます。
この方法は高速ですが、アノテーションを冗長に表現するため、より多くのメモリを使用します。チームは、「さらに明白な方法」を検討し、文字列のみを保存することを評価しました。しかし、これだけでは不十分です。型アノテーションはローカル変数を参照する可能性があるため、その評価にはより多くのコンテキストが必要です。「これが、__future__ import annotations (PEP 563)から移行したり、PEP 649を導入したり、__annotate__関数を生成する際に複雑さを追加したりする理由なのです。」
マイケルは、メモリを節約する代わりに計算速度が低下する代替案について詳しく説明しました。「もし、型注釈として文字列があり、__annotate__ のための環境クロージャがある場合、そのクロージャ内で文字列を eval() することで動作させることができます。それはどのようなものになるでしょうか?
STRING のフォーマットのみを実装する __annotate__ 関数を生成しますが、その関数は、型注釈で使用される自由変数のみを含むように設定します。もし、型注釈がローカル変数を参照する場合、注釈は依然として[その変数]に依存し、[型注釈]は[その変数]を直接操作することはありません。[型注釈]は、単に文字列を含む辞書を構築します。
annotationlib には、[その変数]を取り出し、それらを文字列と組み合わせて、適切に評価する関数があります。__annotate__() 関数は、評価された型注釈を要求されたときに、その [annotationlib] 関数を呼び出すことができます。」
このアプローチは、文字列を使用するよりもはるかに少ないメモリを使用しますが、eval() を使用する必要があるため、非常に遅くなります。
議論
シュテファン・ベネルは、PEP 827 を採用することで、提案された条件文や内包表記によって型システムがチューリング完全になるかどうかを質問しました。マイケルは、Python の型システムは「すでにチューリング完全ですが、偶然にチューリング完全である」と答えました。Java は、サブタイプ境界を持つジェネリクスにおいて、この問題に直面しました。そして、「Python は Java と同じジェネリック機能を持っています」。 「型システムがチューリング完全になるように意図されたことはありません」ため、PEP 827 を採用しても、型システムは「意図的にチューリング完全になる」というだけです。
Łukasz Langa氏は、提案されている構文の美学についてコメントし、「スター」を使った内包表記の例を挙げました。彼は、コア開発者がこの構文を見て、反射的に否定的な反応を示すことのないよう注意を促しました。例えば、「ああ、[f-string]、これで型ヒントがさらに使いにくくなる」といった考えにならないように。Łukasz氏は、この機能は「[フレームワーク]が内部的に実装し、型チェッカーが自動的に正しい型を生成できるようにするためのもの」であり、「ユーザーがこのような型を記述したり、ごくまれにしか使用したりするものではない」と明確にしました。
Łukasz氏はまた、文字列を使用した場合のパフォーマンスについてもコメントしました。`from __future__ import annotations` を最初に記述した際、その動機の一つは、文字列が「インターンされる」ことでした。型にインターンを使用すると、「メモリ使用量が少なくなる」と考えられました。しかし、文字列のインターンに使用されるアルファベットには、角括弧([])が含まれておらず、これはPythonの型注釈で頻繁に使用される記号です。この点がPythonで変更できるかどうかという質問に対し、Łukasz氏は「変更することは可能だが、そうすれば他のすべてのユーザーにとってPythonを使いにくくしてしまう」と答えました。なぜなら、この動作は「30年以上も利用されてきた」からです。しかし、Łukasz氏は続けて、「もし`__annotate__()`が常に[文字列]をインターンする場合どうだろうか?」と提案しました。多くの型注釈は、`None`、`str`、`str | None`のように、同じか類似したものが存在すると予想されます。Łukasz氏は、この方法であれば「メモリ使用量はそれほど増えない」と提案し、Michael氏もそれに同意しました。
David Hewitt氏は、非常に複雑な型が存在するTypeScriptでの経験を共有しました。「TypeScriptユーザーがAPIを理解しようとするとき、APIの階層をどんどんクリックしていくことになり」、そしてしばしば問題に遭遇します(「Łukasz氏が説明したように」、そしてそれに伴う「ああ、[f-string]」)。David氏は、この新しい構文がユーザーが頻繁に遭遇するものであるかどうかを懸念していました。
マイケルは、「ORMで特定のメソッドを選択すると、ユーザーは「デフォルトの型」が表示されることになり、それを回避する方法はありません」と告白しました。しかし、「ドキュメントが最初に表示されることを願っています。もし戻り値の型を確認すれば、何か良い情報が表示されるはずです」。彼は、「`select(User)`を使用する場合、特定のプロパティを埋め込むことができるケースもあると思いますが、それは「潜在的に複雑」であり、使用する言語サーバーに依存します」と述べています。
PEPの共同著者であるユリ・セリヴァノフは、「ユーザーが世界の複雑さに気づく瞬間があるでしょう」と述べ、複雑な型アノテーションの必要性を認めました。「現状、世界は型付けされていません」。彼は、「Pythonの型システムは、この言語の表現力に合致していません」と付け加えました。彼はまた、「確かに、一部の人々は混乱するかもしれません」と述べ、この問題を軽減するための方法として、より優れたIDE、言語サーバー、およびドキュメントの改善などを挙げました。