Python+Jupyterによるプログラミング ほんの入り口

櫻川貴司 

ここではJupyter Notebookでプログラミング言語Pythonを用いて演習を行います。少ない時間でプログラムとは何であるかを原理的に理解することと、文房具としてのプログラミングをある程度マスターすることを目的としています。本格的なプログラミングを学ぶためにはそれぞれのプログラミング言語の演習を履修することをお勧めします。

Jupyter Notebookとはプログラムとその実行結果(値(文字列含む)、グラフ、表形式)、文章(説明)を混在させて編集・表示を行えるツールです。Jupyterのデータの処理を行うプログラムはサーバ方式で動作し、それにwebブラウザから接続して使います(OSのメニュー等からJupyter Notebookを起動すれば自動的にブラウザが立ち上がってローカルに動作するJupyerのサーバに自動的に接続します)。メディアセンタのWindowsにはプログラミング言語Pythonの処理系の一つであるAnacondaがインストールされており、それにはJupyterが含まれています。フリーソフトウェアであり、BYODのPCにも無料でインストール可能です。複数のversionのPythonをそれぞれ含んだAnacondaが提供されているので、資料で使っているPython3の方をインストールしてください。

Jupiter Notebookの起動のし方・利用方法

これについては別ページを参照してください。

学習方法

このページを表示しながら、プログラムのコード部分(In [ ]:の右側枠内の部分)を順にJupyterのセル(やはり枠内の部分。プログラムなのでCode属性にする)に手入力するかコピー&ペーストし、実行します。或いは授業中に提示する予定の、notebookのデータファイルをuploadすることで、直接実行することが可能になります。但し提出課題の部分は自分で入力するなり変更するなりする必要があります。

課題の提出方法

課題の回答を作成して動作確認したらコピー&ペーストによりプレーンテキストのファイル等として提出してください(次回以降も同様)。

ファイル名: 課題提出用ID-課題番号.拡張子

ファイル名の例: e20000-kadai09.py

注意点:

  • 適宜コメントを記述してください。
  • プログラムについては、提出前に動作することを必ず確認してください。
  • プログラムの回答は、別ファイルにコピー&ペーストし、プレーンテキストとしてください。
  • 冒頭にもコメントとして課題提出用IDと課題番号を記してください。
  • ファイル中に、第三者が個人を特定できる情報を書かないでください。
  • 上記注意点が守られていない場合、評価対象とならない場合があります。
  • 画像等の結果のファイルの場合もあります。内容に対応した拡張子にしてください。
  • 提出の最終期限は別にお知らせします。

最初のプログラム

下の「1+2」はこれで一つのプログラムです。これを実行する(評価する)にはマウスでクリックして下のセルを選択し、(存在する場合は)ウィンドウ上方の[>|]のボタンをクリックするか、SHIFT+returnキーを押します。Out[..]: 3というように値3が表示されれば正常に動作しています。

In [1]:
1+2
Out[1]:
3

次のセル中に加減乗除(+-*/)による数式を書き、同様に実行してみてください。セルの中を書き直して再実行できます。

In [2]:
10+4*(3/4+100)+101
Out[2]:
514.0

これはPython(チュートリアル)というプログラミング言語のプログラムとして対話的に実行しています。一応ウィンドウ右上に[Python 3]などと表示されていることを確認してください。プログラムの実行の仕方としてバッチ式と対話式という方式があり、前者は(複数の時もありますが)一つの定まったプログラムにデータを与えて処理を行う方式です。後者はプログラムの処理系にプログラムの断片を入力しつつ、対話的に小さいプログラムの実行と結果の表示を繰り返し行う方式です。

対話的に実行する方がとっつきやすいので、この資料では対話的に実行する方法で解説を行います。ただし、Jupyterの場合セルごとに実行を行うため、実行の順序によって処理系(プログラムの実行を行うプログラム達。本体部分はかなりの部分が機械語でできていたりします。実行する対象のプログラムはPython等の、人間が理解しやすい高級言語で書かれたものです。大きく分けてコンパイラ方式とインタプリタ方式があります)の状態が異なる場合があります。また、そもそも実行していないセルがあると、その内容に依存する他のプログラムの断片がうまく動作しない場合がありえますので注意が必要です。これについては後で説明します。

ここでPythonのversionを確認しておきます。importというのはライブラリ(モジュール(大抵の場合一つのファイルにまとめられたプログラム)やパッケージ(いくつかのモジュールを纏めたもの))を処理系に読み込むための命令です。以下ではsysを読み込み、その中のversion_infoの値を参照します。

In [3]:
import sys
sys.version_info   # この資料では**Python3**を使って解説している
Out[3]:
sys.version_info(major=3, minor=6, micro=1, releaselevel='final', serial=0)

Pythonの言語の(major) versionは2と3でかなり違う部分があり、この資料ではPython3について記述しています。先のチュートリアル等を参照する際にはversionが近いものを参照してください。

変数

変数はほとんどのプログラミング言語において重要な概念です。変数(variable)とは、何らかのデータを格納する箱だと思ってください。箱には名前があり、それを変数名と言います。PythonにはPEP-8という標準ライブラリについてのコーディング規約があり、それに従うなら変数名は通常英小文字と「_」(アンダースコア)の列とします。後で出てくるように違う箱だが同じ名前が与えられる場合があり、その場合についての理解が必要です。

また、Pythonを含めて手続き型のプログラミング言語では変数の値をプログラムの実行途中で変更することができ、それが計算の状態変化を表します。このようにプログラムの実行途中で値が変化することが可能な変数をmutableな変数と言います。変数の箱に値を入れることを代入(assignment)と言い、Pythonの場合には=を使って表します。結果的に左右が等しくなるとは限らず、数学での等式を表しているのではないことに注意してください。

