はじめに
昨年(2017年)にとあるコンピュータスクールの講師向けにPythonに関して講義する(Python講座導入するんだけど「Pythonって何?」状態だからと説明する)機会がありました。
そこで聞かれた「Pythonに似てる言語ってなんですか?」という質問に対して「スクールで扱ってないけどRuby・・・、いや、JavaScriptすね。まあスクールのJavaScript講座の内容じゃあ似てるって感じないと思いますが」と答えました。
一方最近「Node.js講座作りたい。PythonやAI(機械学習)の講座作ったけど難しいからとっかかりはJavaScriptの方が簡単だし」という発言があり、
お前は何を言ってるんだ(ミルコ略)
と思った次第。というのが今回の記事を書こうと思った動機です。
先に態度と結論を表明しておくと
どちらか片方が簡単と言うつもりはなく両方の言語をよく調べてみれば片方が簡単だなんて口が裂けても言えない。
どちらにもそれぞれ興味深い点がある。
というのが私の意見です。そんなわけで以下随筆風に両言語のいろんな言語要素を比較していこうと思います。
ブロック表現
さてさっそくPythonバッシングの鉄板からいきましょう。そう、「ブロック1を書くのにインデントが強制されるのがうざい」です。
これに対する回答は「いや、普通どんな言語でもブロックはインデントするやん。なんで強制されるからって文句言うの?」となります。ひょっとしたらその人は「一文だけなのにインデントするのが気に入らない」と思っているのかもしれません。そういう人にはこうも書けますよと教えてあげましょう。
if a > b: print('a is greater than b')
ところで「インデントがー」と言うわりには「これぞJavaScript!」とコールバックに次ぐコールバックをしてるのを見ると・・・、まあ「それは過去の遺物だ。今どきはPromise、さらにasync/await使うんだよ!」とマサカリが飛んできそうなのでやめておきます:-P
「閉じ」がないことについて
もう一つインデントによるブロックで批判があるとすると「閉じかっこがないからどこでブロックが終わっているのかわかりにくい」でしょうか。これに回答すると「わからなくなるぐらい長くブロックを書くな」というところです。
実際に私も先人が残した「ずっとインデントされたまんまのPythonコード」を見て閉口、いやむしろ開いた口が塞がらない?したことがあります。elseが出てきたところで「あれ?これはどういうifに対するelseだっけ?」と上に戻っていき・・・さらにifがネストしてたりすると「あれ?インデントレベル考えるとこっちのifのはずだけど、あってるっけ・・・」とまた下に戻るとコードを行ったり来たりしてやっと理解するということがありました。
『リーダブルコード』の「7.7 ネストを浅くする」です。エラーな場合はさっさと関数を終了しましょう。これはどの言語でも共通ですが特にPythonの場合「アンリーダブルコード」に出くわすと「明確なブロックの終了記号がないからわかりにくい」となるのかもしれません。
変数
どちらの言語も「変数名に型は結びついておらず、変数はただのラベルで参照先が型情報を持っている」という点では共通です。
Pythonの変数
Pythonの変数基本編。
- トップレベルで代入を行うとモジュール変数として定義される。
- 関数の中で代入をすると関数のローカル変数として定義される(引数もローカル変数として扱われる)
- クラス定義直下で代入するとクラス変数になる。
- 関数(outer)の中で関数(inner)が定義できる。innerはouterのローカル変数を参照できる(つまりクロージャ)。ただしinnerがouterの変数を変えることはできない(ここで言う「変える」とはinnerのローカル変数の参照先を「変える」という意味です。参照しているオブジェクトの内容を「変える」ことはできます)
- モジュール変数と同じ名前のローカル変数を定義するとシャドーイングが起こる(クロージャが外側の関数の参照先を変えられえないのもこの一種)
Python変数応用編。
- global文を使うとローカル変数を定義するのではなくモジュール変数を参照する。
- 関数内関数(inner)でnonlocal文を使うとinnerのローカル変数にはならず、代入すると外側の関数(outer)のローカル変数が変更される。
- 定数などない。
- モジュール変数をプライベートにする方法などない。
- 一応言っておくとブロックレベル変数などない。
定数、プライベートがない点についてはたまにやり玉にあがっていますが個人的には言語仕様で用意はせずプログラマの良心にまかせるという思想を気に入ってます。
なお、globalとは言ってもPythonにはC言語のようなグローバル変数はありません。トップレベルに書かれているものはあくまでモジュールの属性です。fooモジュールとbarモジュールで両方abcと定義していても衝突は起こりません。
JavaScriptの変数
- 何もつけないとグローバル変数。トップレベルだろうが関数内だろうがお構いなし。再代入ももちろん該当変数が変更される。
- 関数内でvarを付けるとローカル変数。
- 最近はconstが使えるようになった。
- letも使えるようになった。
- const, letはブロックローカル。
グローバルを汚染しないための名前空間は非常にバッドノウハウだと思いますね(個人の感想です)
余談:「定」とは何なのか
さてPythonには定数はない、Javascriptには定数があると説明しましたが、この「定数」という言葉は非常に誤解されやすいと思われます。例えば、
> const foo = []
undefined
> foo
[]
> foo.push(1)
1
> foo.push(2)
2
> foo.push(3)
3
> foo
[ 1, 2, 3 ]
と「定数」として定義しているにも関わらず内容を書き換えることができます。
もちろん、代入しようとするとエラーになります。
> foo = {}
TypeError: Assignment to constant variable.
つまり、定数の「定」とは「一度指したオブジェクトを変えられない」ということです。「定数」という言葉からオブジェクトの内容自体を変更できなくするという印象を持つ人もいるのではないでしょうか。
まあそう思うのはC++に触れたことがある人だけですかね。C++には「constオブジェクト」というものがあり、変数定義時の型にconstを付けると「このメソッドはメンバ変数変更しません!」とconst付けてる(ややこしい)メソッドしか呼べないという仕様があるわけですが。私も初めはこういう意味のconstだと思ってたので慣れるまで違和感がありました。
関数
私がPythonに似ている言語と聞かれてJavaScriptと答えた理由がこれ。関数、特に関数オブジェクトの概念です。というわけで似ている点、違う点について見ていきましょう。
関数オブジェクト
Pythonの場合
以下のように書きます。
def foo(a, b):
return a + b
関数定義以降はfoo
という名前で関数オブジェクトが参照できます。
ラムダ式もありますが機能は限定されており一つの式しか含めません(この式が実行されreturnされます)
なお、以下の例は比較のために代入していますがこのようにラムダ式を変数に代入するのはPEP8というPythonスタイルガイドで「やっちゃいけないこと」とされています。
foo = lambda a, b: a + b
じゃあどういうところで使うのかと言うと、mapなどの処理方法を関数オブジェクトとして渡すがdefを使うまでもない場合(一つの式しか含まない場合)です。
Javascriptの場合
Javascriptでは次のように関数を定義します。
function foo(a, b) {
return a + b;
}
今どきこんな風に書いてるとm9(^Д^)プギャーされます。ナウい書き方はこうです。
foo = function(a, b) {
return a + b;
}
私が初めてJavaScriptを学んだころは一つ目の書き方で、しばらく見なかった後にjQuery全盛ぐらいにJavascriptのコードを見たらこの書き方がばりばりされていて「なんじゃこら」と思ったものです。思えばそのあたりが関数オブジェクトというものを認識する初めだったような気もします。さて、
最近ではアロー関数というものもできたみたいですね。
foo = (a, b) => {
return a + b;
}
{}省くとreturnも書かなくてもいい。Pythonのラムダ式をパクってますね(Javaの方でしょうけど)
foo = (a, b) => a + b;
ところでどの書き方しても関数オブジェクトにfooって名前が付くんですよね(少なくともNode.jsでは)
ていうかJavascriptってどうやって関数オブジェクトの名前取り出すんだろう。
引数の数
こちらは主にJavascriptについて。
Javascriptは関数について以下のルールがあります。
- 関数に定義されている引数より少ない引数の数で呼び出せる。対応する引数がないとundefinedが代入される。
- 関数を定義する際に呼び出し側の渡す引数を全部受け取る必要はない。
特に2つ目の特徴を知って「ほー」と思いました。コールバック文化なので使わない変数は無視とできるのがいいなと思いました。
Pythonでも残余引数を使えば2つ目はできますが、1つ目は無理かな、全部デフォルト値付きにするのは明らかに駄目な設計だし。
インスタンス作成
Pythonの場合
こんな感じ。
class Foo:
def __init__(self, x):
self.x = x
foo = Foo(123)
new付けなくていいのかって?付けなくていいのです。ていうかPythonにはnewなんてありません。
Pythonを気に入ってる理由としてベストスリーぐらいには入るのがインスタンス生成時のこの書き方です。何がいいかって、インスタンスを作っていると意識させる必要がありません。別の言い方をすると関数呼び出しに見えます。そう、組込み関数と言っているものの中には実際にはインスタンスが作られているものもありますが(listとか)初心者の人にはとりあえず関数と言っておいて後から「今こそPythonの真実を明かそう」と言うことができるのです。
もう一言言っておくと、このように関数っぽく呼べるのは裏で__call__という特殊メソッドが動いています。
Javascriptの場合
さてJavascript。
function Foo(x) {
this.x = x;
}
foo = new Foo(123);
ここで間違ってnewをつけ忘れると大変なことになります。
ってdisろうと思ったんですけど最近はclassキーワードが使えるようになってしかもそれで定義したものはnew付けないで呼び出すとTypeError発生させるんですね。
class Foo {
constructor(x) {
this.x = x;
}
}
要素の反復処理
Pythonの場合
リスト(Javascriptで言う配列)要素のイテレーション。
a = [1, 2, 3]
for x in a:
print(x)
辞書の場合。
d = {'foo': 123, 'bar': 456}
for k, v in d.items():
print(k, v)
Python知らない人のためにいろいろ補足。
- Pythonでは文字列をキーとする場合にクォーテーションは省略できません(付けないとfooという変数が参照されます)
- inの後ろにdだけ書くとキーのリスト(みたいなもの)が繰り返し対象になります。
- キーはどうでもよく値だけで繰り返したい場合はvalues()を使います。
ともかく全部「for in」です。
Javascriptの場合
Javascriptは例によって古からの進化の歴史があるみたいですね。
配列。
a = [1, 2, 3]
// forEachメソッド使う書き方
a.forEach((x) => console.log(x));
// for in使う書き方
for (let i in a) {
console.log(a[i]);
}
// for of使う書き方
for (let x of a) {
console.log(x);
}
オブジェクト。
obj = {foo: 123, bar: 456}
for (let key in obj) {
console.log(obj[key]);
}
配列をfor inする場合は添え字じゃなくて値を入れてほしい感ありますがオブジェクトの挙動を考えるとまあ納得という感じです。
後、forEachの場合、関数が実行されてるのでそこでreturnとかしても繰り返しが終わりにならない点に注意が必要ですかね。いやまあ自分が間違えてそう書いただけですが(笑)
あとがき
さてというわけでPythonとJavascriptのいろんな言語要素について眺めてきました。私はプログラミング言語マニアなのでおもしろい言語機能があるとうれしいのですが初心者の場合ははまる・混乱の元になりそうな要素もあります。
特にJavascriptの場合、「それは過去の書き方だ、今はこう書く」と先端の方がおっしゃっても如何せん初心者の方が学ぶには難しすぎるのでレガシーな書き方を学びそれがはびこってしまうのではないかなーと思いました。
参考
-
正確なPython用語的にはスイートと言うらしいですね。私も今調べて知りました。 ↩