リストにどの要素が何個含まれているか、数えたいことってありますよね。もちろん、自分で書いても構いません。でも、あのモジュールのあのクラスを使うのがカンタンですよ。
以下はリストに同一の要素が何回登場するかを数え上げて、要素をキー、その登場回数をキーの値とする辞書を作成するコードである。このコードは意図した通りに動作する。しかし、Pythonに標準で付属しているあるモジュールのあるクラスを使うと同じことが2行で記述できる。そのクラスを使って、このコードを書き直してみよう。
items = ['foo', 'baz', 'bar', 'bar', 'bar', 'baz', 'bar']
counts = {}
for item in items:
c = counts.get(item, 0)
counts[item] = c + 1
print(counts) # {'foo': 1, 'baz': 2, 'bar': 4}
どうもHPかわさきです。
先週こんなことを書きました。「0 <= num < 10」のような書き方について「このような書き方ができる言語はあまりないようです」と。そしたら、某所よりツッコミがありまして、BCPL(Cの先祖であるBの先祖に当たる言語とも)ではこのような書き方が許されているそうです。BCPLを使ったことがなかったので知りませんでした。
https://www.cl.cam.ac.uk/~mr10/よりダウンロード可能なBCPLの言語マニュアル『The BCPL Cintsys and Cintpos User Guide』(bcplman.pdf)には「An expression of the form: E relop E relop ... relop E where each relop is one of =, ~=, <=, >=, < or > returns TRUE if all the individual relations are satisfied and FALSE, otherwise.」とあります。適当に訳すと「E relop E relop ... relop E(ここでrelopはそれぞれ=、~=、<=、>=、<、>のいずれか)という形の式は全ての関係が満たされていればTRUEを、そうでなければFALSEを返す」となります。
というわけで、BCPLでも「0 <= num < 10」のように比較演算を連鎖させることは可能です。勉強になりました(ただし、BCPL処理系のビルド、コードの実行まではしていないので、あくまでもマニュアルを見ただけの話です)。
正解のコード例を以下に示します。
from collections import Counter
items = ['foo', 'baz', 'bar', 'bar', 'bar', 'baz', 'bar']
counts = Counter(items)
print(counts) # Counter({'bar': 4, 'baz': 2, 'foo': 1})
ここではcollectionsモジュールのCounterクラスをインポートして、そのインスタンス生成時にリストを渡しています。これにより、リストの要素を数え上げて、要素をキー、出現回数を値とするCounterオブジェクト(辞書と同様なマッピングオブジェクト)を得ています。
この他にもitertoolsモジュールのgroupby関数を使う方法もありますが、ここでは紹介はしません(なお、groupbyは関数のように呼び出して使用しますが、その実体はクラスです。Pythonって、そういうの多いですよね)。
Pythonに標準で付属するcollectionsモジュールにはCounterクラスが含まれています。このクラスの生成時にリスト(などの反復可能オブジェクト)を渡すと、各要素の登場回数を数え上げてくれます。
問題文のコードは次のようなものでした。
items = ['foo', 'baz', 'bar', 'bar', 'bar', 'baz', 'bar']
counts = {}
for item in items:
c = counts.get(item, 0)
counts[item] = c + 1
print(counts) # {'foo': 1, 'baz': 2, 'bar': 4}
ここではリストの要素を数え上げています。'foo'は1個、'bar'は4個、'baz'は2個です。そこで、Counterクラスのオブジェクトの生成時にこれを渡してやれば同じことができるということです。
from collections import Counter
items = ['foo', 'baz', 'bar', 'bar', 'bar', 'baz', 'bar']
counts = Counter(items)
print(counts) # Counter({'bar': 4, 'baz': 2, 'foo': 1})
最初のコードではPythonに組み込みの辞書が作成されていましたが、正解例のコードではCounterクラスのオブジェクトになっている点に注意してください。ただし、両者は比較可能です。
from collections import Counter
items = ['foo', 'baz', 'bar', 'bar', 'bar', 'baz', 'bar']
counts_dict = {}
for item in items:
c = counts_dict.get(item, 0)
counts_dict[item] = c + 1
counts_counter = Counter(items)
print(counts_dict == counts_counter) # True
ここでは問題文と同じやり方で数え上げた結果の辞書をcounts_dictに、Counterクラスを使って数え上げた結果をcounts_counterに代入して、それらが等しいかを試しています。その結果はTrueになります。同じ型にそろえたければ「dict(counts_counter)」あるいは「Counter(counts_dict)」のようにすることも可能です。マッピングオブジェクトを使って、Counterクラスのオブジェクトを生成すると、そのマッピングオブジェクトのキー/値の対がそのままCounterクラスにおけるそれとなります(コード例は省略)。
辞書とCounterクラスの大きな違いとしては存在しないキーを角かっこ「[]」に指定した場合の振る舞いが挙げられます。
print(counts_dict['hoge']) # KeyError
print(counts_counter['hoge']) # 0
このコードは、上で作成したcounts_dictとcounts_counterに存在しないキーを指定して、その値を表示しようとしています。辞書では(もちろん)KeyError例外になりますが、Counterクラスのオブジェクトでは0と表示されます。
また、updateメソッドの振る舞いにも差があります。辞書のupdateメソッドに辞書(マッピングオブジェクト)を渡すと、そのキー/値の組が元の辞書にあれば、値を上書きしますが、Counterクラスではその値が加算されます。また、辞書のupdateメソッドに上で見たitemsのようなリストは渡せませんが、Counterクラスのupdateメソッドにリストを渡すと、要素を数え上げて、既存のキーの値についてはその値が加算されます。以下に例を示します。
tmp_dict = {'baz': 3, 'qux': 6}
counts_dict.update(tmp_dict)
print(counts_dict) # {'foo': 1, 'baz': 3, 'bar': 4, 'qux': 6}
counts_counter.update(tmp_dict)
print(counts_counter) # Counter({'qux': 6, 'baz': 5, 'bar': 4, 'foo': 1})
もともと、counts_dictもcounts_counterも'baz'キーの値は2でした。そして、counts_dict.updateメソッドに辞書{'baz': 3, 'qux': 6}を渡すと、'baz'キーの値が上書きされて3になります。一方、counts_counter.updateに同じ{'baz': 3, 'qux': 6}を渡すと、'baz'キーの値である3が加算されて、counts_counter['baz']は5になっています。これまでになかったキー'qux'についてはそれらが新規に追加されている点は両者で同じです。
リストを追加すると次のようになります。
tmp_list = ['qux', 'quuux']
counts_dict.update(tmp_list) # ValueError
counts_counter.update(tmp_list)
print(counts_counter) # Counter({'qux': 7, 'baz': 5, 'bar': 4, 'foo': 1, 'quuux': 1})
辞書のupdateメソッドにリストを渡すとValueError例外となりますが、Counterクラスのupdateメソッドでは渡したリストの要素が数え上げられて、その値が加算されていることが分かります([('foo', 0), ('bar', 1), ('ba', 2)]のような2つの要素からなる反復可能オブジェクトを要素とするリストなら辞書のupdateメソッドに渡せることには留意してください)。
また、Counterクラスにはキーの値の数だけキーを反復するイテレータを返すelementsメソッドや値が多いキーを指定した数だけ表示するmost_commonメソッド、キーの値を減算するsubtractメソッド、カウントの合計を計算するtotalメソッドもあります。以下にこれらのメソッドの使用例を示します(ここでは新規にCounterオブジェクトを作成しています)。
cnt = Counter({'foo': 1, 'bar': 2, 'baz': 3})
print(cnt) # Counter({'baz': 3, 'bar': 2, 'foo': 1})
# elementsメソッド
result = list(cnt.elements())
print(result) # ['foo', 'bar', 'bar', 'baz', 'baz', 'baz']
# most_commonメソッド
print(cnt.most_common(2)) # [('baz', 3), ('bar', 2)]
# subtractメソッド
cnt.subtract(['foo', 'foo', 'bar'])
print(cnt) # Counter({'baz': 3, 'bar': 1, 'foo': -1})
# totalメソッド
print(cnt.total()) # 3
ここではcntオブジェクトはCounter({'baz': 3, 'bar': 2, 'foo': 1})となっています。最初に、elementsメソッドを呼び出して得たイテレータからリストを作成しています。このとき、'foo'キーの値は1なので、結果のリストには'foo'が1つだけ含まれ、同様に、'bar'は2つ、'baz'は3つ含まれています。リストの要素を数え上げた場合の逆変換をできると考えてもよいでしょう。most_commonメソッドには2を渡しています。そのため、値が多い要素を2つ取り出して(キー, 値)のタプルとして、それらをリストに格納したものが返されています。
subtractメソッドにはリストを渡しています。リストには'foo'が2つ、'bar'が1つ含まれているので、cntオブジェクトの'foo'キーの値は2だけ減算され、'bar'キーの値は1だけ減算されているのが分かります(負値も取れることに注意)。最後のtotalメソッドでは各キーの値を合計して('foo'キーの値は-1、'bar'キーの値は1、'baz'キーの値は3)、ここでは3となりました。
また、2つのCounterオブジェクトについて集合演算を行う==演算子、!=演算子なども使用できますが、ここでは例は省略します。
とまあ、リスト(反復可能オブジェクト)の要素を数え上げたり、数え上げた結果を使ってちょっとした何かをしたりするために便利なメソッドが備わっているので、Counterクラスはぜひとも覚えておきましょう。
『解決!Python』でもCounterクラスを扱った記事はあるんですが、今回の記事の方がちょっと詳しいので、えー……無理に読まなくっても大丈夫です……。
Copyright© Digital Advantage Corp. All Rights Reserved.