第6回 - 関数の定義、シーザー暗号の解析、アニメーションコンテスト投票

  1. 前回の復習
  2. データの「型」について (再掲)
  3. Python における関数 (function) とは?
  4. 自前の関数を定義する
  5. アニメーション・コンテスト投票
  6. シーザー暗号とは何か?
  7. 本日の課題
今回の授業中に アニメーション・コンテストの投票 をおこないます。

雑談

Python は映画やアニメの製作でもよく使われている: Python For Feature Film

0. 前回の復習

演習 6-1. リストの要素の追加・削除
  1. 以下のプログラムで、リスト a の先頭の要素 (a[0]) を削除して a = [9,4,7,0,0] となるようにしたい。 ??? の中を埋めよ。
    a = [5,9,4,7,0]
    i = 0
    while i < 4:
       a[???] = a[???]
       i = i + 1
    
  2. 今度は逆に、先頭の要素をひとつ空けて a = [5,5,9,4,7] となるようにしたい。が… 以下のプログラムを実行したとき a の中身がどうなるかを予測せよ。
    a = [5,9,4,7,0]
    i = 0
    while i < 4:
       a[i+1] = a[i]
       i = i + 1
    
  3. 前回の教訓を生かし、今度こそ a = [5,5,9,4,7] となるようにしたい。 ??? の中を埋めよ。
    a = [5,9,4,7,0]
    i = ???
    while ???:
       a[i+1] = a[i]
       i = ???
    

実際には、Python にはリストの途中の要素を削除したり 途中から挿入したりするためのもっと簡単な方法がある。 しかし、実際に内部で起きているのはこういうことである。

類似の問題: 0〜4までの番号がついた5個のイスが並んでいる。 0番目のイスをはずして残りを詰めるにはどうすればよいか?

1. データの「型」について (再掲)

Python の変数に格納されるデータは、 実はいくつかの「 (type)」に分類されている:

+ 演算子の意味は、計算対象のデータ型によって異なる。

print(123+456)      # 整数 + 整数: 579
print("123"+"456")  # 文字列 + 文字列: "123456"
print(123+"456")    # 整数 + 文字列: エラー!

型が異なるデータの計算は意味をなさないので、ふつうはエラーが発生する。

1.1. 文字(列)型 → 整数型への変換

構文:
例1:
a = 123         # a は整数の 123
b = int("123")  # b は整数の 123
print(a+b)      # 整数+整数: 246
例2:
print(ord("A"))   # "A"の文字コード: 65
print(ord("あ"))  # 「あ」の文字コード: 12354
例3:
s = input("string:")
for i in range(len(s)):  # i は 0〜(文字数-1) まで変化する。
    print(ord(s[i]))     # i番目の文字コードを表示。

input関数の動き

本来、Pythonのinput関数は入力された 文字列 (string) を返す。

s = input("string:")  # s は文字列型
print(s)

ここに int() 関数を適用することで、 文字列を整数型として利用できる。

s = input("number:")
x = int(s)  # x は文字列型
print(x)

1.2. 整数型 → 文字(列)型への変換

構文:
例1:
a = 123        # a は整数の 123
b = str(a)     # b は文字列の "123"
print(b+"0")   # 文字列+文字列: 1230
例2:
a = 9829
b = chr(a)         # b は Unicode 9829番 で表される文字
print("I"+b+"NY")  # I♥NY

chr()関数に与える文字コードは Unicode で定義されている。

例3:
for i in range(26):   # 26回繰り返す。
    print(chr(65+i))  # (65+i) で表されるUnicodeの文字を表示する。

このプログラムを実行すると、A から Z までの文字が表示される。

まとめると、以下のような関係がなりたつ:

文字 → 数値数値 → 文字
文字列int(x)str(x)
文字chr(x)ord(x)

1.3. print関数における f"〜""〜" の違い

じつは、print関数の機能は

構文:
print(文字列)  # 文字列を表示する。
だけであった。
print("hello")
print(f"x={x}")
などの書き方は、以下のようにもできる:
s = "hello"    # s は文字列型
print(s)       # s の内容を表示
t = f"x={x}"   # t は文字列型
print(t)       # t の内容を表示

さらに、 f"x={x}" は、 文字列の連結と str(x) 関数を 組み合わせたものと同じである:

t = "x=" + str(x)

2. Python における関数 (function) とは?

print(...)input(...) のような部分。 引数 (ひきすう, arguments) をとり、値を返す。

2.1. これまでに習ったPython関数

注意: if文, while文, for 文などは 「文」であって関数ではない。

2.2. いろいろな関数

