見出し画像

for文と、if文の練習

はじめに

皆さん、こんにちは。
今回は、プログラミングのコア中のコアである、for文とif文についての話をさせていただこうと思います。


プログラミングとは

そもそものプログラミングに関する説明は、以下の記事にて行っておりますので、よかったら参照下さい。


for文とは

先程紹介した記事の中に記載させていただいているのですが、プログラムの真髄は自動化にあります。
for文とは、その自動化の根幹となるプログラムの命令で、ある一定の処理を、繰り返し自動に行ってもらうべく、論理的に指示を行うためのものです。

自動化というものは、1回だけ自動化しても一般的には価値はなく、繰り返し安定して自動化することで、初めて価値が生まれます。
自動化にかかるコストを、時間をかけて回収していくような形です。
ある意味では、プログラミングというものが、for文を実現するために存在すると言っても過言ではないでしょう。
それほどまでに重要な、プログラミングにおけるキーファクターが、for文となります。

for文の書き方ですが、プログラミング言語のpythonであれば、以下のような記載をします。

for i in range(10):
   
   print('i =', i)

実行結果は以下となります。

画像1

for文によるループが10周行われた形です。
この繰り返しの概念を用いて、同様の指示を繰り返し行うことで、自動化を実現しようという訳です。

例えば、自動運転における歩行者識別の機能などは、カメラから画像を取得し、その画像内に歩行者が映り込んでいるかを判定するものですが。
1秒間に何回も高頻度で、それは行われます。
その繰り返しの処理にも、for文が使われているという訳です。


if文とは

for文によって繰り返しの概念を実現できることとなりましたが、それだけであると自動化できることの幅が広くありません。
自動化には、条件分岐というものがつきものです。
人間でいえば、「こういう場合は、こうする」という状況判断です。
或いは、人にお願いする際に「こういう場合は、こうして欲しい」というお願いの仕方です。
それを実現するものが、if文となります。

機械が状況判断を行えるようになれば、自動化の幅が広がります。
そのためにif文が存在します。
ある意味で、for文とセットで、for文によるループを思い切り良く実行するために存在するのがif文と言えます。
実際に、ITによる自動化の多くが、for文とif文によって成り立っています。

if文の書き方ですが、プログラミング言語のpythonであれば、以下のような記載をします。

a = 10

if (a < 5):
   print('aに格納されている値は、5よりも小さい')    
else:
   print('aに格納されている値は、5以上')

if (a < 15):
   print('aに格納されている値は、15よりも小さい')    
else:
   print('aに格納されている値は、15以上')    

実行結果は以下となります。

画像2

if文によって、条件分岐が行われた結果です。
変数aに格納されている値を参照し、その値の性質によって、実行されるprint文が取捨選択されました。
この取捨選択の概念を用いて、自動化の幅を広げようとするものが、if文となります。

例えば、自動運転における歩行者識別機能であれば、毎時間おきの監視をfor文によって行い、歩行者が画面上に現れてきた瞬間のみ条件分岐によって、歩行者の存在する位置を四角い枠で囲う、という処理を行う形です。
逆に、歩行者が画面上に現れない時には、四角い枠を表示しません。

画像3


for文/if文の練習(1) : トランプタワー

さて、そんなプログラムの根幹を担うfor文/if文ですが、早速練習を行ってみましょう。
例えば、以下のようなトランプで積んだタワーのようなものを作成してみます。

画像4

これを、プログラムで記載すると、以下です。

height = 10

for i in range(height):
   
    for j in range(height - i):
        print(' ', end='')
   
    for j in range(i):
        print('/\\', end='')

    print()

実行結果は以下です。

画像5

それっぽくできました。(笑)
ポイントとしては、トランプを置く枚数と、トランプの置き場所の調整です。
置く枚数は、段が下に行く程増やしていく形です。
置き場所の調整は、スペースによって行いますが、段が上に行く程増やしていく形です。

尚、for文/if文の練習と言いながら、この場合にはif文が登場しませんでしたが、段によって処理内容を切り替えている感覚は、if文に近いものがあるかと思います。


for文/if文の練習(2) : アスキーアートで円を書く

