文字列の要素を数え上げる方法はいろいろとあります。シンプルなコードからそうじゃないものまで。皆さんは何個のやり方を思い付きますか?
以下は文字列'atmarkit'を構成しているアルファベットが何文字あるかを調べるコードだ。これは正しく動作するが、もっとシンプルに書く方法がある。どんな方法があるかを考えてみよう。何個考え付くかな?
s = 'atmarkit'
d = {}
for k in s:
if k in d:
d[k] += 1
else:
d[k] = 1
print(sorted(d.items()))
出力結果:
# [('a', 2), ('i', 1), ('k', 1), ('m', 1),
# ('r', 1), ('t', 2)]
今回はChatGPT(GPT-5.1)、Gemini(Gemini 3 Proの思考モード)、Claude(Opus 4.5)の3つのLLMに問題文のコードを書き直す方法を考えて、何個を思い付いたかを教えてもらいました。一番たくさんの方法を考え付いたのは誰でしょう? 答えは最後に!
どうもHPかわさきです。
正解の数は筆者には分かりません(開き直り)。読者の皆さんの方が多くの正解を考え付くんじゃないかなと考えています。正解例のコードにないものがあれば、教えてくださいね。
正解のコード例を以下に示します。
# 1:collections.Counterクラスを使う
from collections import Counter
s = 'atmarkit'
d = Counter(s)
print(sorted(d.items()))
# 2:collections.defaultdictクラスを使う
from collections import defaultdict
s = 'atmarkit'
d = defaultdict(int)
for k in s:
d[k] += 1
print(sorted(d.items()))
# 3:辞書のsetdefaultメソッドを使う
s = 'atmarkit'
d = {}
for k in s:
d[k] = d.setdefault(k, 0) + 1
print(sorted(d.items()))
# 4:辞書のgetメソッドを使う
s = 'atmarkit'
d = {}
for k in s:
d[k] = d.get(k, 0) + 1
print(sorted(d.items()))
筆者が考え付いた方法はCounterクラスを使う方法、defaultdictクラスを使う方法、辞書のsetdefaultメソッドもしくはgetメソッドを使う方法です。いずれの方法を使ってもfor文の中でif文を使って文字数をカウントする必要がなくなります。多分、これ以外にも多くの方法があるんじゃないかな?
というわけで、4つの方法についてちょっと説明をしていきましょう。
問題文のコードは次のようなものでした。
s = 'atmarkit'
d = {}
for k in s:
if k in d:
d[k] += 1
else:
d[k] = 1
print(sorted(d.items()))
このコードは、for文で文字列'atmarkit'を構成する各文字を反復しながら、それが辞書のキーに既にあれば、その値を1つ増やし、まだキーになければその文字をキーに、値を1に設定するコードです。最後に、アルファベット順に並べ替えたものを出力していますが、それはまあここでは特に言及しません。
問題なのはif文です。ここでは文字列の要素を1文字ずつ調べて、辞書のキー/値の組を更新しています。が、賢明なPythonistの皆さんは「ここはもっとうまい方法があるはず」と考えるのではないでしょうか。
そのうまい方法として、筆者が考え付いたものが以下です。
これらについて順に見ていきます。
まずはcollectionsモジュールのCounterクラスです。その名前からも分かる通り、反復可能オブジェクト(リストや文字列)に含まれている要素を数え上げるためのものです(辞書を渡せば、それを使って初期化されますが、ここでは説明は省略)。そのため、Counterクラスのインスタンス生成時に文字列を渡せば、それだけでその文字列を構成する文字の数え上げが終わっちゃいます。
from collections import Counter
s = 'atmarkit'
d = Counter(s)
print(sorted(d.items()))
これならfor文も必要ないですし、コードはすごくカンタンになりますね。別の方法としては、例えば、次のような使い方もできるでしょう。
# for文の中で数え上げを自分でやる
s = 'atmarkit'
d = Counter()
for k in s:
d[k] += 1 # d[k] = d[k] + 1
print(sorted(d.items()))
# updateメソッドを使う
s = 'atmarkit'
d = Counter()
d.update(s)
print(sorted(d.items()))
Counterクラスのインスタンスは存在しないキーを指定してもKeyError例外を発生させずに0を返すので、「d[k] += 1」「d[k] = d[k] + 1」のような書き方で数え上げをしてもよいのですが、それならインスタンス生成時に文字列を渡しちゃった方がよさそうですね。また、updateメソッドで現在のCounterクラスのインスタンスのキー/値の組を更新することも可能です。
まあ、この場合は素直にCounterクラスのインスタンス生成時に文字列を渡すのがシンプルでよいやり方でしょう。
次にcollectionsモジュールのdefaultdictクラスを使う例です。defaultdictクラスとは「存在しないキーが指定された場合にそのキーの値の初期値を決定する関数を指定できる辞書」です。初期値を決定する関数は引数なしで呼び出され、それがキーの値となります。
以下の例を見てください。
from collections import defaultdict
s = 'atmarkit'
d = defaultdict(int)
for k in s:
d[k] += 1
print(sorted(d.items()))
注目してほしいのは、「d = defaultdict(int)」の「int」です。つまり、存在しないキーの初期値は「int()」呼び出しの結果である「0」になるということです。for文の最初のループではループ変数kに文字列'a'がに渡されます。そしてd['a']は存在しないので、その値は0になります。そして「d['a'] += 1」はそれに1を加算した結果である1となるというわけです。キーが存在していれば、その値に1が加算されるのでちゃんと数え上げられるはずです。
辞書にはsetdefaultメソッドがあります。このメソッドは第1引数に渡したキーが存在すればその値を返します。キーが存在しなければ、第2引数に指定したデフォルト値をそのキーの値として設定して、そのデフォルト値を戻り値とします。コードにすれば以下のような感じ(ただし、mysetdefaultは関数なので、「d.mysetdefault(……)」ではなく「mysetdefault(d, ……)」のように呼び出す必要があります)。
def mysetdefault(self, key, default=None):
if key in self:
return self[key]
self[key] = default
return default
s = 'ab'
d = {}
for k in s:
value = mysetdefault(d, k, 0) # value = d.mysetdefault('mykey', 0)に相当
d[k] = value + 1
このメソッドを使えば、キーが存在するかどうかを気にすることなく、for文の中で直接「d[k] = d.setdefault(k, 0) + 1」と書けるということです。1を加算しているのは、存在しないキーの初期値を0として1を加算するようにしているからです。初期値を1にしちゃうと、数え上げの結果がおかしくなっちゃうので注意してください。
s = 'atmarkit'
d = {}
for k in s:
d[k] = d.setdefault(k, 0) + 1
print(sorted(d.items()))
getメソッドも同様に例外を発生させずに辞書の存在しないキーにアクセスするためのやり方です。辞書でkeyというキーにアクセスする際には(辞書をdとして)d[key]とアクセスするか、d.get(key)としてアクセスするかの2つの方法があります。前者はキーが存在しなければ例外を発生させますが、後者はキーが存在しない場合には第2引数に指定した値が返されます(デフォルト値はNone)。
このことを利用して、「d.get(k, 0)」と呼び出せば、例外は発生せずに初期値としての0を得られます。それに1を加算してあげれば、初出の文字をキーとする要素の値が1になるというわけですね。
s = 'atmarkit'
d = {}
for k in s:
d[k] = d.get(k, 0) + 1
print(sorted(d.items()))
どの方法が一番よいと思いますか? collectionsモジュールによく慣れているのであればCounterクラスを使うのがオススメでしょう。というか、知識さえあれば、これが一番よいと思います。for文も必要なくなるし、コードが簡潔になるという意味ではCounterクラス一択だと思います。
defaultdictクラスやsetdefaultメソッド、getメソッドはいずれも存在しないキーにアクセスした場合に例外が発生するのを防止するための方策です。ですが、それらの振る舞いは微妙に異なります。
| クラス/メソッド | 存在しないキーにアクセスしようとした場合 |
|---|---|
| defaultdictクラス | 常に、そのキーとデフォルト値の組が辞書の要素として作成される |
| setdefaultメソッド | メソッドを呼び出すとそのキーとデフォルト値の組が辞書の要素として作成される |
| getメソッド | 辞書の内容は変更せずにデフォルト値を返す |
| defaultdictクラス、setdefaultメソッド、getメソッドの違い | |
defaultdictクラスでは存在しないキーにアクセスしようとしたら、その時点でそのキーとデフォルト値の組が辞書の要素として作成されます。setdefaultメソッドはそれを呼び出した場合にのみ、そのキーとデフォルト値の組が辞書の要素として作成されます。getメソッドは辞書を変更せずに、デフォルト値を返すだけです。これ以上の詳細は省略しますが、振る舞いの違いには注意してください。
それでは発表です。
というわけで、ChatGPTさんが一番多くの書き替え案を考えてくれました。いや、実はClaudeさん(Opus 4.1)は10個も考えてくれたんですが、最新バージョンのOpus 4.5でやり直したら、数が減ってしまったのでした。10個ですか、すごいですね。でも、ChatGPTさんも「10個以上は考えられますよ」といっていたので、やる気になればわれわれ人間様でもそのくらいは考え付くのかもしれません。
ちなみに全員がイチオシだったのはやっぱりCounterクラスを使う方法でした。
ここで紹介していない方法としては、次のようなものが挙げられていました。全てではないですが紹介だけしておきましょう。
「シンプルに書き直してください」とお願いしたけれど、そうでもないやり方などもあって、なかなか楽しい感じでした。上記のやり方のコードも紹介します(説明は省略します)。
s = 'atmarkit'
# 辞書内包表記と文字列のcountメソッド
d = {k: s.count(k) for k in set(s)}
# dict.fromkeysメソッド
d = dict.fromkeys(s, 0)
for k in s:
d[k] += 1
# itertools.groupbyクラス
from itertools import groupby
result = [(k, len(list(g))) for k, g in groupby(sorted(s))] # リストになるよ
Copyright© Digital Advantage Corp. All Rights Reserved.