Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

[Up] [Repository]

本講で学ぶこと

  • while文
  • ループのスキップと脱出
  • 関数
  • スコープ

While文

「10回繰り返したい」という場合にはfor文が使えるが、「ある条件が満たされている限り繰り返したい」という場合もあるだろう。そんなときに使えるのがwhile文である。while文は以下のような構文になる。

while 条件:
    処理

例えば、ある変数が正である限り1をひきながら表示するプログラムは以下のようになる。

a = 10
while a > 0:
    print(a)
    a -= 1

実はPythonでは、整数は0で無い限り「真」、「0」は「偽」として扱われるため、以下のようにも書ける。

a = 10
while a:
    print(a)
    a -= 1

ループのスキップと脱出

forwhileといった「ループを作る構文」を使っていると、ある条件を満たした時にループをスキップしたり、ループから脱出したくなることがある。それぞれcontinuebreakで実現できる。

continue

例えば、0から9までの数字を表示するループを考えてみよう。

for i in range(10):
  print(i)

実行すると、0から9まで表示される。これを、「偶数の時だけ表示する」ようにしたい。そのまま書くと以下のようになるだろう。

for i in range(10):
    if i%2 == 0:
        print(i)

同じ処理を、「奇数の時だけループをスキップする」という形でも書ける。

for i in range(10):
    if not i%2==0:
        continue
    print(i)

continueは、「以下の処理をスキップして、次のループに飛べ」という指示文である。やりたい処理をif文で囲むべきか、やりたくない処理をcontinueで飛ばすべきかは場合による。

continueによる深いブロックの縮小

上記の左では、ある条件が満たされた時に「何かやりたい処理」を実行しているが、右図では条件が満たされなかったらループをスキップして、スキップされなかった場合に「何かやりたい処理」を実行している。どちらも全く同じ処理を行うが、左は「for文が作るブロック」の中の「if文が作るブロック」が大きいのに対し、右では「if文が作るブロック」が小さくなり、「何かやりたい処理」が「for文の作るブロック」に移動している。

このように、処理の冒頭で条件をチェックし、continueしたりreturnしたりすることを「ガード節」と呼ぶ。まるでガードマンが入り口の前に立ち、不要な来訪者を追い払う様に似ているからと思われる。人間の頭はネストした構造を正しく把握するのにコストがかかるため、一般に構造のネストは浅い方が良く、「深い」ブロックは小さい方が望ましい。ガード節はそのような深くて大きいネスト構造を防ぐ基本的なテクニックの一つである。

break

ある条件が満たされたら、ループを終了したい場合は、breakを使う。例えば、所持金5からスタートし、確率1/2で所持金が+1か-1されるギャンブルをしたとしよう。所持金が0になったら負け、10になったら勝ちで、いずれも終了とする。そのようなコードは、たとえば以下のように書ける。

import random
  
money = 5

while True:
    money += random.randint(0, 1)*2-1
    if money == 0:
        print("Lose")
        break
    if money == 10:
        print("Win")
        break

random.randint(0, 1)は、確率1/2で0か1を返す乱数であり、2倍して1を引くことで、+1か-1を返すようにしている。所持金moneyにそれを加算し、0になったら「Lose」と表示して終了、10になったら「Win」と表示して終了している。

ここではwhile True:でループ構造を作っている。while 条件:は、条件が満たされている限りループする、というものであった。その条件にTrueを設定しているので、ループの条件は常に真であり、無限にループが回る。このようなループを「無限ループ」と呼ぶ。無限ループを抜けるには、breakもしくは関数からのreturn、プログラムを終了させるexit()などを使うしかない。もし適切な終了条件を設定せずにループを無限ループにしてしまった場合は、コンソールプログラムならCtrl+Cを入力、Google Colab上ならば実行中のセルの四角いボタンを押せば停止できる。

なお、先程と同じプログラムは、breakを使わずにwhileの条件を工夫することでも実現できる。

import random
  
money = 5

while 0 < money < 10:
    money += random.randint(0, 1)*2-1

if money == 0:
    print("Lose")
else:
    print("Win")

