生存報告:0レベル問題を本気で考えるということ
2025年12月31日
最近ブログを書いてなかった理由
最近、ブログの更新が止まっていた。
「最近」と言っても、気づいたら前回の投稿からほぼ半年が経っていた。
何をしていたのかと言われると、別に大層な理由があるわけではない。
- ドメインの有効期限が近づいていたので、新しいドメインに移行した
- 終わりの見えないブログのリファクタリングを進めている(というより、ほぼ作り直し)
- 事業をやっている友人のWebアプリを作りつつ、自分用の自動化ツールをいくつか書いた
- そして、自分の一番の弱点だと思っているコーディングテストとCSの勉強をしている
要するに、目に見える成果物よりも、思考のほうに時間を使っていたという話だ。
で、今回話したいコードは
これ。
def solution(num_list):
return [even := sum(i % 2 == 0 for i in num_list), len(num_list) - even]
num_listの中に含まれる偶数と奇数の個数を配列で返す関数だ。
プログラマーズの0レベル問題
「偶数と奇数の個数」というやつ。
条件は以下の通り。
- リストの長さ:1 ≤ n ≤ 100
- 要素の範囲:0 ≤ 値 ≤ 1000
正直、難しい問題ではない。
一番オーソドックスな解き方はこうなる。
even, odd = 0, 0
for i in num_list:
if i % 2 == 0:
even += 1
else:
odd += 1
return [even, odd]
このコードが悪いと言うつもりはない。
ただし、コーディングテストという文脈に限っては、これは自分の選択肢ではない。
一応先に言っておくと、
実務コードとコーディングテスト用のコードは、はっきり分けて考えている。
実務では可読性、協業性、保守性を優先する。
一方でコーディングテストでは、できる限り思考を圧縮する方向に振り切る。
つまりこの記事は、
実務コードを誇るためのものではなく、考え方を残すための記録だ。
思考過程 #1
sum(1 for i in num_list if i % 2 == 0)
偶数なら1を返して、それをsumで足す。
これで偶数の個数は求められる。
じゃあ奇数も同じようにやれば?
[sum(1 for i in num_list if i % 2 == 0), sum(1 for i in num_list if i % 2 == 1)]
これで答えは出る。
ただし、num_listを2回走査して、ほぼ同じ条件を2回評価している。
とはいえ、この問題での最大要素数は100なので、性能的な問題は一切ない。
それでも、自分の感覚では美しくない。
思考過程 #2 : ウォルラス演算子
ウォルラス演算子とは何か。
Python 3.8で追加された構文だ。
Walrus Operator (:=) 式の中で値を計算しつつ代入できる
この問題でウォルラスがハマる理由はひとつ。
偶数か奇数、どちらか一方の個数が分かれば、もう片方は計算する必要がない。
もちろん、ウォルラスを使わなくても書ける。
even = sum(1 for i in num_list if i % 2 == 0)
return [even, len(num_list) - even]
これで無駄な計算は消えるし、多くの人はここで止まると思う。
ただ、ここでウォルラスを使うとこうなる。
[even := sum(1 for i in num_list if i % 2 == 0), len(num_list) - even]
可読性も協業性も保守性も正直どうでもいい。
コーディングテストという場で、思考をどこまで圧縮できるかを楽しみたい自分には、ちょうどいいコードだ。
思考過程 #3
まだ改善の余地はある。
Pythonでは、Trueは1、Falseは0として扱われる。
つまり、1 for iみたいな分岐を書かなくても、boolean演算をそのまま使える。
sum(i % 2 == 0 for i in num_list)
もちろん、この違いが体感できるほど効くわけではない。
要素数100の問題では、正直ほぼ意味はない。
それでも、条件分岐を減らし、Pythonの型仕様に素直に乗るという意味で、ここまで詰めた。
こうして最終的に残ったコードがこれだ。
def solution(num_list):
return [even := sum(i % 2 == 0 for i in num_list), len(num_list) - even]
0レベル問題をここまでやる人は、たぶん多くない。
でも、冗談で書いたコードではない。
本当に効率的なのか?
可読性や協業性、保守性はいったん置いておく。
時間計算量と空間計算量の観点で見る。
時間計算量
- リストを1回走査 → O(n)
- それ以外はすべて O(1)
全要素を最低1回は見る必要がある以上、これ以上はどうやっても下がらない。
空間計算量
- ジェネレータ使用
- 不要な配列生成なし
- 保持する変数はevenひとつだけ
空間計算量はO(1)。
実務ならどうするかというと、
def solution(num_list):
even = sum(i % 2 == 0 for i in num_list)
return [even, len(num_list) - even]
あるいは素直にfor文を書く。
協業コードでのウォルラスは、ほとんどの場合ノイズになる。
結論
このコードを書けと言いたいわけではない。
簡単な問題でも、いろいろな解き方を試すことで
「何が使えるか」「そして何を使わないか」を意識的に選べるようになる。
知らなくて使わないのと、
知った上で使わないのは、まったく別の話だ。
この記事を書いた理由は、
今の自分のコード美学が、1年後や5年後も同じだとは思っていないからだ。
新しいことを学べば、
将来の自分がこのコードを見て「何やってんだ」と言うかもしれない。
それでも、ここに至るまでの思考は残しておきたい。
2025年の自分は、こう考えていたという記録として。
ちなみに、% 2 == 0すら無駄だと思う人向けに、
ビット演算を使ったやり方も一応ある。
return [len(num_list) - (odd := sum(i & 1 for i in num_list)), odd]
正直、自分の基準でもこれはやりすぎだ。
直感的じゃないし、人間向けのロジックでもない。
Pythonのインタプリタオーバーヘッドを考えれば、
前のコードと実質的な差もない。
ここが、自分の引くラインだ。
もし、ラムダ以外に別の解き方をした人がいたら、ぜひ見てみたい。
問題そのものより、考え方のほうが面白いコードには、学ぶ価値があると思っている。
カカオ
グーグル
ネイバー