変数には種類があり、どのような種類の変数が使えるかはプログラミング言語により変わります。しかしまずここではそれらのうち大域変数(global variable)という、プログラム(ファイル内)のどこからも参照できる(例外があります)変数のみが出てきます。大域変数についてはそれらの箱はプログラムの実行開始時(あるいはその変数が存在することがわかった最初の時点)からプログラムの実行終了時まで存在し続けます。

以下、上から順番に各セルを実行してみてください。例えばx=式、で式の値を計算し、結果をxに代入します。次の行のxはPythonでは代入文が値を持たないためです。なお#から右側はコメントとして扱われ、この部分は実行されません。

In [4]:
x=11 # 変数xに11を**代入**する
x
Out[4]:
11
In [5]:
y=23
y
Out[5]:
23
In [6]:
z=x*y
z
Out[6]:
253
In [7]:
x
Out[7]:
11
In [8]:
z*z
Out[8]:
64009
In [9]:
x=12
x
Out[9]:
12
In [10]:
z=x*y
z
Out[10]:
276
In [11]:
z+=3 # z に3を加える
z
Out[11]:
279
In [12]:
z*=2 # zの値を2倍する
z
Out[12]:
558

この例のように、変数には値を格納することが出来ます。その後の命令の列が全く同じでも、変数の値によって計算は変わってきます。上から順に実行した場合には最初xの値が11であり、それに依存して決まるzの値は最初253になります。しかしその後xの値が12に変更されてからz=x*yという同じ命令によってzの値は276という異なる値になります。変数は電卓のメモリと同じ用途で用いることが可能です。

上から順に実行した場合、最初のz=x*yzの値は253になりますが、その後下の方の命令を実行してxの値が異なる値に変化してから(下の方のではなく)最初と同じセルのz=x*yを実行すると、zの値は異なるものとなります。また、x=11x=12を実行する前にz=x*yを実行するとxの値が定義されていないためにエラーになります。このようにセルの実行順序によってエラーが起きたり変数の値を含めて状態が変化することに注意してください。また、z+=3のように実行の度にzが増加するため実行回数が影響を与える場合もあります。上から順に実行すれば実行回数に関わらず正しく動くようなnotebookとするのが望ましい場合が多いです。

Pythonでは数値以外にも様々なデータを扱います。例えばデータの列であるリストや文字列などがあります。データの種類のことを一般にデータ型といいます。プログラムの実行前に式が表すデータの型が定まるようなプログラミング言語を静的型付けの言語と言います。それに対し、データ型が実行前に決まらず、実行時にチェックされる言語を動的型付けの言語と言います。Pythonは後者になります。

静的型付けの言語はプログラムの実行前に、ある種のプログラムのミス(BUG)がないことを自動的にチェックできます。特に強い型付けという種類に分類される言語では、実行前のチェックを通っていればデータ型に関するエラーが実行時に起きないことが保証されます。ただしプログラミングを学ぶ際にデータ型に関する理論をある程度学ぶ必要がありますし、十分広い枠ではありますが、一定の枠に嵌ったプログラムのみを書けることになります。Pythonは動的型付けの言語なのでデータ型のエラーの実行前のチェックは行われず、実行時に検出されます。

条件判断

ここまではほとんど電卓と同じように計算を行ってきただけでした。しかし今から出てくる条件判断と繰り返しは、通常の電卓にはないものであり、計算機を有用な道具とするには必須のものです。Pythonの条件文は以下のような構文になります。改行とインデント(字下げ)によりプログラムの区切りを表すのはPythonの特徴であり、改行や字下げがそれほど意味を持たないプログラミング言語も多数あります。そのような言語では改行や字下げを入れる理由は専ら人間にとっての読みやすさのためです。

構文:  
if 条件式:  
    文1   # 条件がTrueであればelseまでが実行される。インデント(字下げ)が必要  
    文2   # 続けて実行する行は上と同じインデントとする  
    .
    .
else:   # ifと同じインデントとする  
    文a   # 条件がFalseであればifの終わりの境界までが実行される。インデント(字下げ)が必要  
    文b   # 続けて実行する行は上と同じインデントとする  
    .
    .
次の命令   # インデントを元に戻す(ifと同じ)。条件文の実行終了後に実行される

通常、条件式の部分には等しいかどうか(==,!=)大小関係等(<,>,<=,>=)やそれらの組み合わせ(and,or,notで組み合わせる)等、真偽値をとるような式を記述します。なおelse:から次の命令の前までの行はなくても構いません。その場合は条件式がFalseであった場合、条件文内では追加で何も実行されません。またelse:後の文a...の部分がまた別のif文になっている場合、インデントのレベルを深くしないための書き方(elif)があります。これに付いては後で出てきます。

次の2つの具体例では条件部分の真偽により式の値が変化します。==は両辺が等しい時True、等しくない時Falseとなります。

In [13]:
if 1 == 1:
    x=1
else:
    x=2

x
Out[13]:
1
In [14]:
if 1 == 0:
    x=1
else:
    x=2

x
Out[14]:
2
In [15]:
print(1==1,1==0,1!=0,1>2,True or False, True and True, not True)  # printは引数(ひきすう、argument)の値を出力する。
True False True False True True False

繰り返し

Pythonで繰り返し処理を記述する方法は多数あります。ここではそれらのうちの一つ、forによるものを紹介します。