次に、アスキーアートによる円を書いてみましょう。
どこか、適当に中心座標を決めて、任意の半径と、任意の線の太さにて円を描きます。

例えば、以下のようなプログラムです。

canvas_wh  = [30, 30]
center_xy  = [15, 15]
radius     = 10
line_width = 3

for x in range(canvas_wh[0]):
    for y in range(canvas_wh[1]):
       
        dist = np.sqrt((x - center_xy[0])**2 + (y - center_xy[1])**2)
       
        if ((radius - (line_width/2)) <= dist < (radius + (line_width/2))):
            print('@', end='')
        else:
            print(' ', end='')
           
    print()

実行結果は以下です。

画像6

キレイに円が描けました。
パラメータを変えると、円の状態は様々変わります。

画像7

canvas_whという変数で、円を描く座標全体の大きさを決めてあげます。
center_xyという変数で、円の中心座標を決めてあげます。
radiusという変数で、円の半径を決めてあげます。
line_widthという変数で、円を描く線の太さを決めてあげます。

後は、for文でループを回すことにより、各座標毎に@を描くべきかどうかの判断をしていきます。
円の中心からの距離が、「円の半径±線の太さ」の範囲に収まっているか否かで、収まっていれば@を描く、収まっていなければスペースを描く、という風に処理を行います。
その座標毎の条件判断に、if文が用いられています。


for文/if文の練習(3) : 文字列間の編集距離

最後に難しい問題との向き合いとして、文字列間の編集距離の算出をやってみます。
編集距離については、wikipediaに詳しく書かれています。

ザックリ言えば、2文字列間について、「置換/挿入/削除」を何回行えば、片方の文字列に寄せられるか、変換できるか、という回数のことを編集距離と言います。
或いは、発明者の方の名前にちなんで、レーベンシュタイン距離と言います。

具体的には、wikiに載っている以下の例が分かりやすいです。

画像8

尚、この距離の計算方法ですが、自分で書かずとも、pythonのライブラリを使って計算することができます。

import Levenshtein

a = 'kitten'
b = 'sitting'

print('print Levenshtein.distance(a, b) =', 
      print Levenshtein.distance(a, b))

実行結果は以下です。

画像9

wikiに書いてある距離と同じものが算出されました。
合っていそうです。

尚、編集距離とは、最短距離になります。
置換や挿入や削除は、冗長に遠回りしようと思えば、幾らでもできてしまうものですので。
その距離の求め方については、先人が築いて下さった動的計画法によるアルゴリズムが存在し、一般にそれが用いられます。
そして、そのアルゴリズムこそが、for文とif文を駆使したものとなっております。

アルゴリズムの詳細については、wikiにも書いてありますが、以下の記事などが分かり易く、有り難かったです。

改めて、私の方でも、順を追って説明させていただこうと思います。

ある2つの文字列について、編集距離を求める際には、最初に以下のような表を作ります。
比較する文字は、wikiや前述の記事に倣って、「kitten」と「sitting」にします。

イラスト630

先ず、表の意味について、説明をさせていただきます。
この表は、文字数別の編集距離一覧となっています。

例えば、「sitt」と「kitt」との編集距離は、置換を1回行えば合致するので、1となります。
その場合には、以下の位置に1が埋まります。

イラスト630

或いは、「sitten」と「kittin」との編集距離は、置換を2回行えば合致するので、2となります。
その場合には、以下の位置に2が埋まります。

イラスト630

或いは、wikiに倣って、「sitten」と「kitting」との編集距離は、置換2回と挿入1回を行えば合致するので、3となります。
その場合には、以下の位置に3が埋まります。

イラスト630

この表の意味は、以上となります。
尚、「-」は何もない0文字の文字列を表しています。
その為、比較対象の文字列の長さに応じてカウントアップされるような、編集距離の羅列となっています。
「0, 1, 2, 3...」という具合にです。

イラスト630

次に、表の目的ですが、前述の最終的な右下の値を求めるためのものとなります。
実は、表の値については、順繰りに左上から埋めていく方法があり、それを実施すると最終的な右下の値が得られるようになっています。
つまり、「kitten」と「sitting」との編集距離3という、表の一番右下の値を求めるために存在する表となっています。

