Open In Colab

3. 関数

[この章の目的]プログラム内での関数と戻り値,スコープの概念を獲得する。

ここでは、Pythonにおける関数の定義と関数が返す値[戻り値(返り値とも呼ぶ)]、ややテクニカルですが重要な点である変数のスコープについて説明する。

3.1. 関数の定義

既に現れたprintlenなどはPythonに備え付けられた”組み込み関数”と呼ばれるものの一種。 以下に示すように、組み込み関数とは別にユーザーが独自の関数を定義することもできる。

たとえば点の\(x,y,z\)座標に対応するような3成分のリストがたくさん(たとえば100個)あったとき

p1 = [2.0, 4.0, -5.0]
p2 = [1.0, 3.0, -4.0]
#...中略
p100 = [5.5,-2.0, 3.0]

それらの任意の2つの点の距離を求める操作が必要だったとする。 そんなとき\({}_{100}C_2=4950\)通りに対して毎回

d_1_2 = ( (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2 ) ** 0.5
d_1_100 = ( (p1[0] - p100[0])**2 + (p1[1] - p100[1])**2 + (p1[2] - p100[2])**2 ) ** 0.5

などと書くのは面倒だし、コードが4950行以上になってしまう。

このように、同様の操作が何回も必要になるときに役に立つのが、関数である。

関数とは、処理を抽象化して、(必要なら)引数(ひきすう)を受け取り、(必要なら)戻り値/返り値を返すようなものといえる。

例として「任意の長さが3の数値リストに対して3次元空間での距離を計算する関数」を自作してみよう。

def calc_d(l1,l2):           
    return ( (l1[0] - l2[0])**2 + (l1[1] - l2[1])**2 + (l1[2] - l2[2])**2 ) ** 0.5

t = calc_d(p1,p2) 
print("点1",p1, "と点2", p2, "の距離は", t, "です")

関数の構成要素は以下の通りである。

def 関数名(引数1,引数2,...):
    処理1
    処理2
    ...
    return 戻り値

上で定義した関数では、リスト(l1)とリスト(l2)を突っ込んだときに距離を計算してreturnする(返す)という一連の操作を関数として定義def(defineの略)し、 実際にリストp1,p2に対して関数を適用している。

関数名のあとに付けたコロン:は「以下で関数の中身を記述するブロックが開始する」ことを意味していて、インデントによってどこまでが関数のブロックかがわかるようになっている。(ブロックについてはifforを思い出してください)

定義した関数を使用する際には、この関数calc_d()に必要な引数(変数,今の場合リスト)l1,l2を代入して使う。 関数に入れる引数に用いる変数名は、実際に関数を用いる際の引数として使う変数名とは関係がなく、 関数に入れるもの(関数の外側で扱う変数)をl1,l2という名前にあわせて定義しておく必要はない。これもある種の抽象化である。

print(calc_d(p1,p100)) #←これでも使えるし
print(calc_d([20.0, 1.0, -5.0], [-2.0, 3.0, 5.5])) #←などとして変数でなく値を直接書いても使える

上の例のように100個の点の3次元座標に対応するリストがある場合,

import random 
#3次元の座標点をランダムに100個作っている n,iはダミー変数
lists = [ [ random.gauss(0,1) for n in range(3)] for i in range(100)] 

hit = 0
for j in range(100):
    for i in range(j+1,100): # i>j
        distance = calc_d( lists[j], lists[i])
        #print(j,i, distance) # 4950回文の計算結果をprintすると邪魔なのでコメントアウトした
        hit += 1 
print(hit) #回数だけ表示しよう
#上のjのループ内で、iはj+1から99までを回る。 j+1= 100つまり j=99のとき range(j+1,100)はちゃんと空になる
#つまり、長さ100のリストにindex=100でアクセス(範囲外参照)したりすることはない。

などとすれば、全組み合わせ(\({}_{100}C_2\))に対して距離を計算することが出来る。

引数は通常関数の中で行う操作に必要な変数を指定する。上の例では2つのリストを引数とした。

関数内の操作に関数外からの情報(インプット)が必要ない場合は引数なしの関数でも構わないし、 関数の外に値を渡す(アウトプットする)必要がなければreturn文を明示的に書かなくても問題ない。 return文がない場合は自動でNone(値なし)が返される関数となる。

幾つか例を作って、挙動を理解してみよう。

#引数なしで、ただ以下の文字列を表示する関数を定義し、
def name(): 
    print("私は田中です")

#その関数を使用してみる
name() #←これで関数が実行される
print("print関数内で実行してみます→", name()) #←print関数の中に関数を入れると、関数の戻り値が表示される。戻り値がない場合はNoneが表示される
#引数namaeを使って、以下の文字列を表示する関数とその実行
def myname(namae): 
    print("私は"+str(namae)+"です")

print("myname()の返り値→", myname("吉田"))
# myname()同様の文字列をprintする代わりに返り値として返す関数とその実行
def myname_return(namae): 
    return "私は"+str(namae)+"です"

print("myname_return()の返り値→", myname_return("吉田"))

戻り値returnは単一の値や文字列に限らず、複数の値でも可能で、リストを返すことも出来る。

先程の自作関数calc_dの場合、戻り値はfloat(実数値)だが

def calc_d_print(l1,l2):
    return "距離は"+str( ( (l1[0] - l2[0])**2 + (l1[1] - l2[1])**2 + (l1[2] - l2[2])**2 ) ** 0.5  )+"です"

def zahyo_and_d(l1,l2):
    d = calc_d(l1,l2) #関数の中で、別の自作関数を呼び出すことができる
    return [l1,l2],d  #座標を結合したリストと距離を返す

p1 = [2.0, 4.0, -5.0]
p2 = [1.0, 3.0, -4.0]

ret = calc_d_print(p1,p2)
print("関数calc_d_print→", ret,type(ret))

ret = zahyo_and_d(p1,p2)
print("関数zahyo→ ", ret,type(ret))
print("座標の結合リスト",ret[0],"距離",ret[1])

といったように、様々な返り値を持つ関数を定義できる。

上の例はあくまで「任意の長さが3の数値リストに対して3次元空間での距離を計算する関数」であり、3成分以上のリストに対しては対応していない。

実際に関数を定義する際には、より操作を抽象化することで、より汎用的な関数を作ることが求められることも多い。 上の例で言えば、「長さが共通の任意の2つの数値リストに対して距離を計算する関数」に拡張することなどが考えられ、その場合は以下のように書ける。

# 3つ以外の成分でも使えるユークリッド距離を計算する関数
def calc_d_Re(l1,l2):           
    if len(l1) != len(l2):
        print("次元が違います!")
        return None
    distance = 0
    for i in range(len(l1)):
        distance += (l1[i] - l2[i])**2
    distance = distance ** 0.5
    return distance

注意

当然だが、関数の定義は関数の呼び出しよりも前に行う必要がある。 関数1の中で自作関数2を読み出すような書き方(定義の順序が前後すること)はOKだが、

def func_1():
    print("これは関数1です")
    func_2()

def func_2():
    print("これは関数2です")

func_1()

読み出し時に定義されていない関数は当然ながら使用できない

your_function()

def your_funtion():
    print("定義されていない関数は実行できないので、ココは読まれないよ!!")
    return None

3.1.1. 引数のデフォルト値

関数を定義するときに、引数にデフォルト値(とくに値を指定しなければこの値が選ばれる)を設定することも出来る。

#数値リストの要素のp乗和を計算する関数
def sump(tmp,p=2): 
    return sum([tmp[i]**p for i in range(len(tmp))])

list1 = [10.0, 20.0, 30.0, 40.0]
print("default", sump(list1)) #pを指定しなければp=2が選ばれる
print("p=1", sump(list1,p=1))
print("p=2", sump(list1,2))
print("p=3", sump(list1,3))

上の場合、引数を指定する際にp=などは書いても書かなくてもなくてもOKだが、デフォルト値が複数設定されている関数を作った場合には、どの変数を指定しているのかを明示的にするため、p=3などと引数に入力する。

ココまでで説明したように、自作の関数を定義することで作業を再利用可能なものとして抽象化しコードを簡略化することができます。「繰り返しの操作は関数にする」ことを心がけよう。

3.2. 変数のスコープについて

以下の内容は、これまで学習したfor文や関数のインデントとも関連した話題で、非常に重要な反面、初学者がつまづきやすい点でもある。

一般に、プログラミングではグローバル変数ローカル変数と呼ばれるものがある。 その名(global/local)が示すとおりグローバル変数とはどこからでも参照できる変数で、 ローカル変数とは、ある有効範囲(たとえば関数内)のみで参照できる変数になる。

例を見ながら理解していこう。

a = 2
list1 = [10.0, 20.0, 30.0, 40.0]

のように、関数内での代入などブロック化に書かれたコードではない場合、変数はグローバル変数として定義される。
そのため、一度定義されれば関数に引数として渡さなくても参照することができる。

def testfunc():
    print(a)

a = 2
testfunc()

一方、関数の中で定義(代入)されるローカル変数は、関数の外で参照することはできない。 (注: あとで説明するように関数内でglobal変数であることを宣言すれば関数の外でも参照できるがあまり推奨はされない)

以下のコードを実行して,関数の中で定義された変数abcdprintしようとしてもエラーが起こってしまう。

def testfunc():
    abcd = 1.000
testfunc()
print(abcd)

では、次のコードを実行すると、最後に表示されるaの値はどうなるだろうか?

2?それとも5?

def testfunc():
    a = 5
a= 2
testfunc()
print(a)

となりaの値は更新されない。 これはtestfuncの中で定義されているaは、関数の内部で定義(代入)される変数であるため、ローカル変数とみなされて処理が行われるため。

実際id関数を用いて取得できる変数のIDをprintさせてみると、関数の内と外とでidが異なることも分かる。

def testfunc():
    a = 5
    print("関数の内部", a, id(a))
    
a= 2 
print("関数の実行前", a, id(a))
testfunc()
print("関数の実行後", a, id(a)) 

一方で、

def testfunc():
    global abc, a #global変数の宣言
    abc = 5
    a += 2

a=2
print("実行前")
print("a",a , id(a))
testfunc()
print("実行後")
print("a", a, id(a))  #別の変数として再定義されていることが分かる
print("abc", abc)

といったように、関数の中で使う変数をグローバル変数として宣言すれば、関数の外でもその変数を使うことができる。

ただし、このようなコードの書き方は、処理が複雑化してくるとどこでその変数が定義されたり更新されたりしているかがわかりづらく、予期しない挙動の原因にもなるため一般には非推奨である。

[関数には引数として変数を渡して、必要な戻り値を取得する]というコードを書くのがあくまで基本となる。 (まぁ細かいことを言えばPythonだとそれともちょっと設計思想が違うのですが…)

Pythonでは、インデントでブロックを定義したりすることで短いコードを書くことができるが、一方で変数のスコープが分かりづらいことがしばしばある。 たとえば関数内で、定義されていない変数を用いたコードがあればPythonでは「global変数で定義されているのでは?」と解釈され実行が試行されるが、このことを「気が利いている」と感じる人もいれば、「意図しない参照が起きて余計なバグの温床になる」と、見る人によって違う捉え方になったりする。

関数を用いる際に、変数のスコープに関して混乱を避ける手助けとなる方法は…メインプログラムと関数内とで変数の命名規則を区別しておく:
たとえば…メインコード(global変数)で使うリストの名前の区別には数字を使う、関数の引数にはアルファベットを使うなどの工夫(ルール作り)もオススメ。

def func_join(listA,listB): #特殊なリストの結合をして返す関数, 処理に特に意味はない。
    return listA + 2 * listB 

list1 = [2.0, 30.0, 18.0]
list2 = [9.0, 4.0, 8.0]
nlist = func_join(list1, list2)

3.2.1. \(\clubsuit\) 関数内でのリスト更新

上では、数値(float)と関数を例に説明しましたが、リストの場合はもう少し挙動が複雑になる。

def func_update_list(in_list):
    in_list[0] = "AAA"

tmp = ["SS", 1, 2, 3]
print("実行前", tmp, id(tmp), id(tmp[0]))
func_update_list(tmp)
print("実行後", tmp,id(tmp),id(tmp[0])) 

リストオブジェクト自体のidは引き継がれていて、リスト内要素(0番目)の更新が反映されていることがわかる。

def func_update_list(in_list):
    in_list[0] = "AAA" 
    in_list = ["BBB", 0, 1, 2]  ##ココはローカル変数扱い
    return in_list

tmp = ["SS", 1, 2, 3]
print("実行前", tmp, id(tmp), id(tmp[0]))
ret = func_update_list(tmp)
print("実行後", tmp,id(tmp),id(tmp[0])) 
print("ret", ret,id(ret),id(ret[0])) 

3.3. 関数とメソッド

これまで登場してきたprintlenなどの関数は、Pythonに組み込まれている関数で、関数()という自作関数と同じ方法で呼び出せた。

一方で、リストや文字列などのオブジェクトに対して、オブジェクト.関数()という形で呼び出せる関数がある。

これらはメソッドと呼ばれ、それぞれのオブジェクト(正確にはクラス)に対して定義された関数になっている。

たとえば、リストに対してappendというメソッドを呼び出すと、リストの末尾に要素を追加することができた:

a = [1,2,3]
a.append(4)
print(a)

関数もメソッドも、引数を取り何らかの操作をするという点では同じだが、両者はその設計思想が異なるため、混乱しないように注意する必要がある。 大雑把に言えば、関数は引数を取り、何らかの操作を行い、戻り値を返すという設計思想であるのに対し、メソッドはオブジェクトに対して何らかの操作を行うという設計思想である。

この授業では、クラスについての説明を行わないため、自分でクラスないしメソッドを定義することはないとは思うが、 ライブラリ等で用意されているクラス・メソッドを用いることも多いため、混乱した場合はこの違いを意識するようにすると良い。

3.3.1. 組み込み関数の一覧

Pythonのdocumentから組み込み関数の一覧を確認することが出来る: https://docs.python.org/ja/3/library/functions.html

授業資料で既に使用したものとしてはprint,type, len, range, id, strなどがある。

関数名

出来ること

実行例

print

メッセージを表示する

print("Hello, World!")

type

オブジェクトの型を返す

type(10)

len

オブジェクトの長さを返す

len([1, 2, 3])

range

指定された範囲の整数を生成する

range(5)

id

オブジェクトの識別子を返す

id("Hello")

str

オブジェクトを文字列に変換する

str(3.14)

abs

数値の絶対値を返す

abs(-5)

sum

リストの要素の合計を返す

sum([1, 2, 3, 4])

min

リストの最小値を返す

min([5, 2, 8, 1])

max

リストの最大値を返す

max([5, 2, 8, 1])

sort

リストをソートする

l = [5, 2, 8, 1]; l.sort()

open

ファイルを開く

file = open("data.txt", "r")

int

文字列や浮動小数点数を整数に変換する

int("10")

float

文字列や整数を浮動小数点数に変換する

float("3.14")

complex

実数と虚数から複素数を作成する

complex(2, 3)

list

リストと互換性のあるものをリストに変換する

list((1, 2, 3))

dict

キーと値のペアから辞書を作成する

dict([('a', 1), ('b', 2)])

上で”変換する”や”互換性”と書いたように、幾つかの型やクラスの間には互換性があり、関数(やメソッド)で相互に変換する事ができることも多い。

例えばfor文で頻出するrangeはリストに変換することができた(2章も参照):

list(range(10))