前置き
Natural Language Processing with Pythonの1章を見ていると、以下のような機能説明が。
# Functions Defined for NLTK's Frequency Distributions
fdist1 |= fdist2 # update fdist1 with counts from fdist2
fdist1とfdist2はFreqDistクラスのインスタンスです。FreqDistクラスは単語の頻度分布、つまりFrequency Distributionを取り扱うための色々便利な機能が揃っています。
一方でこの記号"|="、これはPython3.9で登場したマージ演算子による累積代入と同じ書き方です。この書き方だと、存在しないキーについては単純な追加。キーが重複しているときは、fdist1側の要素をfdist2側の要素が一律に上書きするようになっています。(参考: Pythonで辞書に要素を追加、辞書同士を連結(結合)
sample_dict1 = {"k1": 1000, "k2": 2000, "k5": 5}
sample_dict2 = {"k2": 200, "k3": 300, "k4": 400}
sample_dict1 |= sample_dict2
print(sample_dict1)
{'k1': 1000, 'k2': 200, 'k5': 5, 'k3': 300, 'k4': 400}
私はこの記号を見て、「あぁFreqDistもマージ演算の累積代入が可能なんだな」と確かめもせずに思っていましたが、実際は全く同じというわけではありませんでした。
FreqDistの挙動
結論から言いますと、「キーが存在しない場合は単純な追加」という点は同じですが、キーが重複していた場合は必ずしも上書きされるわけではなく、「要素数が多い方が優先される」というのが正しい理解になります。
ここではサンプルプログラムとして、上の書籍のChapter1: Language Processing and PythonのSection3.4: Counting Other Thingsで使われていた単語の長さを確認するプログラムを少しいじって利用します。サンプルの文字列を空白ごとに分割し、文字数をカウントしてから辞書に割り当てる、といった流れですね。
from nltk.tokenize import word_tokenize
from nltk.probability import FreqDist
sample_text1 = "a a a aa aaa aaaa"
fdist1 = FreqDist()
for word in word_tokenize(sample_text1):
fdist1["length" + str(len(word.lower()))] += 1
print(fdist1.tabulate())
sample_text2 = "a aa aa aaaa aaaa aaaaa"
fdist2 = FreqDist()
for word in word_tokenize(sample_text2):
fdist2["length" + str(len(word.lower()))] += 1
print(fdist2.tabulate())
print("------------after------------------")
fdist1 |= fdist2
print(fdist1.tabulate())
print(fdist2.tabulate())
こちらの出力は以下のようになります。
length1 length2 length3 length4
3 1 1 1
None
length2 length4 length1 length5
2 2 1 1
None
------------after------------------
length1 length2 length4 length3 length5
3 2 2 1 1
None
length2 length4 length1 length5
2 2 1 1
None
まずはlength5に注目しましょう。
1つ目のサンプルにはlength5,つまり"aaaaa"が存在していません。よって、afterの方では単純にlength5: 1の組み合わせで追加されています。
次はlength2に注目しましょう。
1つ目のサンプルにはlength2,つまりaaが1個存在しています。テキストの"aa"由来ですね。しかし2つ目のテキストにも"aa aa"、つまりlength2: 2の組み合わせがあったので、そちらでlength2が上書きされています。
とまぁここまでは従来のマージ演算と同じだと言っても問題はありません。length2に対する上書きは単純に右側にあったが故、と考えても成立するからです。
問題はlength1、こちらに注目してください。
1つ目のサンプルではlength1: 3となっています。テキストに"a a a"が含まれているからですね。一方で2つ目のサンプルの方でもlength1: 1があります。テキストに"a"がありますので。
しかし、結果を見てみると、length1: 3はそのままで上書きされていません。これが一律上書きをする組み込み演算子による辞書マージとの違いです。値が大きいためそちらが優先された、と考えると一応挙動に説明はつきます。
まとめ
ここまでで"|="の挙動をまとめると、
1.)左側にキーが無い時は右側からそのまま追加
2.)キーが重複している場合は要素数が多いほうが優先される。つまり、左側のほうが大きかった場合は上書きが起きない。
ということなると思います。FreqDistクラスが辞書と同じ挙動をしている、という考え方がそもそもの間違いの原因である気もします。どちらかというと集合で使われる和集合の演算の方が近いような……でもFreqDistの和集合というのにもどうにも違和感が……的確な説明が出来る方がいたら、ぜひ教えてもらいたいところです。
いずれにせよ、変なハマり方をする前に気づけてよかったです。集合知に感謝。