構文:  
for 変数 in 範囲:  
    文1 # 変数の値を範囲の中で変えてforの境界までの文が繰り返し実行される。
    文2 # インデント(字下げ)が必要。続けて実行する行は上と同じインデントとする。
    .  
    .  
次の命令 # インデントを戻す(forと同じ)。空行は無視される。for文の終了後に実行される。

なおif文やfor文は入れ籠にすることができます。その場合Pythonではインデントを合わせることで実行する文の範囲を指定します。

In [16]:
for i in range(1,100):    # range(1,100)は1から99までを意味する。
    print(i,end=' ')      # end=' 'はiの出力後に改行ではなく空白を出力する指定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

同時代入

代入の際に、「,」で区切って複数個の変数に同時に代入できます。これにより例えば変数の値の交換を簡単に記述できます。

In [17]:
x = 1
y = 10
x,y = y,x    # 同時代入
print(x,y)
10 1

函数定義

新しい函数を定義することが可能です。例えば二次方程式$ax^2+2bx+c=0$の解(の一つ)を求める函数quadraticは以下のように定義できます。

In [18]:
import math    # この例では函数定義の中で使用するため、平方根の値を求める函数等が定義されたパッケージを読み込む。sqrtを使わなければ必要ない。
def quadratic(a,b,c):   # Pythonの函数名・変数名は通常英小文字と「_」の列とする(PEP-8コーディング規約)
    return (-b + math.sqrt(b**2 - a*c))/a    # 返り値。a**b はaのb乗
In [19]:
quadratic(1,1,1)
Out[19]:
-1.0

Pythonの函数定義は以下のようなものになります。

構文:  
def 函数名(引数1,引数2,...):  
    文1        # インデントが必要。
    文2        # インデントを合わせる。
    .  
    .          # returnにより式の値を(返り)値とし函数の実行を終了する。
    return 式  # returnは複数箇所に現れる場合もある。

ここで引数1,...は函数に渡されるデータが格納される変数で、仮引数といいます。これらに対し、実際に函数に渡される具体的なデータを実引数といいます。仮引数は局所変数(local variable)であり、それらの「箱」は函数の値を求める間にのみ一時的に用意され、函数定義内部でのみ利用可能な変数となります。変数の有効範囲のことをその変数のスコープと言います。函数定義のdefにより新しいスコープができます。

以下の例はスコープから外れているために変数が未定義となる例です。

In [20]:
def test1():
    newvariable_fun = 1

test1()
newvariable_fun
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-20-77e9a4279dd2> in <module>()
      3 
      4 test1()
----> 5 newvariable_fun

NameError: name 'newvariable_fun' is not defined

局所変数と同じ名前の大域変数がある場合、それらは函数内部からは基本的に参照できなくなります。大域変数と局所変数で同じ名前の変数がある場合、あるいは函数呼び出しが何重にも重なった場合に同じ変数名の変数が出てくる場合があります。それらは変数名が同じなだけで、「箱」としては別物であることに注意してください。

In [21]:
x = 0
def test2():
    x = 1    # これは局所変数。

test2()
x            # 同じ変数名だが別の箱。
Out[21]:
0

例題: フィボナッチ数列と階乗

ここまでの知識で定義できる函数の例題を見てみます。

フィボナッチ数列とはfib(0)=0, fib(1)=1, fib(n)=fib(n-2)+fib(n-1)として再帰的に定義される数列です。自然界のいろいろなところで現れることが知られています。

In [22]:
def fib(i):
    x,y = 0,1
    for i in range(0,i):
        x,y = y,x+y
    return x
In [23]:
fib(0),fib(1),fib(2),fib(3),fib(4),fib(5),fib(6)    # この式は値の組みを結果とする
Out[23]:
(0, 1, 1, 2, 3, 5, 8)
In [24]:
def fact(i):    # 階乗を計算する函数
    y = 1
    for i in range(1,i+1):
        y *= i
    return y
In [25]:
[fact(0),fact(1),fact(2),fact(3),fact(4),fact(5),fact(6)]    # この式はリストを結果とする。[]ではなく()にして組みとしてもよかった。
Out[25]:
[1, 1, 2, 6, 24, 120, 720]
In [26]:
fact(300)   # 任意多倍長整数演算を行える。5000!等も問題なく計算できる。
Out[26]:
306057512216440636035370461297268629388588804173576999416776741259476533176716867465515291422477573349939147888701726368864263907759003154226842927906974559841225476930271954604008012215776252176854255965356903506788725264321896264299365204576448830388909753943489625436053225980776521270822437639449120128678675368305712293681943649956460498166450227716500185176546469340112226034729724066333258583506870150169794168850353752137554910289126407157154830282284937952636580145235233156936482233436799254594095276820608062232812387383880817049600000000000000000000000000000000000000000000000000000000000000000000000000

グラフ

ここでグラフを描く方法を書いておきます。レポートを作成する際などに大変便利な機能であり、定規や色鉛筆などを超える文房具としてプログラムを利用することになります。ただしこの資料の今の進度よりは相対的に難しい概念が出てくる部分があります。

In [27]:
%matplotlib inline
                        # グラフをnotebookに表示する指定

import numpy as np      # 数値計算ライブラリnumpyを読み込み、npとして参照する
import matplotlib.pyplot as plt    # グラフを描くためのライブラリmatplotlib.pyplotを読み込み、pltとして参照する
r = range(0,15)
plt.scatter(r,list(map(fib,r)))    # rangeの各値にfibを適用したリスト(後述)を作り、それにより点をプロットする
Out[27]:
<matplotlib.collections.PathCollection at 0x10fc21b00>

0,15を他の値に変えれば範囲が変わり、fibを他の函数に変えれば表示する函数が変わります。函数定義を他で行なってそれを実行し(読み込ませ)てから変更した上のプログラムを実行すればグラフが書き換わります。

