Python♪用語集:意外に難しいコンテナ、シーケンスなどの分類

意外に難しい「コンテナ」「シーケンス型」「イテラブル」「整数型」「ミュータブル」「イミュータブル」などの分類を整理し、覚え方を紹介します。Pythonのチュートリアルやドキュメントを読むとき、これらの用語が理解できないと読み進められません。この機会に頭の中を整理しましょう。

なお、ネット上でも情報が入り乱れていて何が正しいのかわからない状態ですので、できるだけ引用先や理由も説明したいと思います。

0.Python♪ モヤモヤを解消する明快な用語集

「Python♪モヤモヤを解消する明快な用語集」の用語集Top(索引)はこちらです。

モヤモヤを解消する明快な用語集を目指します。例えば言葉の定義が「グレー」なものは「グレー」であると解説します。なお、同じ言葉でも、例えば「Python」と「Java」では定義が違うことがあります。 その場合、「python」での定義を解説します。

1.解説する用語

2.データ型の分類

この記事では代表的な組み込みデータ型である「int, float, complex, list, bytearray, dict, set, tuple, str, range, bytes, frozenset, file object」について分類したいと思います。

まず、 以下の「データ型の分類と覚え方」 の中で、(1) 数値型、(2) ミュータブル、(3) イミュータブルを覚えましょう。int, list, tupleなどデータ型の概要が理解できていれば難しくないと思います。

なお、rangeとfile objectは感覚がつかみにくいかもしれません。rangeはシーケンスであり、かつ、イテラブルです。file objectはシーケンスではないですが、イテラブルです。

(1) データ型の分類と覚え方

(1) 数値型:int, float, complex  
 ※boolはintのサブクラスだが数値型ではない。
(2) ミュータブルlist, bytearray, dict, set
 ※変更可能なオブジェクト
(3) イミュータブル数値型, bool, tuple, str, range, bytes, frozenset, file object
 ※変更ができないオブジェクト
(4) ミュータブルなシーケンスlist, bytearray
 ※順序を持たないdict, setはシーケンスではない。
(5) イミュータブルなシーケンスtuple, str, range, bytes
 ※数値型、boolはデータが1つしかないのでシーケンスではない。
 ※順序を持たないfrozensetはシーケンスではない。
 ※len()などが使えるrangeはシーケンス。
 ※len()などが使えないfile objectはシーケンスではない。
 ※file objectのように順番に取り出せるだけではシーケンスではない。
(6) シーケンス型list, bytearray, tuple, str, range, bytes
 ※(4)と(5)を合わせたもの
(7) イテラブル:シーケンス, dict, set, frozenset, flie object  
 ※データが1つしかない数値型, boolはイテラブルではない。
 ※組み込みデータ型では数値型, bool 以外はすべてイテラブル。
 ※file objectも1つずつデータを取り出せるのでイテラブル。
(8) マッピング型:dict
(9) コンテナ:シーケンス, dict, set, frozenset
 ※コンテナとは複数のオブジェクトを格納できるデータ型。
 ※データが1つしかない数値型, boolはコンテナではない。
 ※それぞれの要素を個別に扱うことができないfile objectはコンテナではない。
 ※シーケンス(rangeも含む)は基本的に全てコンテナ
 ※コンテナとは基本的に__contains__メソッドを実装しているオブジェクトのこと。

3.用語の説明

以下の各用語の概要と分類の理由を説明します。

(1) 数値型

数値型は、int型(整数型), float型(浮動小数点数型), complex型(複素数型)です。まさに数値なのでわかりやすいと思います。

ところで、bool型(ブール型、真偽値型)は数値型に含まれるのでしょうか。以下、Pythonのドキュメントの記述です。bool型は整数(int)のサブタイプであると記述されているものの、数値型はint, folat, complexの3種類であると限定されており、bool型は数値型に含まれていません。

4.4. 数値型 int, float, complex
数値型には3種類あります: 整数 、浮動小数点数 、複素数です。さらに、ブール型は整数のサブタイプです。整数には精度の制限がありません。

Python ドキュメント >> Python 標準ライブラリ >> 組み込み型 >> 数値型

参考までに「みんなのPython 柴田 淳(著)」でも数値型にbool型は含まれていませんでした。

みんなのPython 第4版

新品価格
¥2,970から
(2019/12/4 20:59時点)

(2) ミュータブル、イミュータブル

「ミュータブル(mutable)」は変更が可能なオブジェクトであり「変更可能体」ともいいます。また、「イミュータブル(immutable)」は変更ができないオブジェクトであり「変更不能体」ともいいます。それぞれのデータ型は、以下のようにミュータブルかイミュータブルのどちらかに分類されます。

