3. 自作関数#
[この章の目的]プログラム内での自作関数と戻り値,スコープの概念を獲得する。
ここでは、Pythonにおける関数の定義と関数が返す値[戻り値(返り値とも呼ぶ)]と、変数のスコープについて説明する。
3.1. 関数の定義#
既に現れたprint
やlen
などはPythonに備え付けられた”組み込み関数”と呼ばれるものの一種である。
以下に示すように、組み込み関数とは別にユーザーが独自の関数を定義することもできる。
これを指してこの章では”自作関数”あるいは単に”関数”と呼ぶことにする。
関数とは「処理を抽象化して、再利用可能な形でまとめたもの」である。関数を使うことで、同じ処理を何度も書く必要がなくなり、プログラムの見通しがよくなる。
まずはPythonで関数を”定義”(define)する方法を以下に示した:
def 関数名(引数1,引数2,...):
処理1
処理2
...
return 戻り値
関数の構成要素は
関数名: 関数を呼び出す際に使う名前
引数: 関数に渡す値。
なくてもよいし、配列や辞書など様々な変数を渡すことができる。処理: 関数が行う処理
戻り値: 関数が返す値。 なくてもよいし、複数値も返せる。
return文を省略すると関数の返り値はNone
(値なし)となる
である。
関数名のあとに付けたコロン:
は「以下で関数の中身を記述するブロックが開始する」ことを意味していて、
インデントによってどこまでが関数のブロックかがわかるようになっている。(ブロックについてはif
やfor
を思い出そう)
たとえば、作業を複数の工程に分けて、それぞれの工程を関数として定義することで、プログラムの構造を整理することができる。 これまでの内容は、実行の度に出力をするような操作が多かったが、プログラムの用途はそれだけではない。 関数Aに引数を渡して、関数Bにその結果を渡して、関数Cにその結果を渡す、といったように、関数を組み合わせて処理を行うことができる。
幾つか例を挙げながら、関数の定義の仕方と使い方に慣れていこう。
例1: 摂氏温度を受け取り、絶対温度に変換する関数
def Celsius_to_Kelvin(cval):
K = cval + 273.15
return K
上で述べた関数の構成要素と照らし合わせると以下のようになる:
関数名:
Celsius_to_Kelvin
引数:
cval
(摂氏温度, intやfloatを想定)処理: 摂氏温度
cval
を絶対温度に変換して変数K
として定義戻り値: 計算した
K
を関数の外に返す(returnする)
この関数を呼び出す(使う)には以下のようにすればよい:
print("関数に数値0を入れた場合→", Celsius_to_Kelvin(0), "関数に数値100.0を入れた場合→", Celsius_to_Kelvin(100.0))
関数に数値0を入れた場合→ 273.15 関数に数値100.0を入れた場合→ 373.15
変数を関数に渡すことももちろんできる。
C = -123.45
print("摂氏", C, "度は、", Celsius_to_Kelvin(C), "Kです。")
摂氏 -123.45 度は、 149.7 Kです。
注意: 関数の定義
当然だが、関数の定義は関数の呼び出しよりも前に行う必要がある。 関数1の中で自作関数2を読み出すような書き方(定義の順序が前後すること)はOKだが、
def func_1():
print("これは関数1です")
func_2()
def func_2():
print("これは関数2です")
func_1()
これは関数1です
これは関数2です
実行時に定義されていない関数は当然ながら使用できず、エラーが出る。
your_function()
def your_funtion():
print("定義されていない関数は実行できないので、ココは読まれないよ!!")
return None
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[22], line 1
----> 1 your_function()
3 def your_funtion():
4 print("定義されていない関数は実行できないので、ココは読まれないよ!!")
NameError: name 'your_function' is not defined
例2: 3成分の数値を持つリストを2つ受け取り、座標間の距離を返す関数
以下のように\(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行かそれ以上になってしまう。 やはりこの場合も関数を使って処理を抽象化することが望ましい。
def calc_distance(l1,l2):
return ( (l1[0] - l2[0])**2 + (l1[1] - l2[1])**2 + (l1[2] - l2[2])**2 ) ** 0.5
t = calc_distance(p1,p2)
print("点1",p1, "と点2", p2, "の距離は", t, "です")
点1 [2.0, 4.0, -5.0] と点2 [1.0, 3.0, -4.0] の距離は 1.7320508075688772 です
print(calc_distance(p1,p100)) #←これでも使えるし
print(calc_distance([20.0, 1.0, -5.0], [-2.0, 3.0, 5.5])) #←などとして変数でなく値を直接書いても使える
10.594810050208546
24.45914961727002
関数名 =
calc_distance
引数:
l1,l2
(それぞれ座標を表す3成分のリスト or リストと互換性のある型を想定)処理: ユークリッド距離 (成分間の差分の2乗和の平方根)を計算
戻り値: 計算した距離 (float)
上の例のように100個の点の3次元座標に対応するリストがある場合、ループ(for
文)と組み合わせて
#リスト内包表記で3次元の座標点をランダムに100個作る n,iはダミー変数
import random
lists = [ [ random.gauss(0,1) for n in range(3)] for i in range(100)]
count = 0
for j in range(100):
for i in range(j+1,100): # i>j
count += 1
distance = calc_distance( lists[j], lists[i])
#print(j,i, distance) # 4950回文の計算結果をprintすると邪魔なのでコメントアウトした
print(count) #回数だけ表示しよう
#上のjのループ内で、iはj+1から99までを回る。 j+1= 100つまり j=99のとき range(j+1,100)はちゃんと空になる
#つまり、長さ100のリストにindex=100でアクセス(範囲外参照)したりすることはない。
4950
などとすれば、全組み合わせ(\({}_{100}C_2\)=4950組)に対して即座に距離を計算することが出来る。
上の例はあくまで「任意の長さが3の数値リストに対して3次元空間での距離を計算する関数」であり、3成分以上のリストに対しては対応していない。 実際に関数を定義する際には、より汎用的な関数を作ることが望ましい場合も多い。 上の例で言えば、「長さが共通の任意の2つの数値リストに対して距離を計算する関数」に拡張することで、 汎用性の高いN次元空間でのユークリッド距離を計算する関数にすることができる。
# 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
注意: 関数の引数
関数に入れる引数に用いる変数名は、実際に関数を用いる際の引数として使う変数名とは関係がなく、
関数の外側で扱う変数を上の例でいうl1
,l2
という名前にあわせて定義しておく必要はない。
関数の定義文にある引数名はあくまで、外から与えられた引数を関数内部で使う際の名前であることに注意しよう。
例3: 文字列を受け取り、別の文字列を返したり表示する関数
関数内の操作に関数外からの情報(インプット)が必要ない場合は引数なしの関数でも構わないし、
関数の外に値を渡す(アウトプットする)必要がなければreturn
文を明示的に書かなくても問題ない。
return
文がない場合は自動でNone
(値なし)が返される関数となる。
「なぜ返り値とは何なのか、なぜreturnが必要なのか分からない」という質問がたまにある。
これまでは単になにか操作をした値をprint
で表示していたが、
実際にプログラムを書く場合には、関数の結果を他の処理に使いたい場合が多い。
作業工程が単体で完結する場合はreturn
を省略してもよいが、
関数の結果を他の処理に使いたい場合はreturn
を使って返り値を指定する必要がある。
「それぞれの処理がいつどこで必要か?」などを意識すると理解が深まるように思う。
以下の2つの違いに注目しながら、関数を定義してみよう。
文字列を受け取り、上下左右をシャープ記号で装飾した文字列を表示する関数
文字列を受け取り、上下左右をシャープ記号で装飾した文字列を返す関数
def function_3_1(text_input):
text = "#" * 50 + "\n" + str(text_input).center(50, '#') + "\n" + "#" * 50
print(text)
def function_3_2(text_input):
text = "#" * 50 + "\n" + str(text_input).center(50, '#') + "\n" + "#" * 50
return text
一見すると、どちらも同じような操作を行っているように見える(実際に、text=の行は完全に一緒だ)が、 最後に、関数の中でprintで表示をしているのか、関数の外に値を返しているのか、という違いがある。
実際に関数を使ってみよう。
function_3_1(" Hello World! ")
function_3_1(" Hello World!! ")
##################################################
################## Hello World! ##################
##################################################
##################################################
################# Hello World!! ##################
##################################################
関数に引数として与えた文字列の周りをシャープ記号で囲って表示することができた。
関数の中にprint関数が用いられているため、function_3_1
を呼び出すたび、装飾された文字列が表示される。
では、この関数の返り値をprintしてみると…
print( function_3_1(" Hello World! ") )
##################################################
################## Hello World! ##################
##################################################
None
function_3_1
が呼び出されて表示された後、その関数の返り値Noneが表示されていることがわかる。
では次に、関数3_2を使ってみよう。
def function_3_2(text_input):
text = "#" * 50 + "\n" + str(text_input).center(50, '#') + "\n" + "#" * 50
return text
function_3_2(" ByeBye! ")
ret = function_3_2(" ByeBye!! ")
関数3_2の場合、関数の中にprint関数を使うところはないため、関数が実行されても何も表示されない。
また、最後の行のように関数の外で返り値を受け取って、print
関数を使って表示したり他の関数の引数として使うこともできる。
print(ret)
##################################################
#################### ByeBye!! ####################
##################################################
例えば、「入力した文字列を装飾する関数」と「入力された文字列を、指定されたアドレスに送信する関数」を作って、 それぞれの関数を組み合わせて「入力された文字列を装飾してから指定されたアドレスに送信する」といった処理を行うことができる。
\(\clubsuit\)進んだ注
返り値の説明をするうえで、少し厄介なのはGoogle Colab. (Jupyter Notebook)などの環境では、
コードセルの末尾にあるコードが代入を伴わない操作(関数の呼び出しや変数名単体の記載)の場合、
(あたかもprint
関数を使ったかのように)その値が表示される便利機能?が備わっていることである。
したがって、以下のように関数の呼び出しだけ末尾に書くと、
function_3_2(" ByeBye! ")
'##################################################\n#################### ByeBye! #####################\n##################################################'
関数の返り値が表示されることになり、3_1との違いがわかりにくい。
上の行は本来、関数を呼び出しているだけなので、返り値が表示されているのはあくまでJupyter Notebookの機能であることに注意しよう。
例4: 2つの数値\((a,b)\)を受け取り、要素の和と積、二乗和とexp(a+b)のリストを返す関数
戻り値return
は単一の値や文字列に限らず、複数の値やリスト、辞書なども返すことができる。
import math #指数関数を使うためにmathモジュールをインポート
def ret_sum_product(a,b):
return a+b, a*b, [ a**2 + b**2, math.exp(a+b)]
ret_sum_product(10, 40)
(50, 400, [1700, 5.184705528587072e+21])
複数値の返り値の場合はそれらをタプルとしてラップして返してくれていることもわかる。
tmp = ret_sum_product(10, 40)
print(tmp, "type", type(tmp), "tmp[0]", tmp[0])
(50, 400, [1700, 5.184705528587072e+21]) type <class 'tuple'> tmp[0] 50
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変数であることを宣言すれば関数の外でも参照できるがあまり推奨はされない)
以下のコードを実行して,関数の中で定義された変数abcd
をprint
しようとしてもエラーが起こってしまう。
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. 関数とメソッド#
これまで登場してきたprint
やlen
などの関数は、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
などがある。
関数名 |
出来ること |
実行例 |
---|---|---|
|
表示する (書き出しも可能) |
|
|
オブジェクトの型を返す |
|
|
オブジェクトの長さを返す |
|
|
指定された範囲の整数を生成する |
|
|
オブジェクトの識別子を返す |
|
|
オブジェクトを文字列に変換する |
|
|
数値の絶対値を返す |
|
|
リストの要素の合計を返す |
|
|
リストの最小値を返す |
|
|
リストの最大値を返す |
|
|
リストをソートする |
|
|
ファイルを開く |
|
|
文字列や浮動小数点数を整数に変換する |
|
|
文字列や整数を浮動小数点数に変換する |
|
|
実数と虚数から複素数を作成する |
|
|
リストと互換性のあるものをリストに変換する |
|
|
キーと値のペアから辞書を作成する |
|
上で書いた出来ることはあくまで一例であり、それぞれの関数には他にも様々な機能がある。 上で”変換する”や”互換性”と書いたように、幾つかの型やクラスの間には互換性があり、関数(やメソッド)で相互に変換する事ができることも多い。
例えばfor文で頻出するrange
はリストに変換することができた(2章も参照):
list(range(10))