ラベル collections の投稿を表示しています。 すべての投稿を表示
ラベル collections の投稿を表示しています。 すべての投稿を表示

2019年7月20日

OrderedDict の行く末

こちら述べたように、Python 3.7 から、dict の要素順が挿入順であることが保証されるようになりました。
Python には元々 collectionsモジュールに OrderedDict という挿入順を保つ辞書型が用意されていました。dict が挿入順となった Python 3.7 以降では、OrderedDictの使い道はなくなってしまうのでしょうか?
実はそうではありません。OrderedDict と dict は基本的な性質に違いがあります。

まず、OrederDict の基本です。要素の挿入順が保持されます。

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d[1] = 1
>>> d[2] = 2
>>> d
OrderedDict([(1, 1), (2, 2)])  # 1, 2の順
この挿入順は、そのまま同値判定にも使用されます。
つまり、要素が同じでも挿入順が異なれば、OrderedDict としては異なる値として判定されます。
>>> rd = OrderedDict()
>>> rd[2] = 2
>>> rd[1] = 1
>>> rd
OrderedDict([(2, 2), (1, 1)])  # 2, 1の順

# 挿入順が異なるので、異なる値となる
>>> d == rd
False

OrderedDict の同値判定を敢えて書けば、以下のようになるでしょうか。
def eq(lhs, rhs):
    return (len(lhs) == len(rhs)) and
            all(i == j for i, j in zip(lhs.items(), rhs.items()))


一方 dict の場合、挿入順は要素の並び順だけの話で、同値判定には使用されません。
>>> d = dict()
>>> d[1] = 1
>>> d[2] = 2
>>> d
{1: 1, 2: 2}  # 1, 2の順

>>> rd = dict()
>>> rd[2] = 2
>>> rd[1] = 1
>>> rd
{2: 2, 1: 1}  # 2, 1の順

# 挿入順は異なるが、同じ値
>>> d == rd
True

dict の同値判定を敢えて書けば、以下のようになるでしょうか。
def eq(lhs, rhs):
    if len(lhs) == len(rhs):
        try:
            return all(lhs[k] == rhs[k] for k in lhs)
        except KeyError:
            pass
    return False



OrderedDict には基本的な仕様で dict と異なるところがあるので、今後も OrderedDict が廃止になったり、非推奨に降格になることは無いと思われます。

2016年9月9日

collections.namedtuple の注意点

collections.namedtuple のちょっとした注意点を紹介します。

namedtuple は tuple を継承したものです。

>>> from collections import namedtuple
>>> Point = namedtuple('Point', 'x y z')

# namedtuple は tuple のサブクラス
>>> issubclass(Point, tuple)
True

当然、tuple のアトリビュートは namedtuple にも引き継がれます。
# tuple のアトリビュート
>>> dir(tuple)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '_
_eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs
__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__'
, '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__
rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'i
ndex']

# namedtuple のアトリビュート
>>> dir(Point)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '_
_eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs
__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__'
, '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '
__repr__', '__rmul__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__s
ubclasshook__', '_asdict', '_fields', '_make', '_replace', '_source', 'count', '
index', 'x', 'y', 'z']

ここで注目すべきは、tuple は count と index という _(アンダーバー)で始まらないアトリビュートを2つ持っている点です。即ち、namedtuple の要素名として count か index を指定した場合、その namedtuple は親クラスである tuple のインターフェースを変えてしまった良くないサブクラス、ということになります。
結論として、namedtuple の要素名として count と index は使うべきではありません。
>>> WrongNames = namedtuple('WrongNames', 'count index')
>>> wrong_names = WrongNames(1, 2)

# count は要素名になってしまった
>>> wrong_names.count
1
# tuple の count() を期待するとエラー
>>> wrong_names.count(1)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'int' object is not callable

# index は要素名になってしまった
>>> wrong_names.index
2
# tuple の index() を期待するとエラー
>>> wrong_names.index(1)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'int' object is not callable

2015年12月7日

collections.OrderedDict

collectionsライブラリで定義されている collections.OrderedDict を紹介します。
(OrderedDict は Python 3.1 で追加されました。さらに 3.2 と 3.5 で若干の機能追加が行われています。)

dict の key の順序はハッシュ値により決定されます。つまり使う側から見ると、順序は制御不可能です。
OrderedDict は dict を継承したクラスで、key の順序が登録した順になります。
dict と OrderedDict の動作を比較します。

# ビルトインのdict。順序はハッシュ値依存
>>> d = dict()
>>> d['A'] = 10
>>> d['B'] = 11
>>> d['C'] = 12
>>> d
{'C': 12, 'B': 11, 'A': 10}

# OrderedDict。順序は登録順
>>> from collections import OrderedDict
>>> od = OrderedDict()
>>> od['A'] = 10
>>> od['B'] = 11
>>> od['C'] = 12
>>> od
OrderedDict([('A', 10), ('B', 11), ('C', 12)])


OrderedDict は dict を継承して作られています。このため、dict でできることは全て OrderedDict でもできるようになっています。この辺り、コンテナ型を自作する場合には参考にしたい所ですね。
>>> issubclass(OrderedDict, dict)
True

>>> isinstance(od, dict)
True


OrderedDict の使い所は当然ながら、データの順序を保ちつつ dict でデータを管理するようなケースです。設定ファイルの読み込み等で最もよく使われると思います。
以下は、tuple の順序を変えずに OrderedDict へ代入する例です。
# 順序を維持したいデータ
>>> settings = (
...     ('protcol', 'TCP'),
...     ('IP', '192.168.0.1'),
...     ('port', 60000),
... )