(a) ミュータブル( 変更可能なオブジェクト ):
 list, bytearray, dict, set
(b) イミュータブル(変更ができないオブジェクト):
 数値型(int, float, complex), bool, tuple, str, range, bytes, frozenset, file object

コード04は変更不能体であるtupleを代入した変数の変更です。3行目のように要素を部分的に変更することはできませんが、4行目のように内容をそっくり入れ替えることはできます。

#コード04
a = (1, 2)
#a[0] = 9 #n.g.
a = (99, 2)
print(a)
#出力04
(99, 2)

ここで重要なのは、4行目で変数aを(99, 2)に入れ替えた時に、もとの(1, 2)がコンピューターのメモリー上でどうなってしまうかです。

変数aに(99, 2)を上書きし、(1, 2)が消えてしまうのではなく、(1, 2)は現在の記憶場所に放置され、別の記憶場所に(99, 2)が定義し直されます。

「そんな細かいこと覚える必要はないのでは?」と思われるかもしれませんが、「関数へのデータの受け渡し」「浅いコピー」「深いコピー」などの理解に役立ちますので知っておくべきです。分からない場合には、導入として以下の記事を参考にしてください。

用語集:変更可能体(ミュータブル)と変更不能体(イミュータブル)

(3) シーケンス型

複数の要素を順番に並べたデータ型がシーケンス型です。なお、数値型やboolは複数の要素を持ちませんからシーケンス型ではありません。また、dict, set, frozensetは複数の要素を持つことができますが、順番という概念がないのでシーケンス型ではありません。

(a) シーケンス型 :
 list, bytearray, tuple, str, range, bytes

(b) ミュータブルなシーケンス:
 list, bytearray
※s.append(x), s.clear(), s.copy(), s.extend(t), s.insert(i,x), s.pop([i]), s.remove(x), s.reverse()などは、 ほとんどの「ミュータブルなシーケンス」で使用可能なメソッド です。

(c) イミュータブルなシーケンス:
 tuple, str, range, bytes

なお、rangeはシーケンス型ですが、file objectはシーケンス型ではありません。理由は以下のとおりです。

a) file object はシーケンスではない

前から順番に呼び出せるだけではシーケンスではありません。file objectは、他のシーケンスのように要素指定やlen(), max(), index()を使った演算ができません。他のシーケンスとは使える関数が大きく異なるためシーケンスの分類には含まれません。

#コード02
f = open('test_data01.txt', 'r')
#print('f[0]', f[0]) #n.g.
#print('len(f)', len(f)) #n.g.

また、Pythonのドキュメントでも明記されています。

(反復可能オブジェクト) 要素を一度に 1 つずつ返せるオブジェクトです。 反復可能オブジェクトの例には、(list, str, tuple といった) 全てのシーケンス型や、 dict や ファイルオブジェクト といった幾つかの非シーケンス型、 あるいは Sequence 意味論を実装した__iter()__メソッドか__getitem()__メソッドを持つ任意のクラスのインスタンスが含まれます。

Python ドキュメント >> 用語集 >> iterable

b) rangeはシーケンスです

rangeは他のシーケンス(listやtupleなど)と同じようにlen(), max(), index()を使った演算が可能です。5行目のように要素を指定することもできます。さすがに「+」「*」の演算はできませんでしたが、rangeは他のシーケンスと共通点が多くシーケンスです。

#コード01
#print(range(3) + range(2)) #n.g.
#print(range(10) * 10) #n.g.
print('in', 5 in range(10))
print('[3]', range(10)[3])
print('len', len(range(10)))
print('max', max(range(10)))
print('index', range(10).index(2))
#出力01
in True
[3] 3
len 10
max 9
index 2

rangeについてもPythonのドキュメントに明記されています。

基本的なシーケンス型は 3 つあります: リスト、タプル、range オブジェクトです。バイナリデータ や テキスト文字列 を処理するように仕立てられたシーケンス型は、セクションを割いて解説します。

Python >> ドキュメント >> Python 標準ライブラリ » >> 組み込み型 >> シーケンス型

(4) イテレータ

「イテレータ(iterator)」は「反復子」とも言います。

イテレータは、求められるたびに要素をひとつずつ返し、データがなくなるとデータの代わりに StopIteration 例外を返すオブジェクトです。例外を返すことにより、データがなくなったことを示し、その後はデータを返すことができなくなります。イテレータは便利な機能であり、様々な関数で利用されています。