moneyが0から10の間にある場合のみループを実行し、この条件を満たさなくなったらwhile文を終了するコードである。最終的にmoneyが0になったか10になったかを確認し、「Lose」や「Win」を表示している。先程のコードよりも、このループがどのような条件で実行されるべきかがわかりやすくなっているのがわかるであろう。一般に、等価な制御構造の書き方は複数存在する。whileの条件に含めるべきか、breakで脱出すべきかはコードによる。こういったコードの書き方に興味のある人は「リーダブルコード」という古典的な名著があるので参照されたい。

関数

Pythonでは、よく使う処理を「関数」という形で定義し、何度も利用することができる。

def sayhello():
  print("Hello!")

関数は「def 関数名(引数):」という形で定義する。関数定義の右側にある「コロン」を忘れないように。定義した関数は後で何度でも呼ぶこともできる。

sayhello() #=> "Hello!"

関数にインプットを与えることもできる。このインプットを「引数(ひきすう)」と呼ぶ。

def say(s):
  print(s)
say("Bye!") #=> Bye!

関数を実行した結果、値を返すこともできる。返す値はreturn文で指定する。

def add(a, b):
    return a + b
add(3, 4) #=> 7

関数が返した値を変数に代入することもできる。

a = add(1, 2)
print(a) #=> 3

次回学ぶ「タプル」を用いると、複数の値を一度に返すこともできる。

def func(i):
  return i, i+1

a, b = func(5) # a = 5, b = 6が代入される。

スコープ

Pythonはコードブロックをインデントで表現する言語であり、ifforwhileなどがブロックを作ることは既に学んだ。同様に、関数もブロックを作るが、その関数が作るブロック内で宣言された変数の有効範囲は、そのブロック内に制限される。

例えば以下のようなコードを見てみよう。

def func():
    a = 10
    print(a)

func()
print(a)

関数func内で、変数aに10を代入し、その値を表示されている。その後、関数funcを実行すると10が表示され、確かにaに10が入っていることがわかるが、その後でaを表示しようとするとNameError: name 'a' is not defined、つまり「変数aなんて知らないよ」というエラーが出てしまう。

このように、関数内で宣言された変数はローカル変数と呼ばれ、その有効範囲は関数内に制限される。この「変数が見える範囲」をスコープと呼ぶ。ローカル変数が住むスコープをローカルスコープと呼ぶ。

逆に、関数の外で宣言された変数は、関数の中からも見ることができる。

a = 10

def func():
    print(a)

func()

このコードは問題なく実行され、10が表示される。関数の外、つまりインデントがなく、地面に「ベタ」についている場所で宣言された変数をグローバル変数と呼ぶ。グローバル変数はグローバルスコープに住んでいる。

実はPythonには、ここで挙げた「ローカルスコープ」「グローバルスコープ」の他に、「関数内関数のスコープ」「ビルトインスコープ」というものもあるのだが、話がややこしくなるので本講義では取り上げない。

この「ローカルスコープ」と「グローバルスコープ」についてとりあえず知っておくべきことは

  • ローカルからグローバルは見える
  • グローバルからローカルは見えない

の二点である。

スコープ

スコープは意外に難しく、直感に反する振る舞いがいくつかあるのだが、ここでは一つだけ将来ハマりそうなことを紹介しておく。

ローカル変数からグローバル変数は参照可能だが、値を代入しようとすると問題がおきる。こんなコードを考えよう。

a = 10

def func():
  a = 20
  print(a)

func()
print(a)

このコードは

  • 最初にグローバル変数aに10が代入され、
  • 内部で変数aに20が代入して表示する関数funcを実行し、
  • 最後にaの変数の値を表示する

プログラムである。どんな結果になるか想像できるだろうか?

実はこのコードでは、関数funcを実行しても、グローバル変数の値は変更されない。これはa=20が、関数func内のローカル変数の宣言とみなされるからだ。

ローカル変数によるグローバル変数の上書き

多くの場合、これは意図する動作ではないであろう。ローカルスコープからグローバル変数を修正したい場合、ローカルスコープ内でglobal宣言をする。

a = 10

def func():
  global a # 変数aがグローバル変数であることを宣言する
  a = 20
  print(a)  #=> 20

func()
print(a) #=> 20