ここでmap(fib,r)は範囲rの各値に対し函数fibを適用した結果の列を作ります。listでそれをscatterの引数として許される形に変換しています。scatterは2つの列を引数とし、それらを座標として点を平面にプロットします。

次の例はもう少し実用に近い例であり、どのように指定すれば表示がどうなるのかわかると思います。

In [28]:
r = range(0,15)
plt.scatter(r,list(map(fib,range(1,16))),color="red",label="fibonatti(x+1)")    # colorで色の設定。このように複数のグラフを合成できる。
plt.plot(r,list(map(fact,r)),label="factorial(x)")   # こちらは階乗の対数の折れ線グラフ。値の差が大きいので各値の対数を計算している。
plt.legend()                                         # 凡例表示。アルファベットと同様に書くだけでは日本語はうまく表示されなかったりする。
plt.xlabel("x")                                      # x軸ラベル
plt.ylabel("value")                                  # y軸ラベル
plt.title("fibonatti and factorial")                 # タイトル
plt.yscale("log")                                    # y軸はlogスケール

グラフではなく表形式で表示してみます。

In [29]:
import pandas as pd    # このセルは函数定義のセル達の後に実行する
r = range(0,15)
pd.DataFrame([list(map(fib,r)), list(map(fact,r))],columns=r,index=("fib","fact"))
Out[29]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
fib 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
fact 1 1 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600 6227020800 87178291200

キーワードの補完

Jupyterに限らず、ある種のキーワードを補完する機能がついている場合があります。Jupyterの場合には[TAB]キーにより補完を行います。 これはPython以外のkernelの場合にも大抵使える機能です。 Codeセルでキーワードを途中まで打ち込んで[TAB]キーを押すとそれを先頭とするキーワード候補の選択メニューが表示され、選択できる状態になります。

In [30]:
# 次の行に例えば「r」を入力し、`[TAB]`キーを押すとrで始まるキーワード達の選択メニューが表示される。
r
Out[30]:
range(0, 15)

completion-python.png 候補が多数ある場合にはスクロールして選択します。 逆にもしも候補が一つもない場合には何も表示されません。 一つしかない場合にはその候補の残りの文字列が自動入力されます。 この機能を指して補完(completion)と呼びます。 長いキーワードを入力する場合に一意的になるまでキー入力後、 [TAB]により補完するとキーストローク数が減り、 誤りも減らせます。

函数・変数等の説明

Python kernelの場合、函数名や変数名の前後どちらかに「?」を入力して[SHIFT]+↩︎を押すと、 それについての説明が下部に表示されます。 これはPythonの場合に使える機能です。

In [31]:
# 変数内容、函数の説明の例
?print

ウインドウ下部に表示される説明: explanation.png

自分で定義した函数の場合にはソースを参照できるため、 ?の位置に代わりに??を付けると函数のソースを表示します。

In [32]:
# 例:
??fact

最初の課題

ここまでに学習した事柄を使って、引数として与えた2つの整数を含めてそれらの間のすべての整数の和を求める函数sumをPythonで記述してプレーンテキストのファイルにコピー&ペーストして提出せよ。ただし数値演算は和と差のみしか使ってはならない。課題の提出先は従来と同じ。「課題提出用ID-kadai07.py」というファイル名にし、ファイルの冒頭にコメントとして課題提出用IDとkadai07を入れること。

In [33]:
# kadai07 e0123    課題提出用IDを書き換え
#
def sum(x,y):
    return x+y    # 正しいプログラムに書き換えること

print(" ", sum(-2,1), sum(1,-3), sum(10,13), sum(-100,0))
# -2 -5 46 -5050
  -1 -2 23 -100

再帰呼び出し

これまでに、フィボナッチ数列や階乗計算のプログラムを例題として示して解説しました。それらは元々再帰的に定義されています。プログラミング言語によっては再帰的に定義された数列等を再帰的なプログラムで表現できる機能を備えています。Pythonにもその機能はあり、例えば次のようにしてフィボナッチ数列や階乗計算を行うプログラムを記述することができます。

In [34]:
def fib_r(x):
    if x == 0:
        return 0
    elif x == 1:    # elifはelse ifのことで、if文の最初の条件を満たさない時に条件がチェックされる
        return 1
    else:
        return fib_r(x - 1) + fib_r(x - 2)    # 再帰呼び出し。

fib_r(0), fib_r(1), fib_r(2), fib_r(3), fib_r(4), fib_r(5)
Out[34]:
(0, 1, 1, 2, 3, 5)
In [35]:
def fact_r(x):
    if x == 0:
        return 1
    else:
        return x*fact_r(x - 1)    # 再帰呼び出し。

fact_r(0), fact_r(1), fact_r(2), fact_r(3), fact_r(4), fact_r(5)
Out[35]:
(1, 1, 2, 6, 24, 120)

このように、fib_rfact_r自身の定義の中でまたそれらを呼び出すことを一般に再帰呼び出し(retursive call)と呼びます。これもプログラミングにおける重要な概念の一つです。前に例題とした再帰呼び出しによらないプログラムと比較してみてください。ただしfibについては、再帰呼び出しのfib_rの方は非常に効率の悪いものとなっています(なぜでしょうか)が、同じ再帰呼び出しでも効率の良いプログラムを書くことも可能であることを申し添えておきます。

再帰呼び出しのプログラムを書くときの考え方