なお、iter() 関数を用いれば、range, listなどからイテレータを生成することができます。

また、for文はイテレータとは無関係のように見えますが、実はforループを使うたびに、range, listなどから、新たな未使用のイテレータを生成して利用しています。

(5) イテレーション、イテレートする

イテレータの説明を行いましたので、関連用語についても簡単に説明したいと思います。

「イテレーション」
意味:「反復処理」のことです。
用例:「for文のループを抜け出してしまうのではなく、continueにより次のイテレーションを直ちに開始する。」

「イテレートする」
意味:(イテレータから)データを一つずつ取り出すこと。
用例:「zip()により、複数のリストから同時にイテレートすることができる。」

(6) イテラブル

イテレータに変換可能なオブジェクトのことを 「イテラブル(iterable)」または「反復可能体」といいます。 以下のようなデータ型がイテラブルです。

シーケンス(list, bytearray, tuple, str, range, bytes), dict, set, frozenset, flie object

データが1つしかない数値型, boolはイテラブルではありません。しかし、それ以外の多くのデータ型がイテラブルであり、主要な組み込みデータ型では数値型, bool以外は全てイテラブルです。シーケンスがイテラブルであることは容易に想像できますが、シーケンスではないflie objectや順番が定義されていないdict, set, frozensetもイテラブルです。

イテレータは、求められるたびに要素をひとつずつ返し、データがなくなるとデータの代わりに例外を返すオブジェクトですが、非常に便利な機能であるためにイテレーターの機能を利用する関数が多くあります。for文のinの後にイテラブルを使用したり、iter(), max(), min(), sorted(),sum(), list(), set()といった関数の引数にイテラブルを使用します。

もし、イテラブルという言葉がなければ、例えばiter()の引数に使用できるデータ型を説明するためにデータ型を列挙しなければなりません。

データ型がイテラブルであるかどうかはfor文のinの後で使用したり、iter()の引数として使用することで確認することができます。

a) file object はイテラブル

イテラブルはfor文でinの後に用いることができます。file objectもコード03のようにfor文で使うことができ、イテラブルであることが分かります。

#コード03
f = open('test_data01.txt', 'r')
for line in f:
    print(line, end = '')
f.close()

(7) マッピング型

キーとなる値(key)を任意のオブジェクト(value)に対応付けるデータ型です。キーをインデックスとしてオブジェクトを検索するため、キーは他のキーと比較し区別できる必要があり、ミュータブルな値をキーにすることはできません。標準のマッピング型は辞書 (dict) だけです。

(8) コンテナ

コンテナとは「複数のオブジェクトを格納できるオブジェクト」です。数値型やboolは複数のオブジェクトを格納できないのでコンテナではありません。

Pythonのドキュメントでは、コンテナについて「他のオブジェクトに対する参照をもつオブジェクト」と紹介してされています。 他のオブジェクトに対する参照を集めてひとまとめにしています。

他のオブジェクトに対する参照をもつオブジェクトもあります; これらは コンテナ (container) と呼ばれます。コンテナオブジェクトの例として、タプル、リスト、および辞書が挙げられます。オブジェクトへの参照自体がコンテナの値の一部です。

Python ドキュメント >> Python 言語リファレンス >> 3. データモデル >> 3.1. オブジェクト、値、および型

シーケンス型、辞書型、集合型がコンテナであり、以下のデータ型がコンテナに分類されます。

シーケンス(list, bytearray, tuple, str, range, bytes), dict, set, frozenset

list, tuple, dict, set, frozensetは、要素が明確に分かれており、複数のオブジェクトが格納されていることが明快です。

str, bytearry, bytesは複数のオブジェクトには見えませんが、例えばx = ‘abc’では’a’, ‘b’, ‘c’という複数のオブジェクトが集まったものでありコンテナです。

rangeは、実際には複数のオブジェクトが集まったものではないかもしれませんが、複数のオブジェクトが集まったように振る舞うことができるのでコンテナです。

file objectはイテラブルであり、順にデータを出力することはできますが、演算子inによって要素の内容を確認することもできません。それぞれの要素を個別に扱うという機能が低く、複数のオブジェクトをグルーピングすることにより機能性を高めるというコンテナの考え方からは外れており、コンテナではありません。

なお、コンテナであるかどうかの判別は__contains__を実装しているかどうかが有力な判断材料になります。

4.コンテナの分類に関する考察