実行すると、関数内でグローバル変数aの値を書き換えることができたことがわかる。

しかし、このようなコードはバグの元であり、推奨されない。一般に、グローバス変数を使うことそのものが推奨されず、それに伴ってglobal宣言の利用も非推奨である。Pythonには、コードが「ちゃんと」書かれているか確認するツールがいくつかあるが、そのうちの一つであるPylintを使うと、先ほどのコードは「global宣言を使っているよ」と怒られる。

とりあえず「グローバル変数はなるべく使わない」「グローバル変数をいじっていておかしくなったらスコープを疑う」ということを覚えておくと良い。

先に少し触れたように、Pythonは狭い順に「ローカルスコープ」「ネストした外側の関数のスコープ」「グローバルスコープ」「ビルトインスコープ」の四種類のスコープがあり、変数名がぶつかった場合にはどのように扱われるかが決まっている。この変数の名前解決の仕組みを覚える必要は全くないが、Pythonに限らずほとんどのプログラミング言語には「スコープ」という概念があり、中から外の変数を触るとどうなるかは言語によって決まっている、という事実は覚えておいて欲しい。特に、スコープがらみで問題が起きた時に、そもそも「スコープ」という単語を知らないと、ググることもできず、問題解決が難しくなる。

コラッツ問題

ではさっそくwhile文と関数を使ってプログラムを作ってみよう。題材としてコラッツ問題(Collatz problem)を取り上げる。コラッツ問題とは、以下のようなものである。

  • 何か正の整数を考えよ
  • それが偶数なら2で割れ
  • それが奇数なら3倍して1を足せ
  • 以上の処理を数字が1になるまでずっと繰り返せ

たとえば「5」を考えよう。これは奇数なので3倍して1を足すと「16」になる。 これは偶数だから2で割って「8」、さらに2で割って「4」「2」「1」となる。

コラッツ問題とは「上記の手続きを繰り返した場合、すべての整数について有限回の手続きで1になるか?」というものであり、現在も解決されていない。もし有限の手続きで1にならないとしたら、どこかに無限に大きくなるか、ループする構造があるはずだが、今の所どちらも見つかっていない。

課題1

与えられた数字に対して、

  • 偶数なら2でわる
  • 奇数なら3倍して1を足す

という処理を、その数字が1になるまで繰り返しながら表示する関数コラッツを作りたい。 以下の空欄を埋めよ。

de collatz(i):
  print(i)
  while (条件1):
    if (条件2):
      i = i //2
    else:
      i = i * 3 + 1
    print(i)

インデントに注意。最初のprintと二番目のprintはインデントの位置が異なる。

完成したら、関数を入力したセルを実行してから、別のセルに以下のように関数呼び出しを入力、実行し、結果が一致することを確認せよ。

コラッツ(3)
3
10
5
16
8
4
2
1

いろいろな数字を入れて、すべて最終的に1になることを確認せよ。収束するまでの手続きが長い数を探せ。例えば27を入れたらどうなるか。

課題2

コラッツ問題をグラフで可視化してみよう。新しいノートブックを開き、以下のプログラムを4つのセルにわけて入力せよ。

1つ目のセル。

from graphviz import Digraph
import IPython

2つ目のセル。

def collatz(i, edges):
    while (条件1):
        j = i
        if (条件2):
            i = i // 2
        else:
            i = i * 3 + 1
        edges.add((j, i))

3つ目のセル。

def make_graph(n):
    g = Digraph(format='png')
    edges = set()
    for i in range(1, n+1):
        コラッツ(i, edges)
    for i, j in edges:
        g.edge(str(i), str(j))
    g.graph_attr.update(size="10,10")
    g.render("test")

4つ目のセル。

make_graph(3)
IPython.display.Image("test.png")

成功したら、いろんな数字をmake_graphに入れて実行してみよ。20ぐらいがちょうどよいと思うが、27に挑戦してもよい。コラッツ予想とは、このグラフが木構造、つまりループ構造が無いことを主張するものである。

課題3

コラッツ予想には様々な変種がある。例えば、

  • 何か正の整数を考えよ
  • それが偶数なら2で割れ
  • それが奇数なら3倍して 3 を足せ
  • 以上の処理を数字が 1か3 になるまでずっと繰り返せ