Pythonのような手続き型の言語(ここでは変数への代入文があり、計算状態を変化させる操作の列、即ち手続きとしてプログラムを記述する種類のプログラミング言語を手続き型としています。もちろんPythonで函数的にプログラムを書いてゆくことも可能でしょう)の場合には、再帰呼び出しのプログラムを書く際に、一般には「再帰呼び出しを行う」という操作を行うことと、その時の状態を意識する場合が多いかもしれません。しかしこれまでに出てきたようなフィボナッチ函数や階乗のような、いつ計算しても値は同じだし、計算の状態の変化を引き起こさない場合(つまり値を返す以外の状態の変化である副作用(side effect)がない場合)、即ち函数定義としてプログラムを与えている場合には、再帰的プログラムを書く時に宣言的に考えることが可能です。またこのように考えた方がプログラミングを学びたての場合には理解しやすい人もいますし、すでに手続き的な考え方に慣れている人にとってはある意味新鮮な別の考え方であると思います。再帰呼び出しを行う函数を定義するときの考え方を以下に記述します。

  1. 最も基本的な引数の値の場合には、再帰呼び出しによって値を与えないようにする。
  2. 再帰呼び出しについては、呼び出された函数が正しい値を持つと仮定してよい。
  3. 再帰呼び出しの引数が、元の引数よりある意味簡単なものになっている必要がある。
  4. 再帰呼び出しのある函数は未定義の(手続き的に考えた場合には停止しない)場合がある。

1.はfib_rについてはfib_r(0),fib_r(1)の場合、fact_rについてはfact_r(0)の場合が該当します。2.はfib_r(x - 1)fib_r(x - 2)fact_r(x - 1)が正しい値を返すとしてプログラムを書いています。3.はこれらの場合、値が少ないフィボナッチ数や階乗の値を計算する方が、元のフィボナッチ数や階乗の値を計算するよりも簡単だと考えられます。4.について、2つの函数は負の数に対しては計算が止まりません。4があるため、正確には定義しているのは函数ではなく部分函数(partial function)であるということになります。

考え方の内3が一番曖昧で、どのような意味で簡単な引数で再帰呼び出しをするかは場合によって異なります。但し大抵の場合は直観的にわかる範囲の場合が殆どです。あらゆる場合に適用できるような、この部分の厳密な定式化はある意味不可能であることがわかっています。

但し以上の考え方は、あくまで副作用のない場合であり、次に出てくるタートルグラフィクスの再帰的な描画の手続きの場合、基本的にはやはり手続き的な考え方をすることになります。 手続き的に考える場合、仮引数などの局所変数は呼び出しごとに別物となります。

タートルグラフィクス

画面に図形を表示する演習を行うと興味を引かれる人が多いようであるという理由で、ここではタートルグラフィクスによる演習を行います。タートルグラフィクスとは、タートル(亀)の動きによりさまざまな図形を描くというグラフィクスの一方式です。いわゆるフラクタル図形等の描画を簡単に行うことができます。タートルグラフィクスは、それ自身を扱うのが容易で、しかもタートルグラフィクスの基本プログラムを記述するのも簡単なため、入門的なコースや説明でよく使われるものの一つです。

タートルグラフィクスにはPythonのシステムに最初から付いてくるものを用いることにします。小さいプログラムを書けば簡単に使えます。

タートルをPythonのようなオブジェクト指向言語で実現する場合、タートルをオブジェクトとし、マルチタートルグラフィクスとするのが自然です。後で解説するライブラリではそのように記述しています。

Koch曲線、Hilbert曲線、Sierpinski曲線

タートルグラフィクスによっていわゆるフラクタル曲線を簡単に描くことが可能になります。なお、グラフィクスのプログラムは環境によって挙動が変化する場合があります。いつでも単に実行すればウィンドウがポップアップして表示される環境もあれば、Xウィンドウシステムをあらかじめ起動しておく必要がある場合、Jupyter NotebookをXの仮想端末から起動しておく必要がある場合などがあります。

In [36]:
from turtle import *

def koch(x,y):       # returnがないのは函数ではなく手続きだから。つまり返り値がなく、副作用(この場合は描画)のみが結果となる。
    if x <= y:
        forward(x) # 現在のタートルの向きにx進む。ペンが下りていれば描画される。
    else:
        x /= 3
        koch(x,y)    # 再帰呼び出し
        left(60)   # 左に90度向きを変える
        koch(x,y)
        right(120) # 右に120度向きを変える
        koch(x,y)
        left(60)
        koch(x,y)

goto(-300,0)
seth(0)
clear()
color('black')
speed(0)           # 0 is fastest and 1 is slowest.
koch(600,3)

タートルグラフィクスの画像は別ウィンドウで出ます。 pkoch.png

In [36]:
from turtle import *

def hilbert(x,y,theta):
    if x >= y:
        x /= 2
        right(theta)
        hilbert(x,y,-theta)
        forward(y)
        left(theta)
        hilbert(x,y,theta)
        forward(y)
        hilbert(x,y,theta)
        left(theta)
        forward(y)
        hilbert(x,y,-theta)
        right(theta)

goto(-250,-250)
seth(-90)
right(180)
clear()
color('black')
speed(0)
hilbert(256,8,90)

philbert.png

In [38]:
from turtle import *

def sierpinski(x,y,theta):
    if x < y:
        forward(x)
    else:
        x /= 2
        left(theta)
        sierpinski(x,y,-theta)
        right(theta)
        sierpinski(x,y,theta)
        right(theta)
        sierpinski(x,y,-theta)
        left(theta)

goto(-300,-300)
seth(0)
clear()
color('black')
speed(0)
sierpinski(640,5,60)

psierpinski.png

副作用と再帰呼び出し

