はじめに
ある日会社で「次のプロジェクトは Python でやってほしい」と言われました。普段はずっと Node.js を触ってきたので、「まあ文法がちょっと違うくらいでしょ」と軽く考えていました。async/await もあるし、動的型付け言語だし、きっとすぐ慣れるはず……。
ところが実際に触ってみると、思っていた以上に“違い”が多かったんです。予想通りな部分もあれば、「えっ!?」と声が出たような挙動もありました。そして、中には「こっちの方が分かりやすい!」と感動したものも。
この記事では、Node.js から Python に移って最初の数週間で気づいたこと、つまずいたことをまとめてみました。
スコープ:最初の「え、まじで?」
JavaScript の世界では、let や const でブロックスコープを当たり前のように使います。だから Python でこんなコードを書いたとき――
if (true) {
let x = 1;
}
console.log(x); // ReferenceError
でも Python ではブロックにスコープがない。
if True:
x = 1
print(x) # 1 が出力される
思わず二度見しました。JS なら外から参照できないはずなのに…。
同じく for ループでもびっくり。
for (let i = 0; i < 3; i++) {}
console.log(i); // ReferenceError
for i in range(3):
pass
print(i) # 2
JS の for (let i = 0; i < 3; i++) {} ならループ変数は外に漏れないのに、Python は最後の値が残ります。最初はバグかと思いました。今は「Python では関数単位がスコープ」と理解したので気をつけるようにしています。
クロージャー:「既視感あるけど違う」
JS のクロージャー感覚で書いたら、こんなことに。
JavaScript の場合:
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(() => i);
}
console.log(funcs.map(f => f())); // [0, 1, 2]
Python の場合:
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2]
全部「2」になるんです。JS なら [0, 1, 2] なのに。
答えはシンプルで、Python は「変数」をキャプチャするのでループが終わったあと全部同じ値を見るんですね。回避するにはデフォルト引数を使います。
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
「そういう仕様か」と理解するまでに、ちょっと時間を食いました。
デフォルト引数の罠
これも有名どころですが、初めて遭遇したときは本当に混乱しました。
JS だと引数のデフォルト値は毎回評価されます。
function f(x, arr = []) {
arr.push(x);
return arr;
}
console.log(f(1)); // [1]
console.log(f(2)); // [2]
ところが Python は関数定義時に一度だけ評価される。
def f(x, arr=[]):
arr.append(x)
return arr
print(f(1)) # [1]
print(f(2)) # [1, 2]
最初「なぜ?」と悩みましたが、関数オブジェクトが作られるタイミングでデフォルト引数も一緒に作られるから、という話。今はすぐにこう書き直す癖がつきました。
def f(x, arr=None):
if arr is None:
arr = []
arr.append(x)
return arr
等価性と None
JS でさんざん苦しんだ == と === の二重ルール。Python では基本 == 一つで十分ですが、代わりに is という概念が登場します。
JavaScript では == と === の二重ルール。
console.log([] == []); // false
console.log([] === []); // false
console.log("5" == 5); // true
console.log("5" === 5); // false
Python では == と is。
[] == [] # True
[] is [] # False
値として同じでも、オブジェクトとして同一かどうかは別。これは JS の === とも違うので、慣れるまでに数回間違えました。
あと、null と undefined が分かれている JS に比べ、Python は None 一択。このほうが頭の整理はしやすいですね。未定義変数を参照すると NameError が飛ぶのも、個人的には好きです。
割り算のスッキリ感
JS だと / しかなくて整数かどうかは意識次第。でも Python は /(通常の割り算)と //(切り捨て割り算)がちゃんと分かれています。
JavaScript:
console.log(5 / 2); // 2.5
console.log(5 / 0); // Infinity
Python:
5 / 2 # 2.5
5 // 2 # 2
5 / 0 # ZeroDivisionError
あと 5 / 0 が Infinity ではなく ZeroDivisionError になるのも分かりやすい。
文字列操作:f-string の快適さ
最初は JS のテンプレートリテラルが恋しかったのですが、Python の f-string を知った瞬間に吹っ飛びました。
JavaScript のテンプレートリテラル:
const name = "Alice";
console.log(`Hello, ${name}!`); // Hello, Alice!
Python の f-string:
name = "Alice"
print(f"Hello, {name}!") # Hello, Alice!
イテレーション
for...of に慣れていた自分にとって、Python の for item in iterable: はすぐに馴染みました。むしろ読みやすいくらいです。
JavaScript:
for (const item of [1, 2, 3]) {
console.log(item);
}
Python:
for item in [1, 2, 3]:
print(item)
ただしリスト内包表記は独特で、最初は「JS の map/filter 的なやつ?」と思いましたが、結構表現力が高い。
実際の JS では map を使う:
const squares = [0,1,2,3,4].map(x => x * x);
Python:
squares = [x*x for x in range(5)]
Python ではリスト内包表記の変数はループの外に漏れないので、この点ではむしろ安全でした。
まとめ
最初は「JS と似てるし余裕」と思っていましたが、数週間で学んだのは「別の文化を持つ言語」ということでした。
つまずいたポイントは…
- ブロックスコープがない
- クロージャーは変数キャプチャ
- デフォルト引数が一度だけ評価される
気に入ったポイントは…
- f-string の快適さ
- 割り算の明快さ
-
None一本化のシンプルさ
振り返ると、Python は「なるべくシンプルで明示的に」という思想が全体に流れている気がします。JS の柔軟さやカオス感も面白いですが、Python の「ひとつのわかりやすい道筋」に救われる瞬間も多いです。
これから Python を始める JS エンジニアの方へ――最初は驚きの連続かもしれません。でもその驚きが一周すると、「Python って気持ちいいな」と感じる瞬間がきっと来ます。