よみとばしポイント
どうも限界派遣SESなnikawamikanです。
アドカレ16日目の本日もQiitan獲得のために記事を書いていきます。
今回はプログラミング初心者な頃にJavaを学んだエピソードなどなどを交えて、プログラミング初心者にはPythonを勧めるけど、何が起こっているかを理解はしづらいよねという話をしていきます。
Pythonを勧める理由
まず、初心者には私はおそらく間違いなくPythonを勧めます。
理由はいくつかあります。
- 構文がシンプル
- ライブラリが豊富
- とりあえず動かしてみることが出来るのでTry&Errorがしやすい
などなど。
とりあえず、Pythonで書いてみて動かしてみることが出来るので、プログラミングの楽しさを感じやすいと思います。
でもお前Javaから始めたやん
はい、その通りです。
私はプログラミングスクール出身なのですが、その時に教えてもらったのがJavaでした。
15人ほどのクラスで、一部を除いてほとんどの人がプログラミング初心者でした。
その中で実際に「コード書いてるなぁ」という印象の人は私を含めて3人ほどでした。
これは私の主観ですが、Javaって「これ作ってみたいなぁ」って思えるものが少ないんですよね。
そもそもJavaは業務システム向け
Javaはその構文の厳密さや型の厳密さから、業務システム向けの言語として使われることが多いです。
環境構築のハードルもPythonなどと比べると高いですし、「へー!こんなん作れるんやおもろ!」となりにくいのが個人的な感想です。
ここで私は自分なりにコンソールで動くRPG的なゲームを作ったり、ゲーム感覚でプログラミングを楽しんでいましたが、たぶんそれで喜べる人は少ないと思います。
実際、JavaでWebアプリケーションなどを作ろうとするとサーバーがApache Tomcatとか、DBがMySQLとか、フレームワークがSpringとか、最初に学ぶには要素が多すぎると思います。
Apacheに至っては私もプログラミングスクールで触って以来触ってないので、今触ろうと思ったらどうやって環境構築すればいいのかわからないです。
Javaは前提を覚えるための前提が必要
例えば、JavaでHello Worldを書くときには以下のようなコードを書きます。
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
これを書くためには、以下のような前提が必要です。
-
()
で囲まれた値はメソッドの引数であること -
""
で囲まれた値は文字列リテラルであること -
public static void main(String[] args)
というメソッドはエントリーポイントなので無ければならない-
(String[] args)
はコマンドライン引数を受け取るもので、使ってなくても書かないといけない
-
-
System.out.println("Hello World")
というメソッドが必要-
System.out
は標準出力を表す
-
といった感じで、Javaを書くためにはJavaの文法だけでなくお作法のようなものを覚える必要があります。
そのため最初にJavaを学ぶ際はこれらのことを「おまじない」と表現して暗記させるということが多いです。
確かにSystem.out
が標準出力を表すことを覚えておけば、Javaの標準出力を使う際には困らないですが、それが何をしているのかはわからないです。
実際にはSystem.out
というようなクラスがあって、その中にprintln
というメソッドがあって、それを呼び出しているということを覚える必要があります。
更にここでクラスやメソッドの概念を覚える必要が出てくるので深さ優先探索のように、掘り続けるようなイメージになってしまうのです。
そのため、Javaを最初に学ぶ際は「おまじない」を暗記して、あとから「アレはこういう意味やったんか」と理解していくというスタイルになりがちです。
Pythonなら1行
一方で、PythonでHello Worldを書くときには以下のようなコードを書きます。
print("Hello World")
これだけです。
Pythonは標準出力を表すprint
というメソッドがあって、それを使って標準出力を行うことが出来ます。
最初に覚えることはprint
というメソッドがあって、それを使って標準出力を行うことが出来るということだけです。
上記で覚える必要があるのは
-
print
というメソッドがある -
()
で囲まれた値はメソッドの引数であること -
""
で囲まれた値は文字列リテラルであること
だけです。
Javaと比べると、Pythonはシンプルな構文で書かれているので、初心者には覚えることが少なくて済みます。
さらに豊富なライブラリを使うとDiscord Bot
なんかも数行で書くことが出来ますし、面倒なことを考えずにプログラミングを楽しむことが出来ます。
それでも低レイヤを意識すると理解が深まる
もっとも、Pythonを勧める理由は「低レイヤを意識しなくてもいいから」というのもあります。
低レイヤを意識しなくてもいいというのは、プログラミング初心者にとっては非常に大きなメリットです。
しかし、低レイヤを意識するとプログラミングの理解が深まるというのも事実です。
例えばJavaで動的にArrayList
に要素を追加する際に以下のようなコードを考えます。
import java.util.ArrayList;
public class HelloWorld {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
// start time
long startTime = System.currentTimeMillis();
for (int i = 0; i < (1 << 28); i++) {
list.add(i);
}
// end time
long endTime = System.currentTimeMillis();
// result
System.out.println("Count:" + list.size() + ", Time: " + (endTime - startTime) + "ms");
}
}
このコードは、ArrayListに1 << 28
個の要素を追加する処理を行っています。
この処理にかかる時間を計測して標準出力に出力しています。
私の環境で実行するとCount:268435456, Time: 5028ms
という結果が出ました。
この実装は基本的に良くない実装です。
なぜなら、ArrayList
は要素を追加する際に要素数が増えると内部で配列を再確保するため、要素数が増えるたびに配列を再確保する処理が走るためです。
これは、ArrayList
は動的にメモリを確保する際にランタイムに対して毎回「メモリくださーい!」とお伺いを立てるためです。
メモリアロケーターくん
メモリアロケーターとは、アプリケーションがメモリを効率良く利用するために動的にメモリを割り当てる仕組みです。
C言語を扱ったことがある人ならmalloc
やfree
といった関数を使ったことがあるかと思いますが、これがメモリアロケーターの一部です。
ちなみに私はC言語を触ったことがないので、malloc
やfree
はエアプです。
それでも、メモリアロケーターについて知っておくと、プログラムがどのようにメモリを確保しているかということが理解できるようになります。
先程のArrayList
の例で言うと、ArrayList
は内部で配列を使って要素を管理しています。
要素を追加する際には、配列のサイズが足りなくなると新しい配列を確保して、古い配列の要素を新しい配列にコピーしています。
この時、JDK8以降では配列のサイズが元の配列の1.5倍にしているので、要素を追加する際には配列の再確保が発生する頻度は減ります。
ArrayList
において、初期のキャパシティは10なので11個目の要素を追加しようとした際には配列の再確保が発生します。
10からスタートした場合 15 -> 22 -> 33 ... -> 354836040 といった感じで43回の再確保をすることになります。
それは遅いよねって話です。
それを踏まえて改善
それを踏まえて、ArrayList
に要素を追加する際には以下のようなコードを書くと良いです。
import java.util.ArrayList;
public class HelloWorld {
public static void main(String[] args) {
final int MAX_LENGTH = 1 << 28; // 予め要素数を定義
ArrayList<Integer> list = new ArrayList<Integer>(MAX_LENGTH); // 初期キャパシティをMAX_LENGTHに設定
// start time
long startTime = System.currentTimeMillis();
for (int i = 0; i < MAX_LENGTH; i++) {
list.add(i);
}
// end time
long endTime = System.currentTimeMillis();
// result
System.out.println("Count:" + list.size() + ", Time: " + (endTime - startTime) + "ms");
}
}
先ほどとほぼ一緒ですが、ArrayList
の初期キャパシティをMAX_LENGTH
に設定しています。
これにより、要素を追加する際に配列の再確保が発生しなくなります。
このコードを実行するとCount:268435456, Time: 2674ms
という結果が出ました。
先ほどの5028ms
から比較するとおよそ半分の時間で要素を追加することが出来ます。
このように、低レイヤを意識することでプログラムのパフォーマンスを向上させることが出来ます。
ある程度低レイヤを意識すると楽しい
このような低レイヤを意識することでプログラミングはパズルゲームのような感覚で楽しむことが出来ます。
先ほどの例では、JavaのArrayList
の内部実装を知ることで、プログラムのパフォーマンスを向上させることが出来ました。
残念ながらPythonのlist
には初期キャパシティを指定することが出来ないので、同じようなことをすることは出来ません。
「じゃあ、意味ないじゃん」と思われるかもしれませんが、最低限実装を意識するということが大事です。
データ構造を使い分ける
例えば、Pythonは糖衣構文で配列の中に任意の値が存在するか?ということを調べるin
演算子があります。
list = [1, 2, 3, 4, 5]
print(1 in list) # True
print(6 in list) # False
これを使うことで、配列の中に任意の値が存在するか?ということを調べることが出来ます。
でも、これを実行する際は最低限全ての要素と比較しているということを考えなくてはなりません。
実際には以下のようなコードを実行しているのと同じです。
list = [1, 2, 3, 4, 5]
for i in list:
if i == 5: # 5が存在するか?
print(True)
break
else:
print(False)
要素数5個の配列であれば5回の比較を行っています。しかし、要素数が多ければ多いほど比較回数が増えていきます。
これはset
を使うことで解決することが出来ます。
list = [1, 2, 3, 4, 5]
set_list = set(list)
print(1 in set_list) # True
print(6 in set_list) # False
set
はハッシュテーブルを使っているため、要素の存在確認をO(1)で行うことが出来ます。
そのため、要素数が多い場合でも比較回数は変わらず、ほぼ1回の比較で要素の存在確認を行うことが出来ます。
こうやって、置き換えられることを探していくことがある種のパズルゲームのような感覚で最適化を行うことが出来ます。
これが楽しめるなら競技プログラミングがおすすめ
もっと楽しみたい!という方はおそらく競技プログラミングがおすすめです。
主にAtCoderなどのオンラインジャッジを使って、プログラミングのスキルを競い合うことが出来ます。
競技プログラミングは、アルゴリズムやデータ構造を使って問題を解くことが求められるため、低レイヤを意識することでプログラムのパフォーマンスを向上させることが出来ます。
以下のサイトで登録してすぐに始めることが出来ます。
以下のサイトではアルゴリズムをわかりやすく学ぶことが出来るので、アルゴリズムに興味がある方はこちらもおすすめです。
なお筆者は登録はしたものの殆ど参加していません(エアプ)。
まとめ
とりあえずプログラミングたのしもうぜ!って話でした。