この部分は少々普通ではない説明をしていますので最初は飛ばしてください。課題を解くのにも必要ないと考えられます。

上のプログラムでは再帰呼び出しを行なっていますが、函数には返り値がありません。即ち副作用のみのプログラムです。この資料ではこのような函数を手続き(procedure)と呼ぶことにします。この場合の副作用はウィンドウにタートルの跡を描きつつタートルの位置や向き、色を変更することです。このように副作用のみの場合には再帰呼び出しを行うプログラムの内容の理解やそれを書く場合にどのように考えればよいのでしょうか。これには2つの方法があります。

  1. プログラムの動作を手続き的に理解し、記述するという標準的な方法。
  2. プログラムの副作用をタートルへの操作の列であると考え、操作の列全体を結果とする宣言的なプログラムとして捉え直す。

ここでは2番目の理解の仕方を説明してみます。 手続き(函数)が呼び出され、forwardやleftなどの操作をタートルに行うのを、操作の列として求める函数だと捉えます。それらの操作は順序に意味がありますが、結合律を満たします。即ち$a_i$を個別の操作とすると、$(a_1,a_2),a_3$という操作と$a_1,(a_2,a_3)$という操作は操作として同じことです。また何もしない、という操作も考えられ、それがある種の単位元になっています。こうしてタートルへの操作の列を生成する宣言的なプログラムであると考えることができますが、これだけですと抽象的な説明であって具体性が不十分なので操作の列を生成するような宣言的なプログラムに書き換えてみます。但し$\lambda$式や後で説明するような概念のリストを先走って使っているので、この部分は飛ばしていただいても構いません。またこのような書き換えを行なうと実際に描画が行われる時刻が変わってくるので、その意味では元のプログラムと等価ではなくなります。

次のプログラムでは再帰的に定義されたsierpinskiという函数の代わりにsierpinski_mという副作用のない函数を定義しています。この函数はタートルへの操作の列(リスト)を値として返します。但し後で実行できる形式とするために未説明の言語要素である$\lambda$式を使っています。変数への代入が行われていますが、これは一回だけの代入であり、mutableな変数として扱ってはいません。再帰呼び出しの際に同じ変数名の変数に何度も代入が行われますが、これもそれぞれが論理的に別の変数であり、宣言的なプログラムであることに変わりはありません。上のプログラムと見比べてみてください。

但しプログラムの最後の部分では、リスト中の操作を全て行なって実際の描画を行なっています。この部分だけが手続き的なプログラムとなっています。

In [36]:
from turtle import *

def sierpinski_m(x,y,theta,a0):
    if x < y:
        return a0 + [lambda:forward(x)]
    else:
        x /= 2
        a1 = a0 + [lambda:left(theta)]
        a2 = sierpinski_m(x,y,-theta, a1)
        a3 = a2 + [lambda:right(theta)]
        a4 = sierpinski_m(x,y,theta,a3)
        a5 = a4 + [lambda:right(theta)]
        a6 = sierpinski_m(x,y,-theta,a5)
        return a6 + [lambda:left(theta)]

a1 = [lambda:goto(-300,-300)]
a2 = a1 + [lambda:seth(0)]
a3 = a2 + [lambda:clear()]
a4 = a3 + [lambda:color('black')]
a5 = a4 + [lambda:speed(0)]
a6 = sierpinski_m(640,5,60,a5)

for i in a6:
    i()

psierpinski.png

オブジェクトとは

ここで改めてオブジェクト指向プログラミングについて説明しておきます。オブジェクトという概念はできるだけ何にでも当てはめられるようなものですので、必然的に抽象的な概念であり、説明も抽象的なものとなります。したがってこの節の内容を実感を伴って理解できない場合、ざっと読飛ばすだけで十分であり、あとでプログラムの具体例を理解してから再読されるとよいと思います。

オブジェクトとは自分自身の局所的なデータを備え、計算を行うものです。また、他のオブジェクトとデータをやり取りします。やり取りするデータのことをメッセージと呼びます。

メッセージの解釈は各オブジェクトが自分自身で行います。各オブジェクトの局所的なデータは特に指定しない限り他のオブジェクトから見たり書き換えたりすることができません。局所的なデータを見たり書き換えたりできる場合があるといっても、それらもメッセージにより行います。基本的には見るためあるいは書き換えるためのメッセージへの反応の仕方を記述したプログラムは、メッセージを送る側ではなく、メッセージを受けるオブジェクト側が持っています。従って外側から見てあるオブジェクトの局所的なデータを操作しているように見えたとしても、外側からはそれらしく見えているだけであって実際にそうなのかどうかは当該オブジェクト自身にしかわかりません。

このように、データとそれへの操作方法をまとめて定義し、それらを外から直接見られなくすることをデータ隠蔽とかカプセル化と呼びます。このようにするとデータとそれへの操作のプログラムをまとめて取り替えることが可能になり、ソフトウェアの部品化が容易になるし、プログラマは部品の余計な内部構造まで考えなくてすみます。この概念はプログラミング言語の発展における重要な概念の一つです。

同種のオブジェクトをまとめたものをクラスと呼ぶ場合があります。すなわち、クラスが同じオブジェクトは、保持している局所的データの種類と個数が同じで、メッセージへの反応の仕方を記述するプログラムも同じです。メッセージにはメソッドと呼ばれる処理プログラムを表す一種のタグ(荷札)がついており、メソッドごとに処理プログラムを記述します。Pythonを含めてクラス概念があるプログラミング言語では、局所変数やメッセージへの反応の仕方を記述するプログラムを与えてクラスを定義します。

クラスを新たに定義する場合、既存クラスの記述を利用することが可能です。この機能をインヘリタンス(継承)と呼びます。

