1. はじめに
皆さんは Python でコーディングする際に、型を意識することはありますか?
Python はそういったことを意識せずにある程度コーディング出来てしまうので、多くの初学者は型やオブジェクトを意識せずにコーディングしているのではないでしょうか?
本記事はそういった方々がデータ型について関心を持って、日々のコーディング活かしてもらうことを目的としています。
対象者
- Pythonからプログラミングを始めた人
- データ型ってなんですか?な人
- Pythonで型推論を使いたい人
- オブジェクト指向と型付けについて取っ掛かりが欲しい人
環境
- Python:
3.10.4
- mypy:
0.971
- pyright:
1.1.268
2. 型とは
2.1. 導入
型とは、取っ掛かりやすい言い方をするとint
とかstr
とかです。
この単語は下記のようにキャストする際に見かけると思います。
hoge = str(1)
print(hoge)
>>> "1"
このキャストで int
型から str
型に変換されました。
こういった int
型や str
型などの分類を総称して「型(データ型)」と言います。
2.2. データ型って具体的にどうなっているの?
まずここで言うデータとは、メモリの 2 進数情報を変換した時の解釈物です。
01000001(2 進数) -> 65(10 進数)
例えば C 言語の基本データ型であるchar
は 1 バイト(-128 ~ 127 の値)記憶できます。
(英数字などの1バイト文字を1字記憶できます。上記例だとA
が割り当てられています。)
Python でも、もちろん同じ原理です。(Python3 からはUnicodeが標準です)
# Aのコードポイントを2進数で出力
bin(ord('A'))
>>> '0b1000001'
このような解釈物を分類して、集合のように扱っているのが「データ型」になります。
3. データ型の種類について
データ型には、色々と種類があります。同じデータ型であれば代替性や等価性があります。
例えば、下記のようなadd
関数は int 型であれば、どのような値が来ても足し算されます。
def add(x:int, y:int) -> int:
return x + y
ここに、例えばadd("1",1)
を入れるとエラーになります。
(Python は実行時までこれがエラーかどうか分かりません。詳しくは型付けで後述します。)
add("1",1)
>>> Traceback (most recent call last):
>>> File "<stdin>", line 1, in <module>
>>> TypeError: can only concatenate str (not "int") to str
これは異なる型同士の連結を Python が許容していないからです。こういった機構は「型安全」と呼ばれるものの一種です。
また、データ型には「組み込み型(プリミティブ型)」と「複合型(コンポジット型)」といった分類があります。
この 2 つはよく対比されますが、対照的な概念ではないです。
どちらかというと、ラッパーとかそういう組み込み型をカスタマイズしたイメージに近いです。
3.1. 組み込み型
各プログラミング言語が規定している基本的なデータ型。(例. char, int, float, bool 等)
そのため、ある言語では複合型に分類されるデータ型も別の言語では組み込み型になる可能性があります。
3.2. 複合型
プリミティブ型および複合データ型の構造的または再帰的な組み合わせ(コンポジション)で形成されるデータ型
(wikipedia参照)
構造体や、配列やリストなどはそれらに該当することが多いです。
3.3. Python の組み込み型、複合型は?
組み込み型とコンテナデータ型(複合型)があります。
また、Numpy
のndarray
などの外部ライブラリが提供するデータ構造や自前で実装したデータ構造なども複合型に該当します。
そして、これらすべてのデータ型はオブジェクトからできています。
興味があればPython のオブジェクトとクラスのビジュアルガイドを読んでみてください。
4. 型付けについて
先ほど、「Python は実行時までこれがエラーかどうか分かりません。」といった話をしました。
これは Python がインタプリタを介してデータ型を解釈するからです。
こういったプログラミング言語がデータ型を解釈することを「型付け」といいます。
分類としては、大きく 2 観点あります。
(名前的型付けと構造的型付けにはポリモーフィズムの章で触れます)
- 動的/静的型付け
- 強い/弱い型付け
「動的」だから良いとか「強い」から良いとかはありません。
個人やチームの文化・価値観などに依存します。
4.1. 動的/静的型付け
ここでは便宜上よく使われている処理方式で動的/静的を区別しています。
※ インタプリタ言語でもコンパイルが提供するような静的解析をサポートしているものはあるし、コンパイラ言語でもインタプリタのような逐次実行をサポートしているものもあります。昨今、静的/動的の垣根が薄くなってきているような印象を受けます。
静的型付け
コンパイル時(実行前)に型が決まる(コンパイラ言語)
- Java, C, C#, Go, Rust 等
- コンパイラ言語は実行前にデータ型が正しいかを判断できる。
- コンパイルから実行までのハードルが高い反面、高速に動作する。
動的型付け
実行時に型が決まる(インタプリタ言語)
- Python, Ruby, Perl, PHP 等
- インタプリタは実行時までデータ型が分からない。
- 実行が気軽に行える反面、機械語に逐次翻訳されるので速度は遅め。
現在では、動的型付け・静的型付けの各特徴を使い分ける漸進的型付け(Gradual Typing)の機能を導入している言語もあります。
(詳細は「What is Gradual Typing」や「What is Gradual Typing: 漸進的型付けとは何か」がオススメです)
漸進的型付け
実行前に型推論される(決まるわけではない)
- Python, TypeScript 等
- 現状、動的型付け言語に型推論を導入して実現するケースが多い。
- C 言語の auto 等は静的型付け言語に動的な特徴が足されていたりします。
- Python は型アノテーション機能とサードパーティの型解析ツール(mypyやpyright)を利用することで実現します。
- Python だと型アノテーションに強制力がなかったりするし、TypeScript だと型が表現されないと any 型になったりと、言語によって実現範囲が異なります。
※ TypeScript
が静的型付け言語であると言われることも多いですが、個人的には any 型が強すぎたり、js にトランスパイルされる辺りがモヤるのでこちらに区分しています。
まとめると、Python は「動的型付け」か「漸進的型付け」言語にあたります。
どちらで書くべきかと言われると、漸進的型付け言語として型アノテーションを賢く使うべきです。
例えば、ハンガリアン記法のようにデータ型を変数名に含む必要がなくなります。
(エディタのツールチップ等が使えないプロジェクト等を除く)
# NG
drink_str_list = []
# OK (Python3.8 は List[str])
drinks: list[str] = []
で、右辺で型が明らかな場合は、わざわざ型を書く必要はありません。
# NG
id: int = 1
# OK
id = 1
# OK (明らかだが型そのものが大事な場合)
DEFAULT_ID: Final[int] = 1
ちなみに、Final
などの typing に追加されている型アノテーションを利用する場合、「mypy」を使用するほうが無難です。「Pylance(Pyright)」だと、PEP で定義されたエラーやワーニングが出ないこともあります。
(PEP-0591の内容はmypyを前提に議論されている)
ただ Pylance(Pyright)は mypy よりも便利な場面も多いので、私は Pylance(Pyright)を使っています。
Python は型アノテーションだけを提供して後はサードパーティー任せているような節があるので、厳密に型判定したい場合は別言語を選ぶべきですね。
4.2. 強い/弱い型付け
強い、弱いの定義は割りとまちまち(この記事を参照)
今回は「型安全」に触れておきたいため、言及します。
強い型付け
型の安全性が言語仕様で保たれているもの
- Java, C#, Python, Ruby 等
- 型安全: 型によるバグの検知ができる(静的・動的に関係なく)
- null 安全とは限らない。(Java や Python 等は許容されている)
- Python は型を定義しない場合は null(
None
)を許容すべきではないです。(意図しないデータ型を混入させたくないため)
- Python は型を定義しない場合は null(
弱い型付け
型の安全性が保証されないもの
- C, C++, JavaScript 等
- 型安全が保たれていない
型安全が保たれていないとどうなるかは下記の JavaScript の例を見ると分かりやすいかと思います。
console.log(1 + "1");
>>> 11
このように、int
型と string
型で意図しない結合ができてしまいます。
Python だとどうかと言うと、先ほども触れましたが下記のようなエラーになります。
print(1 + "1")
>>> 1 + "1"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
余談になりますが、Python がこの観点で型安全なんですが注意すべき点があります。
例えば下記のように int
型と bool
型の計算は可能なんですよね。
print(1 + True)
>>> 2
これはどういうことかというと、bool 型は int 型のサブクラスだからなんですよね。
(True は 1, False は 0 で計算されます。)
print(issubclass(bool, int))
>>> True
これはint
型に限った話ではなく、str
型やlist
型等にも当てはまります。
また、Python のオブジェクトには__bool__
という特殊メソッドがあり、これを定義するとオブジェクトに bool
の規定値をセット出来たりします。
このように、型安全とされている言語にも言語仕様による落とし穴とか違いがあるので気をつけましょう。
5. OOP におけるデータ型
オブジェクト指向プログラミング(Object-Oriented Programming, OOP)とは、「オブジェクト」という概念に基づいたプログラミングパラダイムの一つである。
(Wikipedia参照)
OOP を構成する要素は以下になります。
- クラス(継承、抽象化)
- カプセル化
- ポリモーフィズム
オブジェクト指向プログラミングもデータ型と密接な関係があります。
(class で定義されるオブジェクトは基本的に複合型のデータ構造を持ちます。)
今回は Python における OOP のクラスとポリモーフィズムをデータ型の観点から解説します。
5.1. クラス
クラスとは「データと処理をまとめて隠蔽し、たくさん作る」ための機能です。
ここで議論することは主に 2 点あります。
- 構造体とクラスの違い
- 抽象クラスとインターフェースの違い
まず、クラスと構造体についてですが、以下のような違いがあります。(言語仕様によって若干異なります。)
クラス
- 参照型: データの参照情報をもっている。
- 継承や抽象化ができる。
構造体
- 値型: データの値情報を直接もっている。
- 継承や抽象化ができない。
後、違いというより個人の所感ですが、構造体はあくまでデータ構造に重きをおいているのでメンバ変数は基本的に公開し、メソッドにはあまり複雑なものを持たせないイメージがあります。
Python では、クラスと構造体に違いはありません。というより構造体はなく、クラスが構造体も兼ねています。
そのため、Python では、クラスと構造体を同じ文脈で使わないように注意する必要があります。
(例えば、構造体としてデータ構造を定義したクラスに処理をもりもり付けたり、継承・抽象化することはやめましょう。)
こういった決め事やルールは個人やチームである程度決めておきましょう。
※ 余談ですが、namedtuple
で構造体のような振る舞いを定義できます。
from collections import namedtuple
from typing import NamedTuple
# おすすめのnamedtuple宣言
class LoginForm(NamedTuple):
id: str
password: str
# こちらだと型が書けないため、ただの名前付きtupleとして使う場合以外おすすめしない
LoginForm = namedtuple(
"LoginForm",
[
"id",
"password",
],
)
次に、抽象クラスとインターフェースについてですが、クラスを「抽象化」や「継承」することでクラスを分類することもできます。
今回は、抽象化に着目して説明します。抽象化の方法として抽象クラス(abstract
)を定義する場合とインターフェース(interface
)を定義する場合があります。
それぞれの違いは以下になります。
抽象クラス
「動物と聞かれると、こういうイメージです」を定義する感じです。
- 処理が書ける(オーバーライドで書き換える前提のため)
- 継承元の抽象クラスは 1 つのみ
インターフェース
「動物にはこういう特徴がある」といったスペックを定義する感じです。
- 処理が書けない
- インターフェースを複数組み合わせたりできる(mix-in)
- 多重継承可(振る舞いが描かれるわけはないため)
Python には、インターフェースは存在せず、全て抽象基底クラスで表現することになります。
昨今の言語の潮流も踏まえ、インターフェース的な利用をおすすめします。
(クラスなし/インターフェースありの仕様を最近流行りの言語でよく見かけるため)
インターフェースで書く際も、簡単な抽象化に留めて mix-in や多重継承は避けましょう。
複雑な実装や、抽象クラス的な考え方はそれが出来てからあくまで手段として持っているくらいでちょうどよいです。
5.2. ポリモーフィズム
ポリモーフィズムは日本語で「多態性」、「多相性」等と呼ばれるものです。
簡単に説明すると、「クラスの類似するメソッド(関数)を同じような動作としてみなす」ということです。
よくあるAnimal
クラスを例に確認してみましょう。
(ここでは Java で記載します。Python の例は後で記載します。)
abstract class Animal {
abstract String bark();
}
動物の抽象クラスです。これらを継承したクラスを作成してみましょう。
class Dog extends Animal {
String bark(){
return "ワン";
}
}
class Cat extends Animal {
String bark(){
return "ニャー"
}
}
これらクラスのbark
メソッドは動物(Animal
)の種類に問わず必ず持つ動作です。よってこれらのメソッドを使う側は動物の種類に問わず、鳴いてもらうことが可能です。
ここでは、飼い主(PetOwner
)が笛を吹く(blowWhistle
)と鳴く処理を実装しています。
class PetOwner {
void blowWhistle(Animal animal){
System.out.println(animal.bark());
}
}
こんな感じで実装できるので、わざわざ「Dog のとき」、「Cat のとき」のような処理を書く手間が省けます。
以上がポリモーフィズムのよくある具体的な説明なんですが、少し型を絡めた解説をします。
ポリモーフィズムを実現する方法は、静的型付け言語と動的型付け言語で少々異なります。静的型付け言語では下記のように継承元のクラスやインターフェースの参照情報が必要になります。
多くの静的型付け言語では継承やインターフェースを実装していることを明示する必要があります。これを Nominal Subtyping と呼びます。Nominal Subtyping の場合、前もって宣言されていなければ期待される振る舞いを持っていてもコンパイル時にエラーになってしまいます。
(RubyKaigi 2016 基調講演のレポート参照)
これに対し、動的型付け言語では継承元のクラスやインターフェースの参照情報を必要とせず、メソッドや関数だけを検証します。これを「ダックタイピング」と呼びます。
以下に2つの違いをまとめます。
Nominal Subtyping(名称的部分型付け): 親クラス情報が同じ場合、同じメソッドと認識される。
- ポリモーフィズムの縛りが強いため、厳密に実装できる。
- 静的型付け言語の多く(動的型付け言語でも実現できる場合があります)
ダックタイピング: どのようなクラスでも、特定のメソッドがあれば同じと認識される。
- ポリモーフィズムの縛りが少ないため、柔軟に実装できる。
- 動的型付け言語の多く(静的型付け言語でもライブラリで実現できる場合があります)
Go のように静的型付け言語でありながらダックタイピングのようなデータ解析ができる言語もあります。
これを Structural Subtyping(構造的部分型付け) と呼びます。
Python では、以下の方法でポリモーフィズムを実現できます。
-
同じ名前のメソッドを作成する。(ダックタイピング)
class Cat: def bark(self): return "ニャー" class Dog: def bark(self): return "ワン" def blow_whistle(f) -> None: print(f.bark()) blow_whistle(Cat()) blow_whistle(Dog())
>>> ニャー >>> ワン
-
abc.ABC
を使い、抽象クラスを作成する。(名称的部分型付け)from abc import ABC, abstractmethod class Animal(ABC): @abstractmethod def bark(self): ... class Cat(Animal): def bark(self): return "ニャー" class Dog(Animal): def bark(self): return "ワン" def blow_whistle(f: Animal) -> None: print(f.bark()) blow_whistle(Cat()) blow_whistle(Dog())
>>> ニャー >>> ワン
-
typing.Protocol
を使い、同じ名前のメソッドを作成する。(構造的部分型付け)from typing import Protocol class Animal(Protocol): # ここのreturnの型を外すとCat()、Dog()で型の警告が出ます。※1 def bark(self) -> str: ... class Cat: def bark(self) -> str: return "ニャー" class Dog: def bark(self) -> str: return "ワン" def blow_whistle(f: Animal) -> None: print(f.bark()) blow_whistle(Cat()) blow_whistle(Dog())
>>> ニャー >>> ワン
※1 型の警告を外すと Pylance(pyright)だとこういう感じのエラーがでます。
Argument of type "Cat" cannot be assigned to parameter "f" of type "Animal" in function "blow_whistle" "Cat" is incompatible with protocol "Animal" "bark" is an incompatible type Type "() -> str" cannot be assigned to type "() -> None" Function return type "str" is incompatible with type "None" Type cannot be assigned to type "None"Pylance(reportGeneralTypeIssues)
この 3 つのうちどれを使えばよいかという話ですが、1 はインターフェースが不要なため推奨できません。
そのため、2 か 3 を使うことになるのですが、おすすめは 2 かなといった印象です。
理由は、3 のtyping.Protocol
は型チェッカー依存で意図したチェックが出る保証がサードパーティーのライブラリに委ねられてしまうためです。
実装にある程度柔軟性を持たせたい場合や、チームで型チェッカーを統一している場合は選択肢になりそうです。
2 は融通のない形ですが、Python は動的型付け言語なのである程度かっちり定義できる箇所は定義してしまった方が間違いないかと思います。
6. おわりに
データ型について最低限知っておいてほしいことを解説しました。
最後の方はデータ型というより書き方の話になってしまいましたが、最近はtyping
モジュールの更新が盛んなので簡単に触れました。
最近では OOP より関数型プログラミングみたいな流れもあり、データ型というのはより重要な要素になってくると思うので Python エンジニアの方にも興味をまず持ってもらいたいなと思っています。
参考
- python.org: https://www.python.org/
- wikipedia: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
- Python のオブジェクトとクラスのビジュアルガイド: https://postd.cc/pythons-objects-and-classes-a-visual-guide/
- What is Gradual Typing: https://wphomes.soic.indiana.edu/jsiek/what-is-gradual-typing/
- What is Gradual Typing: 漸進的型付けとは何か: https://qiita.com/t2y/items/0a604384e18db0944398
- PEP-591: https://peps.python.org/pep-0591/#reference-implementation
- C#は強い型付けの言語?それとも弱い型付けの言語?: https://matarillo.hatenadiary.jp/entry/20121017/p1
- まつもとゆきひろさん、楽しくプログラミングができる Ruby3 の型の提案: https://gihyo.jp/news/report/01/rubykaigi2016/0001