イテラブルはfor文のinのあとに使えるかどうかで判断することが可能ですが、コンテナは定義が曖昧であり、コンテナであることを明確に判断する方法はありません。そこで、コンテナの分類を上記のように判断した理由を説明したいと思います。

(1) コンテナであることが明らかなデータ型

Pythonのドキュメントにおいて以下の記述があり、str, tuple, list, dict, set, frozensetがコンテナであることは明記されています。

ブール演算のコンテキストや、式が制御フローの文で使われる際には、次の値は偽だと解釈されます: False 、 None 、すべての型における数値の 0、空の文字列、空のコンテナ (文字列、タプル、リスト、辞書、集合、凍結集合など)。 それ以外の値は真だと解釈されます。 ユーザ定義のオブジェクトは、__bool__() メソッドを与えることで、真偽値をカスタマイズできます。。

Python ドキュメント >> Python 言語リファレンス >> 6. 式 (expression) >> ブール演算

しかし、bytes, bytearray, range, file object については、それぞれ、コンテナなのかどうか明記されていません。そこで、これらは__contains__の実装を確認することによって判断します。

(2) 文献調査

書籍「科学技術計算のためのPython入門」では、コンテナ型、シーケンス型を以下のように定義しされており、シーケンス型はコンテナ型の一部であると記述されています。つまり、bytes, bytearray, rangeもコンテナに分類されています。

コンテナの定義について記述された書籍は少なく、良書だと思います。

複数のデータを保持できる文字列型やリスト、タプル、バイト、バイト配列、集合型、辞書型は、総称してコンテナ型と呼ばれます。さらに、コンテナ型のうち順番付きのデータ集合であって、インデックス(データの順番を表す数値)を使ってそのデータにアクセスできるデータ型をシーケンス型また、そのデータ型変数を単にシーケンスと呼びます。

科学技術計算のためのPython入門 中久喜 健司著 技術評論社

科学技術計算のためのPython入門 ――開発基礎、必須ライブラリ、高速化

新品価格
¥3,520から
(2019/12/4 21:03時点)

(3) なぜ、__contains__によって判別できるのか

Python ドキュメント >> Python 標準ライブラリ » 8. データ型 >> 8.4. collections.abc — コレクションの抽象基底クラス >> 8.4.1. コレクション抽象基底クラス」において、 コレクション抽象基底クラス(collections.abc)の説明があり、 「抽象基底クラス、継承しているクラス、抽象メソッド、mixin メソッド」の一覧表が紹介されています。

ABC (抽象基底クラス)継承しているクラス抽象メソッドmixin メソッド
Container __contains__
SequenceReversible, Collection__getitem__ , __len__ __contains__ , __iter__ , __reversed__ , index, count
SetCollection__contains__ , __iter__ , __len__ __le__ , __lt__ , __eq__ , __ne__ , __gt__ , __ge__ , __and__ , __or__ , __sub__ , __xor__ , isdisjoint

新しいクラスを自作するときに、コレクション抽象基底クラスを基底クラスとすると、一覧表の抽象メソッドを持つことが強制され、一覧表のmixinメソッドについてはコードを記述することなく使うことができるようになります。

以下のコードは「 8.4.1. コレクション抽象基底クラス 」で示されたサンプルコードですが、Setの抽象メソッドは、__iter__ , __contains__ , __len__であり、例えば18, 19行目を削除すると、21, 22行目でインスタンスを生成できなくなります。また、__and__はmixiメソッドであり、コードを記述しなくても23行目のように「&」を使用することが可能です。

#コード08
#「8.4.1. コレクション抽象基底クラス」より引用
class ListBasedSet(collections.abc.Set):
    ''' Alternate set implementation favoring space over speed
        and not requiring the set elements to be hashable. '''
    def __init__(self, iterable):
        self.elements = lst = []
        for value in iterable:
            if value not in lst:
                lst.append(value)

    def __iter__(self):
        return iter(self.elements)

    def __contains__(self, value):
        return value in self.elements

    def __len__(self):
        return len(self.elements)

s1 = ListBasedSet('abcdef')
s2 = ListBasedSet('defghi')
overlap = s1 & s2            # The __and__() method is supported automatically

つまり、コレクション抽象基底クラスは、Pythonというプログラム言語が、コンテナ、シーケンス、setといった型について、どのような性質を持つべきかを示したものであるともいえます。

そして、一覧表では、抽象基底クラス:Containerに対して、抽象メソッドが__contains__となっており、コンテナは、最低限__contains__メソッドを持つことが求められていることがわかります。