マルチタートルの例: 渦

使用しているタートルグラフィクスのライブラリではタートルを複数同時に使うことが可能です。タートルはそれぞれオブジェクトとして実現されています。以下の例では2つのタートルを使って色が異なる2つの図形を同時に描きます。タートルが一つしかなければもう一つ生成し、タートルを表す2つのオブジェクトをleft、rightという変数に代入して操作を行います。例えばleft.forward(i)とすればleftに入っているオブジェクトにforward(i)(但しiはその時点でのiの値に置き換えられる)というメッセージが送られます。leftは、それが属するTurtleというクラスのforwardメソッドの定義に従って解釈します。この場合そのメソッドの定義はタートルグラフィクスのライブラリの中で与えられているのでユーザが記述する必要はありません。

In [ ]:
from turtle import *

if len(turtles()) == 1: # タートルが1つの場合
    right = Turtle()    # もうひとつタートルを生成する
left = getturtle()      # 1つ目のタートル
right = turtles()[1]    # 2つ目のタートル(現在存在するタートルの2つ目([0]とすると1つ目)をrightに代入)
left.speed(0)           # 1つ目のタートルのspeedを0(最速)に設定
right.speed(0)
left.penup()
right.penup()
left.goto(-200,0)
right.goto(200,0)
left.pendown()
right.pendown()
left.clear()
right.clear()
left.color('red')
right.color('blue')
for i in range(0,500):
    left.forward(i)
    left.right(121)
    right.forward(i)
    right.right(119)

puzu.png

クリアがタートルごとに行われるため、複数カーソルが存在していて以前の単一タートルのプログラムを再び動かす場合には以下のプログラムで画面クリアする必要があります。但しタートルの数は減りません。タートルの数を元の1に戻すには、jupyterのKernelメニューからRestartを選ぶのが一つの方法です。これはPython処理系をリセットするものです。

In [45]:
for t in turtles():    # 現在存在する全てのタートルについてクリアを行う
    t.clear()

以下は深さ優先で木を描くプログラムです。

In [44]:
from turtle import *

def dtree(depth, theta, length):
    forward(length)      # 現在の方向に進める
    if depth > 1:        # 葉ではない場合
        left(theta)      # 左に回転
        dtree(depth - 1, theta, length/1.2)    # 長さを一定の比率で縮める
        right(2*theta)   # 右に回転
        dtree(depth - 1, theta, length/1.2)
        left(theta)
    backward(length)     # 逆方向に戻す

goto(0,-250)
clear()
seth(90)
dtree(8,15,120)

pdtree.png

以下は木の分岐ごとにタートルをコピーして増やし、並列に木を描くプログラムです。タートルが多数生成されるため、実行後にkernel restartした方がよいでしょう。

In [46]:
from turtle import *

def brtree(ts, depth, theta, length):
    ts2 = ts[:]                 # 現在のタートルのリストをコピー
    for t1 in ts:
        t1.forward(length)      # 現在の方向に進める
        if depth > 1:           # 葉ではない場合
            t2 = t1.clone()     # タートルをコピーする向きや色は同じ
            t1.left(theta)      # 左に回転
            t2.right(theta)     # 右に回転
            ts2.append(t2)      # コピーしたタートルをリストの末尾に加える
    if depth > 1:               # 葉ではない場合
        brtree(ts2, depth - 1, theta, length/1.2)    # 長さを一定の比率で縮める

goto(0,-250)
for t in turtles():
    t.clear()
seth(90)
brtree([getturtle()],8,15,120)

pbrtree.png

2つ目の課題

タートルグラフィクスのライブラリを冒頭でimportして図形を描くプログラムを作成し、プレーンテキストのファイルにコピー&ペーストして課題提出用ID-kadai08.pyとして提出せよ。図形は自分が選んだ、あるいは創作したものとしてよい。ただし描画のプログラムは自分で記述すること。また、kernel restart後に提出プログラム部分のみを実行し表示を確認すること。表示したウィンドウをキャプチャして画像ファイルとしたものを課題提出用ID-kadai08.pngなどとして提出すること。Windowsの場合、対象ウィンドウをクリックして選択し、[Alt]+[Print screen]キーを押し、ビットマップ画像描画プログラム(MSペイント等)を起動してペーストして保存すれば画像ファイルを作成できる。提出時にすべきことや提出方法は前回と同じである。

これまでに学習したプログラミングの技法以外の技法も用いてもよい。タートルグラフィクス、Pythonチュートリアル標準ライブラリのリンクを見たり、サーチエンジンでPython言語要素などを調べれば多数の解説サイトが見つかるので、それらを参照すればさまざまなプログラミングの技法やPythonの機能を知ることが可能である。

In [ ]:
# kadai08 s1234   課題提出用IDを書き換え
#
from turtle import *

# def 描画手続き 

home()
clear()
seth(90)
# 描画手続き呼び出し

リスト

様々なプログラミング言語に出てくる基本的な概念の一つであるリストについて説明します。Pythonの場合にはリストが配列と同じように扱えます。

リストとは、何かが入る箱が繋がったもので、繋がりを途中で切ったり繋いだりできるものです。繋がっている箱の数をリストの長さと言います。配列とは、何かが入る箱がいくつか並び、箱の位置を数字で指定して中のデータを読み書きできるものです。箱の位置を指定する数字のことを添字(index)と言い、並んだ箱の数のことを配列の大きさと言います。配列とリストは似ていますが、異なるデータ型です。Pythonの場合にはリストにも添え字を使えます。また、リストや箱の中に入る物は同じ種類のデータしか許していない言語もありますし、Pythonのように箱ごとに異なる種類のデータを入れられる言語もあります。例えばリストの中にリストを入れることも可能です。