abs: 絶対値を求める。
簡単な処理だが、よく使われるので Python に組み込まれている。
>>> abs(2)
2
>>> abs(-11)
11
pow: べき乗を求める。
関数によっては、2つの値をとることがある。
>>> pow(3, 4)
81
>>> pow(2, 0.5)
1.4142135623730951
>>> 3 ** 4
81  # **演算子を使っても同じ
sum: リストの和を求める。
リストを与えるような関数も存在する。
>>> a = [5,9,4,0]
>>> sum(a)
18
注意事項
各関数によって、とれる値の数、およびそのデータ型が決まっている。
>>> chr("a")
Traceback (most recent call last):
  File "", line 1, in 
TypeError: an integer is required (got type str)
エラー: chr関数には、数値型を与えなければならない。
>>> sum(4) Traceback (most recent call last): File "", line 1, in TypeError: 'int' object is not iterable
エラー: sum関数には、リスト型を与えなければならない。
>>> input("x:", "y:") Traceback (most recent call last): File "", line 1, in TypeError: input expected at most 1 arguments, got 2
エラー: input関数は、値を1つだけしか与えてはいけない。

プログラムを書かずにPythonを使う

ターミナルで、プログラム (.pyファイル) を書かずに、ただ

$ python
とだけ入力すると、Pythonが「対話モード」で起動する。 この場合、Pythonのプログラムを1行ずつ直接入力して実行することができる。 これは、ちょっとした動きを確かめたいときなどに便利だが、 複雑なプログラムは書けない。
$ python
Python 3.7.4 (default, Oct  4 2019, 06:57:26)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x=2
>>> y=3
>>> x+y
>>> print(x,y)
2 3

3. 自前の関数を定義する

Python では、自前の関数を新しく定義することができる。 新しく定義された関数は、従来からある関数とまったく同じように使える:

# 関数 plus(a, b) … aとbの和を求める。
def plus(a, b):
    return a + b

x = 2
y = 3
z = plus(x, y)  # zの値は 5。

プログラムが複雑になってくると、プログラムを全部一度に書くのではなく、 部分ごとに分けて書く、ということが必要になってくる。 関数はもともと数学の概念だが、Python における関数とは、 プログラムの処理の一部をおこなう部品である。 ここでは入力と出力をもった「ブラックボックス」に近い。

関数 入力A 入力B 入力C 出力

3.1. 関数を定義するには

def 関数名(変数名1, 変数名2, ...):
    関数の中身
    ...
    return 関数が返す値

3.2. 定義した関数を利用するには

変数名 = 関数名(値1, 値2, ...)

図示すると、以下のようになる:

plus return 2 3 x y 5 z a b
def plus(a, b):
    return a + b

x = 2
y = 3
z = plus(x, y)
演習 6-2. 自前の関数の呼び出し
  1. 上のプログラムで関数 plus(x, y) を呼び出したとき、 関数の中で変数 a, b に入る値は何か?
  2. 以下のように呼び出したとき、 関数の中で変数 a, b に入る値は何か?
    p = 1
    z = plus(3, p)
    

3.3. いろいろな関数の例

2つの値の平均を求める関数

def avg(a, b):
    c = (a+b)/2
    return c

print(avg(3, 5))  # 4

関数の中で別の変数を使うこともできる。

関数の中で関数

print(1 + avg(3, 5))      # 5
print(avg(avg(3, 5), 8))  # 6

関数の返り値を計算したり、それをさらに関数を渡すこともできる。

文字列を返す関数

def sign(x):
    if x < 0:
        return "negative"
    elif 0 < x:
        return "positive"
    else:
        return "zero"

print(sign(-3))  # "negative"と表示される。
print(sign(4))   # "positive"と表示される。
関数の中で if文や while文を使うこともできる。 関数が返すデータの型は、数値以外のものでもOK。 また、関数の中に return文を複数書いてもよい。

値を返さない関数

def greet(name):
    print(f"Good morning, {name}")
    return

greet("euske")  # "Good morning, euske"と表示される。

(数学ではありえない) 値を返さない関数というものも存在する。 この場合、関数の中で print文を実行させることで 画面にものを表示させることのみが目的となる。

引数をとらない関数

def constant():
    return 1279

print(constant())  # 1279
print(constant())  # 1279
print(constant())  # 1279

引数をとらない関数は、何回実行しても同じ結果となる (当然)。

リストを返す関数

# 与えられた画面にフィットする画像の大きさを計算する。
def fitsize(x, y, w, h):
    if w*y < x*h:
        return [w, y*w/x]  # 横フィットの場合
    else:
        return [x*h/y, h]  # 縦フィットの場合

print(fitsize(20,10,100,100))  # [100, 50]
print(fitsize(20,10,400,100))  # [200, 100]