最後に、具体的にどう埋めていくかのロジックを説明します。
前述の通り、0文字の文字列である「-」に対しては、対応する文字列の長さを埋めていきます。
そうすると、表は以下の状態となります。

イラスト630

この表について、次に埋まっていないマスの内の、一番左上の値に注目をします。

イラスト630

ここには、どんな値が入るでしょうか?
「k」と「s」との編集距離が埋まる為、答えは1となります。
しかし、人が都度考えていては大変ですので、一律の計算式を適用します。

実は、以下の3つの値の内、最も小さいものを埋めると整合する形となっています。

(1)対象マスの左の値に、1を足したもの
(2)対象マスの上の値に、1を足したもの
(3)対象マスの左上の値に、0か1を足したもの[※]

[※]対象マスの左に位置する一文字と、
上に位置する一文字とが同じであれば0、異なれば1を足す

注目しているマスについて、説明をすると。
(1)の意味合いとしては、「k」と0文字の文字列「-」との編集距離が1だった状態から、一方に1文字が追加される形である為、「+1」という最悪の見積もりをしています。
(2)の意味合いとしては、0文字の文字列「-」と「s」との編集距離が1だった状態から、一方に1文字が追加される形である為、「+1」という最悪の見積もりをしています。
(3)の意味合いとしては、0文字の文字列「-」同士との編集距離が0だった状態から、両方に1文字ずつが追加される形である為、最悪の見積もりとして「+1」をしています。
ただし、両方に追加される1文字ずつが、同じ文字である場合には、その追加によって編集距離が増加することは無い為、「+0」とします。
つまり、左上の編集距離を右下にスライドさせます。

尚、注目マスによって求められる(1)(2)(3)の値は、以下となります。

(1)1 + 1 = 2
(2)1 + 1 = 2
(3)0 + 1 = 1[※「k」と「s」が異なる為、+1]

この内、最も小さい値は(3)の1である為、「k」と「s」との編集距離を埋めるマスには、1をセットします。
実際の編集距離とも合っています。

イラスト630

次に、今の右隣のマスに注目をズラします。

イラスト630

この注目マスにて求められる(1)(2)(3)の値は、以下となります。

(1)1 + 1 = 2
(2)2 + 1 = 3
(3)1 + 1 = 2[※「k」と「i」が異なる為、+1]

この内、最も小さい値は(1)(3)の2である為、「k」と「si」との編集距離を埋めるマスには、2をセットします。
実際の編集距離とも合っています。

イラスト630

次に、今の左下のマスに注目をズラします。

イラスト630

この注目マスにて求められる(1)(2)(3)の値は、以下となります。

(1)2 + 1 = 3
(2)1 + 1 = 2
(3)1 + 1 = 2[※「i」と「s」が異なる為、+1]

この内、最も小さい値は(1)(3)の2である為、「k」と「si」との編集距離を埋めるマスには、2をセットします。
実際の編集距離とも合っています。

イラスト630

次に、今の右隣のマスに注目をズラします。

イラスト630

この注目マスにて求められる(1)(2)(3)の値は、以下となります。

(1)2 + 1 = 3
(2)2 + 1 = 3
(3)1 + 0 = 1[※「i」と「i」が合致する為、+0]

この内、最も小さい値は(3)の1である為、「ki」と「si」との編集距離を埋めるマスには、1をセットします。
実際の編集距離とも合っています。

後は、このルールに則って、左上から順繰りに値を埋めていくと、以下の表のように値が埋まります。

画像23

こうして値が埋まった後、最後に一番右下の値をピックアップすると、「kitten」と「sitting」との編集距離3が得られます。

さて、アルゴリズムの説明は以上となります。
それでは、このアルゴリズムをプログラムに起こしましょう。
結論としましては、以下となります。

import numpy as np

# prepare 2 strings
str_A = 'kitten'
str_B = 'sitting'

# get length
len_A = len(str_A)
len_B = len(str_B)

# make table of levenshtein distance
ld_table = np.zeros([(len_A + 1), (len_B + 1)], dtype=np.int)

