概要
typingモジュールのTypeVarが変性に関するフラグを持っていることがわかったので、挙動を確認してみます。
簡単な利用例
変性に関しては、以下のWikipediaの記事を参照してください。
用途について
変性はジェネリック型を定義する際に指定できます。
Pythonにおける用途は以下の通りになります。
| 変性 | 用途 |
|---|---|
| 非変 | Read & Write |
| 共変 | Read only |
| 反変 | Write only |
非変
まず、以下の様に何も指定しない場合は「非変」になります。
T = TypeVar('T')
非変なので、read, writeの両方が定義できます。
class I(Generic[T]):
def __init__(self, value: T):
self.value = value
def read(self) -> T:
return self.value
def write(self, value: T) -> None:
self.value = value
共変
共変を利用したい場合は、以下のようにします。
T_co = TypeVar('T_co', covariant=True)
共変なので、writeを定義しようとするとエラーになります。
class F(Generic[T_co]):
def __init__(self, value: T_co):
self.value = value
def read(self) -> T_co:
return self.value
def write(self, value: T_co) -> None: # 定義しようとするとエラー
self.value = value
反変
反変を利用したい場合は、以下のようにします。
T_contra = TypeVar('T_contra', contravariant=True)
反変なので、readを定義しようとするとエラーになります。
class G(Generic[T_contra]):
def __init__(self, value: T_contra): # これはOK
self.value = value
def read(self) -> T_contra: # 定義しようとするとエラー
return self.value
def write(self, value: T_contra) -> None:
self.value = value
他言語のextendsを実現するには
他言語のジェネリックでは、ジェネリック型の継承関係をextendsなどを利用して指定します。
これをTypeVarで実現するには、以下のようにします。
Baseの定義は後述しますが、この場合、Baseを継承した型、又はBase自身のみ許可するということになります。
T_base = TypeVar('T_base', bound=Base)
具体的な利用例は以下の通りです。
class Base:
pass
class Derived(Base):
pass
class Another:
pass
class H(Generic[T_base]):
def __init__(self, value: T_base):
self.value = value
h_base: H[Base] = H(Base()) # OK (Base自身)
h_derived: H[Derived] = H(Derived()) # OK (Baseを継承した型)
h_another: H[Another] = H(Another()) # エラー (Baseとは無関係)
他言語における変性
共変や反変の概念は、他言語でも実現されています。
KotlinやTypeScriptでは、共変をout, 反変をinとして利用できます。また、Scalaでは、共変を+、反変を-として利用できます。
out, inの方が実現したいこととイメージが近いので、T_co, T_contraをT_out, T_inと書いて利用するのもよいかと思います。
各言語における変性の扱いについては、以下のリンクを参照してください。
- Kotlin : https://kotlinlang.org/docs/generics.html#declaration-site-variance
- TypeScript : https://www.typescriptlang.org/docs/handbook/2/generics.html#variance-annotations
- Scala : https://docs.scala-lang.org/tour/variances.html
共変でwrite、反変でreadが定義できない理由
以下の圏を考えます。
圏論の表現なのでわかりにくいですか、Hom(A, B)は、引数の型がAであり、戻り値の型がBであるような関数の集合と考えてください。
-
Typ: プログラムの型の圏- 対象 :
None,str,list, ... - 射 : 型の包含関係 (継承関係)
- 合成 : 包含関係の推移 (継承関係の推移)
- 対象 :
-
Set: 型(クラス)のメソッドの圏- 対象 : 関数(メソッド)の集合
-
Hom(None, str),Hom(list, str)のような関数の集合
-
- 射 : 関数変換
- 以下のような関数の集合の変換
- $Hom(None, int) \to Hom(None, str)$ (共変)
- $Hom(int, None) \to Hom(str, None)$ (反変)
- 以下のような関数の集合の変換
- 合成 : 関数合成
- 対象 : 関数(メソッド)の集合
Pythonで具体的な例を見ていきます。
Type圏の対象としてAnimal, Dogを考えます。
この2つの型(クラス)には、継承関係 Dog extends Animalが存在します。
class Animal:
pass
class Dog(Animal):
pass
共変
関手 $Producer : Typ \to Set$を以下の様に定義します。
T_co = TypeVar('T_co', covariant=True)
class Producer(Generic[T_co]):
pass
Producer(Dog)にreadが定義できるがwriteが定義できないことを見ていきます。
readの型をHom(None, Dog)とします。
ここで、Homの第一引数は、固定の型にする必要があるため、外部の値を渡す場合は、__init__の引数として渡します。
今回は、何も渡さないのでNoneになります。
Pythonで実装すると以下のような感じになります。
def __init__(self):
pass
def read(self) -> T_co:
return self.value
ここでProducer(Dog)のインスタンスを生成すると、内部的には、T_coにDogが代入され、以下のようになります。
class Producer(Dog):
def __init__(self):
pass
def read(self) -> Dog:
return self.value
一般的に共変関手の場合、Typ圏のAという型は、Set圏のHom(a, A)に割り当てられます。
Producerは共変関手として定義しているので、Dogは、Producer(Dog)において、Hom(None, Dog)に割り当てられます。
readの型はHom(None, Dog)であったので、Producer(Dog)で定義可能ということになります。
逆に、以下のように定義できるwriteはHom(Dog, None)であり、Hom(None, Dog)と型が一致しないので、定義不可ということになります。
def write(self, item: Dog) -> None:
self.value = item
反変
今度は反変性について見ていきます。
関手 $ Consumer : Typ \to Set $を以下の様に定義します。
T_contra = TypeVar('T_contra', contravariant=True)
class Consumer(Generic[T_contra]):
pass
Consumer(Dog)にwriteが定義できるがreadが定義できないことを見ていきます。
writeの型は、上述の通り Hom(Dog, None)であり、Pythonで以下の様に実装できます。
def write(self, item: Dog) -> None:
self.value = item
Consumer(Dog)としてインスタンスを生成すると、内部的には以下のような感じになります。
class Consumer(Dog):
def write(self, item: Dog) -> None:
self.value = item
一般的に反変関手の場合、Typ圏のAという型は、Set圏のHom(A, a)に割り当てられます。
Consumerは反変関手なので、Dogは、Consumer(Dog)において、Hom(Dog, None)に割り当てられます。
writeはHom(Dog, None)であったので、Consumer(Dog)で定義可能ということになります。
逆に、readはHom(None, Dog)であったので、Hom(Dog, None)と型が一致しないので、定義不可ということになります。
履歴
2026/08/07 : 一部修正
2026/08/23 : 理解不足による説明がおかしかった箇所の修正、圏論との対応性について追記
参考
- https://kdot.co.jp/kdotdev/python-covariance-contravariance-variance
- https://docs.python.org/3/library/typing.html
- https://peps.python.org/pep-0484/#covariance-and-contravariance:~:text=By%20convention%2C%20it%20is%20recommended%20to%20use%20names%20ending%20in%20_co%20for%20type%20variables%20defined%20with%20covariant%3DTrue%20and%20names%20ending%20in%20_contra%20for%20that%20defined%20with%20contravariant%3DTrue.
- https://zenn.dev/yuchiki/articles/type-variance