リストを返すこともできる。

関数の中でさらに関数

# aとbを足す
def plus(a, b):
    return a + b

# xに2を足す
def add2(x):
    # 定義した関数を使っている。
    return plus(x, 2)

print(add2(3))  # 5

一度定義した関数は他の関数と同じように使えるので、 さらに別の関数からも使える。

複数のreturn文

def navi():
    print("Hey!")
    return
    print("Listen!")  # 表示されない。
    return

navi()  # "Hey!" のみが表示される。

関数の実行は、最初の return文を実行した時点で終了する。

3.4. 関数内における変数の扱い

注意: 関数の内と外で同じ名前の変数を使っている場合、 関数内の変数は、外の変数とは別物として扱われる:

def plus(x, y):
    print(f"x={x}")  # ???
    print(f"y={y}")  # ???
    z = x + y
    print(f"z={z}")  # ???
    return z

x = 100
y = 200
z = 300
a = 2
b = 3
print(plus(a,b))  # 5
print(f"x={x}")    # ???
print(f"y={y}")    # ???
print(f"z={z}")    # ???

これは、関数がプログラムの他の部分とは完全に独立した プログラムと考えられているためである。 関数の外でどんな名前の変数を使っていようが、 関数の実行結果が影響を受けるべきではないからである。 関数内で使われる変数には、引数の値が一時的にコピーされるだけと 考えるのがよい。

演習 6-3. 関数の練習いろいろ (本日の小課題)
  1. 上の例を実行して、関数の内と外で x, y, z の値が それぞれどう表示されるかを確認せよ。
  2. 以下のリストの平均値を求める関数 averageを完成させよ:
    def average(a):
        x = 0
        for i in range(len(a)):
            x = x + ???
        return ???
    
    print(average([1,2,3]))        # 2
    print(average([10,15,17,20]))  # 15.5
    
  3. 与えられた個数のアスタリスクを表示する関数 bar を書け。
    bar(3)  # "***" を表示する
    bar(5)  # "*****" を表示する
    
  4. 3. で定義した関数を bar を使って、 与えられた大きさの三角形を表示する関数 triangle を書け。
    # 以下のように表示する:
    # *
    # **
    # ***
    triangle(3)
    
    # 以下のように表示する:
    # *
    # **
    # ***
    # ****
    # *****
    triangle(5)
    

4. アニメーション・コンテスト投票

演習の合間に、11月1日に提出したアニメーション・コンテストの 投票をおこなう。

アニメーション・コンテスト投票

以下のページから、1a クラスの参加作品一覧が見られる。 いくつかのアニメーションは1度目の再生ではブラウザの反応が遅くなり、 うまく動かないことがある。おそらくこれは表示が速すぎてブラウザが 追いつかないためだと思われる (2度目の再生ではうまく動くことが多い)。 time.sleep(0.01) は毎秒100コマという意味であり、 ほとんどのPCはこんな速度で描画を行うことはできない。 教訓: 自分のプログラムが自分のパソコンで動いたからといって、 世界中のパソコンで同じ条件で動くとは限らない。

投票は、以下のページからおこなう:

投票の結果、本授業の最後に優秀者を決定する。

5. シーザー暗号とは何か?

「シーザー暗号」とは、ジュリアス・シーザー (Julius Caesar, ユリウス・カエサル) が 使ったと言われる暗号で、アルファベットの順番をずらして文を作成する方法である。 暗号を元にもどす (復号) ためには、順番を逆方向にずらせばよい。 このとき、ずらす回数 k をその暗号の (key) とよぶ。

シーザー暗号の例:

このような暗号は、Python のプログラムで簡単に作ることができる。 入力された文字が英文のアルファベットであると仮定して、 各文字コードに与えられた数 k を足せばよい:

caesar.py
# 与えられた文字列 text を k回先にずらす。
def caesar(text, k):
    # 結果を入れる文字列変数を準備する。
    result = ""
    for i in range(len(text)):
        c1 = text[i]  # i番目の文字を取得する。
        n1 = ord(c1)  # その文字コード。
        n2 = n1 + k   # k 回ずらした文字コード。
        # ずらした文字を結果に追加する。
        result = result + chr(n2)
    # できた文字列を返す。
    return result