__contains__メソッド を持たなくとも、複数のオブジェクトを格納できるオブジェクトを自作することはできるかもしれませんが、それはPythonというプログラム言語が求める方向性から外れており、統一性のないオブジェクトを量産することになります。

__contains__の実装がコンテナであることの必要十分条件ではありませんが、有力な判断材料であり、少なくとも自作ではない組み込みデータ型では__contains__の実装で判断してもよいと考えられます。

参考までに、listやrangeのクラスの継承関係をissubclassで確認してみると、コード09のように複数のコレクション抽象基底クラスを継承しているという結果が出力されます。

#コード09
import collections.abc

print(type(s1))
print('(1) list:')
print(issubclass(list, collections.abc.Container))
print(issubclass(list, collections.abc.Sequence))
print(issubclass(list, collections.abc.MutableSequence))
#以下、listはSetではないためFalseとなる。
print(issubclass(list, collections.abc.Set)) 

print('(2) range:')
print(issubclass(range, collections.abc.Container))
print(issubclass(range, collections.abc.Sequence))
#以下、rangeはMutableではないためFalseとなる。
print(issubclass(range, collections.abc.MutableSequence)) 
#出力09
(1) list:
True
True
True
False
(2) range:
True
True
False

(4) __contains__の実装の確認

オブジェクトに__contains__を実装すると、演算子inによって所属関係を調べることができます。例えば 「x in s」では、xがsの要素であればTrueとなり、そうでなければFalseとなります。

つまり、__contains__は複数の要素があることを前提に実装されるメソッドであり「__contains__メソッドが実装されたオブジェクトがコンテナである」という判別に用いるのは自然な考え方であるといえます。

つまり、演算子inを使うことができるかどうかで判断できます。ただし、__iter__によっても、演算子inを用いることができるので注意する必要があります。

例えば以下のinput_data05.txtに対してコード05を実行すると、1回目の「’a¥n’ in f」の出力はTrueですが、2回目はFalseです。これは、__contains__ではなく、__iter__のイテレータの機能を使ったため、1度だけしかデータが読めず、2度目はfile objectにデータが残っていないと判断されたためです。

なお、入力データファイルは1行に改行を含むので’a’だけではなく’a\n’とする必要があることに注意しましょう。

#input_data05.txt
0
1
2
a
b
#コード05
f = open('input_data05.txt', 'r')
print('a¥n' in f)
print('a¥n' in f)
#出力05
True
False

比較のために、__contains__を実装しているrangeで検証してみます。今度は1度目も、2度目もTrueになりました。なお、5行目のように直接__contains__呼び出すこともできます。

#コード06
a=range(10)
print(3 in a)
print(3 in a)
print(a.__contains__(3))
#出力06
True
True
True

また、__contains__を実装しているかどうかは、以下のようにdir()を用いて直接調べることもできます。dir()は引数にオブジェクトを指定することで、そのオブジェクトのメソッドなどの一覧を出力します。コード07の出力は長いので省略しますが、rangeは__contains__を実装しており、file objectは実装していないことがわかります。

#コード07
print('■file object')
f = open('test_data02.txt', 'r')
print(dir(f))
f.close()

print('■range')
a = range(10)
print(dir(a))

なお、bytes, bytearrayも同様に__contains__を実装しており、コンテナであると判断できます。

(4) rangeは本当にコンテナ?

range(100)は実際に0~99のオブジェクトが格納されているわけではありません。rangeの引数100から、0~99のintが格納されているようなふるまいをしているだけです。そういう意味では、「他のオブジェクトに対する参照をもつオブジェクト」ではないようにも思えます。 書籍「科学技術計算のためのPython入門」でもrange()はコンテナに分類されていましたが、念のためPythonのドキュメントも色々調べてみました。

以下の引用文ではlen()の引数は何らかのコンテナであるという記述があり、この記事のとおりに考えれば、len()の引数として使えるrangeもコンテナということになります。

このように、公式の見解とも乖離していません。

len(x)というコードを読んだ時、私はそれが何かの長さを問うているのだなと知ることができます。これは私に2つの事を知らせています。一つは結果が整数であること、そして引数は何らかのコンテナであることです。対して、x.len()を目にした場合、私はその時点でxが何らかのコンテナであり、それが標準のlen()を持っているクラスを継承しているか、インターフェースを実装していることを知っている必要があります。

Python ドキュメント>> Python よくある質問 >> デザインと歴史 FAQ
Python にメソッドを使う機能 (list.index() 等) と関数を使う機能 (len(list) 等) があるのはなぜですか?

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー

UdemyのPythonの動画講座を書籍を買う感覚で購入してみた