0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Typevarの変性について

0
Last updated at Posted at 2026-08-07

概要

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とは無関係)

他言語における変性

共変や反変の概念は、他言語でも実現されています。

KotlinTypeScriptでは、共変をout, 反変をinとして利用できます。また、Scalaでは、共変を+、反変を-として利用できます。

out, inの方が実現したいこととイメージが近いので、T_co, T_contraT_out, T_inと書いて利用するのもよいかと思います。

各言語における変性の扱いについては、以下のリンクを参照してください。

共変で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_coDogが代入され、以下のようになります。

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)で定義可能ということになります。

逆に、以下のように定義できるwriteHom(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)に割り当てられます。
writeHom(Dog, None)であったので、Consumer(Dog)で定義可能ということになります。

逆に、readHom(None, Dog)であったので、Hom(Dog, None)と型が一致しないので、定義不可ということになります。

履歴

2026/08/07 : 一部修正
2026/08/23 : 理解不足による説明がおかしかった箇所の修正、圏論との対応性について追記

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?