# OrderedDictへ代入
>>> od = OrderedDict(settings)
>>> od
OrderedDict([('protcol', 'TCP'), ('IP', '192.168.0.1'), ('port', 60000)])


また、通常の dict から key の大小で並び替えた OrderedDict を作るには以下のようにします。(当然、dict の key は大小比較が可能である必要があります。)
# 通常のdict。順序はハッシュ値依存
>>> d = dict()
>>> d['A'] = 10
>>> d['B'] = 11
>>> d['C'] = 12

# key順でOrderedDictへ代入
>>> od = OrderedDict(sorted(d.items()))
>>> od
OrderedDict([('A', 10), ('B', 11), ('C', 12)])

2015年12月5日

collections.Counter

コンテナの要素数を数える時に便利な collections.Counter を紹介します。
(※ collections.Counter は Python 3.1 からの機能です。)


'refrigerator'という単語に含まれている文字をカウントするプログラムを考えます。
dict内包表記を使うと、以下のように実装できます。

>>> s = 'refrigerator'

>>> d = {i:s.count(i) for i in set(s)}
>>> d
{'o': 1, 'a': 1, 'i': 1, 'r': 4, 't': 1, 'g': 1, 'f': 1, 'e': 2}

同様の処理を collections.Counter では以下のように実現できます。
>>> from collections import Counter

>>> c = Counter(s)
>>> c
Counter({'r': 4, 'e': 2, 'o': 1, 'g': 1, 'i': 1, 't': 1, 'a': 1, 'f': 1})

collections.Counter はいくつかの専用メソッドを備えています。要素数に関する何らかの値を取得したい場合に、これらのメソッドは有用です。
# 最多の要素を表示
>>> c.most_common(1)
[('r', 4)]

# 要素数 0 でもKeyErrorではない
>>> c['b']
0

頻繁には使わないけど、目的とガッチリ噛み合った時には非常に強力、そんなツールだと思います。

2015年11月4日

collections.abcで型チェック

collections.abc を用いた型チェックの方法を紹介します。

collections.abc にはコンテナのための様々な抽象基底クラスが定義されています(ちなみに、abc は"Abstract Base Classes"の略です)。この抽象基底クラスを使うことで、可読性が高く、さらに汎用性もある型チェックを行うことができます。



以下のようにコンテナを前半と後半に分割する関数 divide() について考えます。

def divide(data):
    # スライスで分割
    half = len(data) // 2
    return (data[:half], data[half:])


ここで引数 data はどのような型であるべきでしょうか?
スライス機能を受け入れる型であれば動いてくれそうです。すぐに思いつくのは list、tuple、str 辺りでしょうか。もう少し調べると、range、bytes、bytearray、memoryview もスライス機能を受け入れるようです。
では、divide() をより安全な関数とすべく、引数 data の型チェックを追加してみましょう。
def divide(data):
    # dataの型チェック
    if not isinstance(data, (list, tuple, str, range, bytes, bytearray, memoryview)):
        raise TypeError

    # スライスで分割
    half = len(data) // 2
    return (data[:half], data[half:])


全くもって醜いソースコードになってしまいました。醜いだけならまだしも、以下の2つは明らかに欠点です。
① 7つの型を列挙したが、まだ漏れがあるかもしれない。
② data が自作コンテナクラスである場合、(たとえスライス機能があっても)TypeError になる可能性がある。

このような時に役に立つのが collections.abc を用いた型チェックです。スライス機能があるかどうかは collections.abc.Sequence を使うことで確認できます。
collections.abc.Sequenceを使うと型チェックは以下のようになります。

def divide(data):
    # dataの型チェック
    from collections.abc import Sequence
    if not isinstance(data, Sequence):
        raise TypeError

    # スライスで分割
    half = len(data) // 2
    return (data[:half], data[half:])

可読性が向上しましたし、欠点①と②がいずれも解決しています。Coolですね!


collections.abc には Sequence 以外にも多くの抽象基底クラスが定義されています。他のクラスについては項を改めて紹介します。

2015年11月3日

collections.namedtuple

collectionsライブラリには様々な有用な型が定義されています。
ここでは collections.namedtuple を紹介します。

namedtuple とは、tuple を拡張したもので、変数をまとめるだけの簡単なクラス(のようなもの)を手軽に作成することができます。

型の定義は以下のように行います。

# Pointという名前のnamedtupleを定義。
# 変数としてx, y, zを持つ
>>> from collections import namedtuple
>>> Point = namedtuple('Point',
...                    ['x', 'y', 'z'])


このような定義することで、あたかも x, y, z というメンバ変数を持つ Pointクラス(のようなもの)を作成できます。
# Point変数を新規作成
>>> p = Point(10, 20, 30)

# x, y, zにメンバ変数のようにアクセスできる
>>> p.x
10
>>> p.y
20
>>> p.z
30


注意点として、あくまで tuple ですので要素の変更は許可されません。
# 要素の変更はできない
>>> p.x = 11
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: can't set attribute


要素を変更したい場合、変更そのものはできないので、変数の新規作成を行う必要があります。
型名を指定して新規作成する方法と、_replace()を利用する方法があります。
# 新規作成で x のみ変更
>>> p = Point(11, p.y, p.z)
>>> p
Point(x=11, y=20, z=30)

# _replace()で x のみ変更した値を再代入
>>> p = p._replace(x=12)
>>> p
Point(x=12, y=20, z=30)


クラスと比べるとずっと手軽に定義でき、list や tuple と比べると要素の意味が把握しやすい、というのが namedtuple の特徴と言えます。
クラスを定義するほどでもない小さなデータをまとめる場合に便利です。是非使いこなしましょう。