# set levenshtein distance with null
for vert_i in range(np.shape(ld_table)[0]):
    ld_table[vert_i][0] = vert_i

# set levenshtein distance with null
for horz_i in range(np.shape(ld_table)[1]):
    ld_table[0][horz_i] = horz_i

print('ld_table =\n', ld_table)
print()

# loop of browsing table
for vert_i in range(1, np.shape(ld_table)[0]):
    for horz_i in range(1, np.shape(ld_table)[1]):

        # (1)対象マスの左の値に、1を足したもの
        cost_insert = ld_table[vert_i - 1][horz_i] + 1

        # (2)対象マスの上の値に、1を足したもの
        cost_delete = ld_table[vert_i][horz_i - 1] + 1

        # (3)対象マスの左上の値に、0か1を足したもの[※]
        if (str_A[vert_i - 1] == str_B[horz_i - 1]):
            # [※]対象マスの左に位置する一文字と、
            #       上に位置する一文字とが同じなので、0を足す
            cost_replace = ld_table[vert_i - 1][horz_i - 1]
        else:
            # [※]対象マスの左に位置する一文字と、
            #       上に位置する一文字とが異なるので、1を足す
            cost_replace = ld_table[vert_i - 1][horz_i - 1] + 1

        # select minimum cost
        cost_min = np.min([cost_insert, 
                           cost_delete, 
                           cost_replace])

        # update table
        ld_table[vert_i][horz_i] = cost_min

print('ld_table =\n', ld_table)
print()

実行結果は以下です。

ld_table =
[[0 1 2 3 4 5 6 7]
[1 1 2 3 4 5 6 7]
[2 2 1 2 3 4 5 6]
[3 3 2 1 2 3 4 5]
[4 4 3 2 1 2 3 4]
[5 5 4 3 2 2 3 4]
[6 6 5 4 3 3 2 3]]

しっかりと、編集距離が求まっています。
ポイントとしては、事前にアルゴリズムを腑に落としておき、それをプログラムに積み上げた点です。
いきなりプログラミングに取り掛かるのではなく、先ずはしっかりとアルゴリズムを把握する、或いは、机上で設計を済ませておくと、割と迷わずに実装することができます。


for文/if文を実装するコツ

さて、3つのfor文/if文による実装例を紹介させて頂きました。
しかし、仕上がったものを見るだけだと、「ふーん…」という感じで響かないかもしれません。
そこで、私が思うfor文/if文プログラムの実装のコツについても、恐縮ながら書かせていただこうと思います。

私が思うfor文/if文プログラムの実装のコツは、大きく以下3つです。

(1)極力頻繁に、デバッグを行う
(2)添字「i」に単一の値をセットして、試してみる
(3)for文の中で、変わるもの/変わらないを整理する

以降で、1つずつ説明していきます。


for文/if文を実装するコツ(1) : 極力頻繁に、デバッグを行う

デバッグについては、プログラムの基本としてよく言われることです。
要するには、print文のことです。

例えば、先程の編集距離を計算するプログラムの実装にて、途中途中にprint文を挟み、意図するデータが格納されているかを確認します。
例えば、以下のような要領です。

画像24

画像25

極力頻度高く、print文を実行します。
そして、printした結果が想定と異なれば、すぐに修正を行います。
そうすることで、実装のミスを削減することができます。

プログラミングの速さ、プログラムの実装の速さについては、私の経験上ですと、ミスをしないことが最も肝要です。
ミスをしないことが、最もプログラミング速度を向上させると思います。
print文の実装は冗長で煩わしく、一見するとプログラミング速度の低下を招いているように感じてしまうかもしれません。
しかし、実際には、print文を沢山書くほど、プログラミング速度は向上すると思います。
プログラムの動作確認を、小さなピッチでぐるぐる回しながら、足場を固めつつ進んでいくイメージです。
これは、アジャイル開発などにも共通する概念かと思います。
或いは、飲食業における味見や、営業職におけるアイスブレイク、交通事業における車両整備/機体整備などとも共通するかと思います。
「急がば回れ」です。
特に、for文/if文は、ミスが混入しやすいので尚更です。