というものである。これもやはり有限回の手順で止まるらしい(おそらくこちらも未解決問題)。これを確認してみよう。

まず、先程課題2で作成した課題に名前をつけて保存しよう。ファイルメニューから「名前を変更」をクリック、もしくは上の「Untitled.ipynb」をクリックし、「collatz.ipynb」と名前を変えよう。その後、「ファイル」メニューの「保存」を選ぶと保存される。

この状態で「ファイル」メニューから「ドライブで探す」をクリックせよ。「マイドライブ」の「Colab Notebooks」に「collatz.ipynb」があるはずである。それを右クリックし「コピーを作成」を選ぶと「collatz.ipynbのコピー」というファイルが作成されるので、それを右クリックして「名前を変更」を選び、「collatz2.ipynb」という名前にしよう。「collatz2.ipynb」が作成されたら、右クリック→「アプリで開く」→「Colaboratory」を選ぶことで開くことができる。

「collatz2.ipynb」がGoogle Colabで開かれたら、

  • 数字が1か3になったら終了とする
  • 奇数だったら3倍して3を足す

となるように修正せよ。

修正したら、例えばmake_graph(5)として、1に収束する数字と3に収束する数字があることを確認せよ。 make_graph(50)ではどうなるか?1に収束するのはどういう数字か?

余談:数論について

コラッツ予想に代表されるような、「整数がこの条件を満たすか?」のような問いを扱うのが整数論(数論)である。一般に数論は「問いを理解するのは易しいが、その解決は極めて難しい」という性質を持つ。 ガウスの「数学は科学の女王であり、数論は数学の女王である」という言葉は有名だ。 数論の中でも特に有名なのは「フェルマーの最終定理」であろう。これは「三以上の自然数nについて、x^n + y^n = z^nを満たす自然数の組(x,y,z)は存在しない」という定理である。フェルマーはフランスの弁護士であったが、余暇に行った数学で大きな功績を残し、「数論の父」とも呼ばれる。彼は趣味でディオファントスの著作「算術」の注釈本を読み、その余白に有名な注釈を書き込んだ。その多くは後に証明、もしくは反証されたが、一つだけ証明も反証もされずに残ったのが「フェルマーの最終定理」である。フェルマーが「フェルマーの最終定理」を記述した横に「私はこの定理の驚くべき証明を手に入れたが、ここに書くには余白が足りない」と書いたのは有名である。この問題を解決したアンドリュー・ワイルズは、7年の間、秘密裏にこの問題に取り組み、1995年に解決した。フェルマーによる提唱から証明に至るまで、実に360年かかっている。数論の面白さは、整数しか扱わないにもかかわらず、そこに幾何や解析がからんでくることである。 数論は入門しやすく、一方で極めて奥が深いため、その難しさ、美しさに魅せられて人生を捧げる人も多い。2011年5月にコラッツの弟子が「コラッツ予想を解決した」という論文を投稿した。しかし、すぐに証明の不備が見つかり、6月に撤回された。その論文には、

The reasoning on p. 11, that "The set of all vertices (2n,l) in all levels will contain all even numbers 2n ≧ 6 exactly once." has turned out to be incomplete. Thus, the statement “that the collatz conjecture is true” has to be withdrawn, at least temporarily.

適当な和訳。

「11ページにある証明は不完全であることがわかった。したがって、『コラッツ予想は真である』という主張は、今のところは撤回する」

とある。最後の「at least temporarily (今のところは)」に悔しさがにじむ。サイモン・シンは、こうした数学の未解決問題へ取り憑かれることを熱病に例えた。フェルマーの最終定理に取り憑かれるフェルマー熱、ポアンカレ予想に取り憑かれるポアンカレ熱などが有名だが、この二つは近年解決した。しかし、コラッツ予想は未解決であり、今後もコラッツ熱の感染者を生み続けるのであろう。

このような数学の話に興味のある人は「数学を作った人々 (E. T. Bell著、田中 勇、銀林 浩 訳)」をおすすめする。数学という、一種無味乾燥にも思える学問の構築の裏に、様々な人間ドラマがあったことを知れば、数学を学ぶ楽しさも増えるに違いない。