print(caesar("HELLO", 1))   # "IFMMP" と表示する。
print(caesar("IFMMP", -1))  # "HELLO" と表示する。
演習 6-4. 関数caeserの改良
  1. 上のプログラム caesar.py にはバグがある。 入力した文字列が HELLO で k=1 の場合はよいのだが…
    print(caesar("HEY!", 2))
    # "JG[#" と表示される。
    
    問題は、文字コードをずらしたときに A〜Z の範囲を超えてしまうことである。 このような場合でもうまく動くように、以下の改良を加えよ:
    1. 英文アルファベット (A〜Z) 以外の文字は変更せず、そのまま表示する。
    2. 「"Z" の 1つ先の文字」が "A" になるようにする。(2つ先の文字は "B")
      ヒント: % を使うべし。

5.1. 暗号解析とは?

上のような簡単な英文のシーザー暗号は、すぐに解読することができる。 エドガー・アラン・ポーが 19世紀に発表した推理小説 「黄金虫」では、 このような暗号が使われていた (こちらは任意の文字が別の文字と対応するので、より難しい)。 暗号解析 (cryptoanalysis) とは、暗号の鍵がわからない状態で、 暗号文だけを頼りにもとの文を推測 (解読) することである。 暗号解析技術は、現在でも各国の軍事機関でさかんに研究されている。 cf. 暗号技術博物館

英文でもっともよく現れる文字は "e" であることから、 ある英文がシーザー暗号で書かれていると仮定すると、 暗号解析は以下のように行える:

  1. 暗号の中でもっとも多く現れている文字をさがす。
  2. その文字が E になるように、シーザー暗号の鍵 k を決定する。
  3. その値を使って文字を k回文だけ逆にずらし、元の文を復元する。

「暗号の中でもっとも多く現れている文字をさがす」処理は、 前々回にやった投票システムの課題と似ている。 つまり、暗号中の各文字をその文字に対する票と考え、 もっとも票の多かった文字を見つければよい。

暗号解析をおこなう関数 analyze.py (未完成版):
def analyze(text):
    # 26文字分の投票結果を用意する。
    votes = [0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0]
    # 1文字ずつ調べる。
    for i in range(len(text)):
        c = ord(text[i])  # i番目の文字の文字コードをとりだす。
        # それが英大文字の範囲であれば、票を1増やす。
??? ...
# votes の中で、最も多く得票したものを見つける。 maxi = 0 for i in range(len(votes)):
??? ...
# この時点で、「maxi番目の文字」がもっとも多く現れることが判明。 # これが "E" を表すように、k の値を計算する。 k = ??? return k
小課題 7. 関数barおよび関数triangleの完成 (11月22日締切)

以下のようなプログラムとして T2SCHOLA から提出すること。

triangle.py
# bar(n): n個のアスタリスクを表示する。
def bar(n):
    ...

# triangle(h): 高さhの三角形を表示する。
def triangle(h):
    ...

# inputで与えられた高さxの三角形を表示する。
x = int(input("height:"))
triangle(x)

6. 本日の課題

中課題 3. シーザー暗号の暗号解析をおこなうプログラム (11月29日締切)
analyze.py (未完成版)
# 関数 caesar の定義
def caesar(text, k):
    ...

# 関数 analyze の定義
def analyze(text):
    ...

# 解読対象の暗号文。
cipher = (
 "JXU ISYUDJYIJ XQI Q BEJ EV UNFUHYUDSU MYJX YWDEHQDS"
 "U QDT TEKRJ QDT KDSUHJQYDJO, QDT JXYI UNFUHYUDSU YI"
 " EV LUHO WHUQJ YCFEHJQDSU. MXUD Q ISYUDJYIJ TEUID’J"
 " ADEM JXU QDIMUH JE Q FHERBUC, XU YI YWDEHQDJ. MXUD"
 " XU XQI Q XKDSX QI JE MXQJ JXU HUIKBJ YI, XU YI KDS"
 "UHJQYD. QDT MXUD XU YI FHUJJO TQHD IKHU EV MXQJ JXU"
 " HUIKBJ YI WEYDW JE RU, XU YI IJYBB YD IECU TEKRJ. "
 "MU XQLU VEKDT YJ EV FQHQCEKDJ YCFEHJQDSU JXQJ YD EH"
 "TUH JE FHEWHUII MU CKIJ HUSEWDYPU EKH YWDEHQDSU QDT"
 " BUQLU HEEC VEH TEKRJ. ISYUDJYVYS ADEMBUTWU YI Q RE"
 "TO EV IJQJUCUDJI EV LQHOYDW TUWHUUI EV SUHJQYDJO — "
 "IECU CEIJ KDIKHU, IECU DUQHBO IKHU, RKJ DEDU QRIEBK"
 "JUBO SUHJQYD.")

# 暗号解析をおこない、鍵の値 k を決定する。
k = analyze(cipher)
# その値を使って暗号文を元にもどす。
text = caesar(cipher, -k)
# 復号した文字列を表示する。
print(text)

Yusuke Shinyama