ちなみに、ミスが潜伏すると大変です。
仮に、プログラムを仕上げる速度が早かったとしても、そこからのバグ解消に膨大な時間がかかります。
プログラムの構造自体が芳しくないと、プログラムを全て破棄するという最悪の事態も有り得ます。
そういった事態を未然に防ぐために、デバッグを極力多めに実施するのです。
恐らく、多過ぎると思うくらいに実施するのが良いかと思います。
また、鋭さよりも量が重要です。
量は配慮漏れをカバーするためです。


for文/if文を実装するコツ(2) : 添字「i」に単一の値をセットして、試してみる

次に紹介するコツですが、百聞は一見に如かずということで、先ずは実践を見て下さい。
以下の要領です。

画像26

for文、及び、そのループの中に含まれるif文については、いきなり実装する方が多いかと思います。
そこを、先ずは1周回して、上手く動いているか確認してみるのです。

そして、1周回して上手く動いているようであれば、もう1周回してみます。
それも、上手く動いているようであれば、もう1周。
そんな要領で何周か回してみて、問題無さそうであれば、本格的にfor文のループを実施します。
そうすることで、for文で括られたコード内にバグが混入しづらくなります。

或いは、for文から外すことで、デバッグもしやすくなります。
以下の要領です。

画像27

for文をぐるぐる回しながらですと、デバッグがしづらかったりしますが、回っていない状態だと、デバッグがしやすいかと思います。
こうして、小規模なコードの組み合わせが上手く行っていることを積み上げると、大規模なプログラムでも上手く動く可能性が上がる形となります。


for文/if文を実装するコツ(3) : for文の中で、変わるもの/変わらないを整理する

ここまでで紹介した2つのコツについてですが、共通点があります。
それは、人為的なミスを芽が小さい内に摘み取るという点です。
そうする為の工夫として紹介させていただいたのが、(1)と(2)のコツでした。
或いは、もう少し俯瞰してみると、意外と人間の思考整理がいい加減であると、そこに対して対処するのがコツであると、そんな形です。
その意味では、この3つ目のコツも同じです。
或いは、1つ目、2つ目のコツを、更に後押しするようなコツとなります。

この3つ目のコツは、for文のスコープの中で、変わるもの/変わらないものを整理して、プログラミング脳の整理を行うものです。
整理をしてから取り組むと、圧倒的に実装の見通しは明るくなります。
しかし、意外と、ここのところを整理せずに、プログラミングに取り掛かってしまっている人は多いかと思います。

例えば、最初に紹介したトランプタワーのプログラムで、このコツを実践してみます。

height = 10

for i in range(height):
   
    for j in range(height - i):
        print(' ', end='')
   
    for j in range(i):
        print('/\\', end='')

    print()

先ず、for文のスコープの中で変わるものです。
for文のスコープの中で変わるものを、もう少し厳密に定義すると、for文の実施前に定義した変数で、かつ、for文の中で代入の左辺にあり、変更が加えられているものです。
上記プログラムでは、そういうものは存在しません。
尚、そのような場合は、for文のループで回る毎の処理が、前のループや後のループに関連せず独立している為、比較的簡単な部類となります。
ループ一周一周を別々に捉えて、淡々と想像すれば良い為です。

尚、for文のスコープの中で変わらないものとしては、heightがあります。
for文のスコープの中で変わらないものについても、厳密に定義すると、for文の実施前に定義した変数で、かつ、for文の中で代入の左辺にはなく、変更が加えられていないものです。
それが、上記の例でいうとheightとなります。
変わらないものについては、考え過ぎる必要はありません。
「この変数は変わらないんだ」と意識すれば問題ありません。
よくありがちなのは、イメージとして変化が著しいものが変数として存在する場合に、実際には変化がない為に意識しなくても良いところを、不当に意識し過ぎてしまうことです。
それを避けるために、未然に思考整理をする形です。

アスキーアートの円を書くプログラムについても、コツを実践してみましょう。

canvas_wh  = [30, 30]
center_xy  = [15, 15]
radius     = 10
line_width = 3