Pythonでは、リストもオブジェクトの一つです。リストからデータを読み出したり書き込んだりするのもメッセージによります。

In [41]:
a = [1,3,5,7,9] # 最初の要素が1、2番目の要素が3...という大きさ5のリスト
a
Out[41]:
[1, 3, 5, 7, 9]
In [42]:
len(a), type(a) # リストの大きさとクラス
Out[42]:
(5, list)

[1,3,5,7,9]というのは最初の要素が1、2番目以下の要素が3,5...という大きさ5の配列で、上の文によりaという変数にリストが入ります。printによりこのままの形で表示されます。

添字が0から始まるか1から始まるかの違いを除き、他のプログラミング言語の場合とほぼ同様に以下のようになります。

In [43]:
a[0],a[3]
Out[43]:
(1, 7)
In [44]:
a[0] = 10
a
Out[44]:
[10, 3, 5, 7, 9]

Pythonのリストはスタックやキューも兼ねている

In [45]:
a = [1, 2, 3, 4, 5]
a
Out[45]:
[1, 2, 3, 4, 5]
In [46]:
a.append(6)    # リストの大きさを1増やし、最後にデータを入れる
a
Out[46]:
[1, 2, 3, 4, 5, 6]
In [47]:
a.pop(0)    # リストの最初のデータを取り除く。入れたデータを順に取り出す仕組みをキューと呼びます
Out[47]:
1
In [48]:
a
Out[48]:
[2, 3, 4, 5, 6]

appendpop(0)というメッセージでリストをキューとして使えます。今の場合には、最初1,2,3,4,5というデータがこの順にキューに入っていたところ、最後に6というデータを追加し、その後キューから最初のデータである1を取り出しました。

In [49]:
a.pop() # リストの最後のデータを取り除く
Out[49]:
6
In [50]:
a
Out[50]:
[2, 3, 4, 5]

入れたデータを逆順に取り出す仕組みをスタックと呼びますappendpop()というメッセージで配列をスタックとして使えます。

リストのいろいろな操作

In [51]:
a = [1, 2, 3, 4]
b = [5, 6, 7, 8]
a + b # 2つの配列をくっつける
Out[51]:
[1, 2, 3, 4, 5, 6, 7, 8]
In [52]:
a.remove(3)    # 要素3をリストから取り除く
a
Out[52]:
[1, 2, 4]

例題: エラトステネスの篩

ここではリストを使った例題として、エラトステネスの篩(ふるい)と呼ばれる手法により、ある数までの素数をすべて求めてみます。以下がそのプログラムです。Noneは通常のデータがないことを表すデータです。

In [53]:
n = 300
a = [None,None] + list(range(2,n))    # 範囲をリストに変換してその前にNoneを2つ入れる
for i in a:
    if i:                             # None,0以外の場合
        for j in range(2*i, n, i):    # i以外のiの倍数を消す
            a[j] = None
a = [x for x in a if x]               # aの各要素に対し、それがNoneや0以外の時のみ結果のリストに入れる
for i in a:
    print(i, end=',')
2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,

このプログラムでは、最初に2つのNoneがあり、その後2以上与えられた数までの数値を要素として持つ配列を2行目で作っています。この時点では、0と1は素数でなく、素数だとわかっているのは2だけで、3以降は素数である可能性がある数値が配列中に残っている状態です。ある添字の要素がNoneでなければその添字の値自身がその添字の要素として入っています。

4-6行目で、配列の各要素について、それがNone(または0)でなければその数の2倍の添字位置からリストの最後までの、その数の倍数の添字位置の要素をNoneに置き換えるという操作を繰り返し行います。ここではループが2重になっていることに注意してください。また、if文でNoneFalseと同様に偽として扱われることにも注意してください。

下から3行目はリスト内包表記といわれるもので、合成数の位置に入っているNoneを消しています。

3つ目の課題

3つ目の課題はオプションです。即ち、提出したい人だけが課題提出用ID-kadai09.pyとして提出してください。提出方法等は前回までと同じです。

先の節である数nまでの素数をすべて求める方法を見てみました。今度はまず、ある数までの素数の表を求め、それを使って数mを素因数分解する函数factorを書いてください。mは、函数の引数として与えるようにしてください。また、出力の形式は以下のようなものとしてください。

[[素因数1, 個数1], [素因数2, 個数2], … ,[素因数k, 個数k]]

素因数iの個数i乗をi=1,...,kについてすべて掛け合わせた結果がmとなります。また、個数iは1以上の自然数です。ただし、あらかじめ求めた素数の表が、mの分解に足りない場合には、その旨を出力するようにしてください。その場合、途中まで行える分解の結果も表示してください。

例えば[2,3,5,7]という素数の表を求めていた場合、53の素因数分解には7*7 < 53なので表が足りません。63の分解であれば3で2回割り切れて7となりますので分解できます。3339=3*3*7*53の分解は、3で2回割って7で割ると53となり、7*7<53なので表が足りませんが、[[3,2],[7,1],[53,1]]というところまでは分解できます。ただしこの場合には最後の53が素数かどうかはわからないということになります(今の場合にはたまたま素数です)。表が足りない場合には、素数かどうかわからない部分も指示するようにしてください。例えば「[[3,2],[7,1],[53,1]](ただし最後の因数は素数とは限りません)」などと出力すれば十分です。

これまでに学習したプログラミングの技法以外の技法を用いても構いません。

In [ ]:
# e81234 kadai09
#

# def factor(m)