for x in range(canvas_wh[0]):
    for y in range(canvas_wh[1]):
       
        dist = np.sqrt((x - center_xy[0])**2 + (y - center_xy[1])**2)
       
        if ((radius - (line_width/2)) <= dist < (radius + (line_width/2))):
            print('@', end='')
        else:
            print(' ', end='')
           
    print()

このプログラムも、トランプタワーのプログラムと同様に、変わるものが無く、変わらないものが幾つか存在するという形です。
その為、思考整理としては、ループ1週毎を独立して考慮すれば良い形になります。

さて、最後に編集距離のプログラムについても、コツを実践してみましょう。

import numpy as np

# prepare 2 strings
str_A = 'kitten'
str_B = 'sitting'

# get length
len_A = len(str_A)
len_B = len(str_B)

# make table of levenshtein distance
ld_table = np.zeros([(len_A + 1), (len_B + 1)], dtype=np.int)

# set levenshtein distance with null
for vert_i in range(np.shape(ld_table)[0]):
    ld_table[vert_i][0] = vert_i

# set levenshtein distance with null
for horz_i in range(np.shape(ld_table)[1]):
    ld_table[0][horz_i] = horz_i

print('ld_table =\n', ld_table)
print()

# loop of browsing table
for vert_i in range(1, np.shape(ld_table)[0]):
    for horz_i in range(1, np.shape(ld_table)[1]):

        # (1)対象マスの左の値に、1を足したもの
        cost_insert = ld_table[vert_i - 1][horz_i] + 1

        # (2)対象マスの上の値に、1を足したもの
        cost_delete = ld_table[vert_i][horz_i - 1] + 1

        # (3)対象マスの左上の値に、0か1を足したもの[※]
        if (str_A[vert_i - 1] == str_B[horz_i - 1]):
            # [※]対象マスの左に位置する一文字と、
            #       上に位置する一文字とが同じなので、0を足す
            cost_replace = ld_table[vert_i - 1][horz_i - 1]
        else:
            # [※]対象マスの左に位置する一文字と、
            #       上に位置する一文字とが異なるので、1を足す
            cost_replace = ld_table[vert_i - 1][horz_i - 1] + 1

        # select minimum cost
        cost_min = np.min([cost_insert, 
                           cost_delete, 
                           cost_replace])

        # update table
        ld_table[vert_i][horz_i] = cost_min

print('ld_table =\n', ld_table)
print()

このプログラムについては、前2つのプログラムと異なり、変わるものが存在します。
for文の実施前に定義されて、for文の中でうねうねと変化し、それがループの中で頻繁に参照されるようなものです。
このプログラムにて、for文の中で変わるものは、編集距離を管理する表である変数「ld_table」です。

変わるものが存在すると、ループの1周が次の1周、及び、その次の1周にと影響を与えていくことになります。
そうすると、for文にて行われる処理を想像することが難しくなります。
難しくなるのはしょうがありません。
難しくなることを認識することが、先ず大事です。

一方で、他にも変わる対象が無いかを考えてみると、他には無いようです。
となると、プログラミングの際の集中力は、大半を「ld_table」に割くことでバグ混入を防げそうです。
このような思考整理、集中ポイントの絞り込みも、非常に大事です。
或いは、デバッグを行うべきタイミングや、対象、観点などもクリアになってきます。
途方に暮れる感じはしないかと思います。

よく、for文やif文などが壁で、プログラミングが嫌になってしまう人がいるかと思います。
しかし、この記事で書かれているような向き合い方をすれば、そう嫌になってしまう程の混乱は、招かずに済むかと思います。
或いは、躓く人ほど地頭の良い人で、いきなり一発解決しようとしてしまう為に、処理にバグが混入してしまう傾向があろうかと思います。
そんな時は少し歩を緩めて、着実に積み上げるような形で、ストレス軽めに実装を進めていくと、決してプログラミングが嫌になることもないのかなと、個人的には思う次第です。


おわりに

ここまで読んで下さった方、ありがとうございました。🙇
プログラミングを学んでいる方の後押となれましたら幸いです。

この記事が気に入ったらサポートをしてみませんか?