関数

関数の定義と引数

関数を定義するにはdefキーワードを用いる.関数が呼び出されたときの実行内容は,インデントを1つ下げて記述する.

def 関数名(引数1の変数, 引数2の変数, ...):
    # 関数が呼び出されたときに実行する処理

関数wは1つの引数nを受け取る関数で,その実行内容は文字'w'n回表示することである.

def w(n):
    for i in range(n):
        print('w', end='')
w(5)
wwwww
w(3)
www

以下の関数wは2つの引数ncを受け取り,文字cn回表示する.

def w(n, c):
    for i in range(n):
        print(c, end='')    
w(3, 'フ')
フフフ

以下の関数wは2つの引数ncを受け取り,文字cn回表示する.ただし,引数cが指定されなかったときは,cの規定値(デフォルト値)として'w'を用いる.

def w(n, c='w'):
    for i in range(n):
        print(c, end='')
w(3, 'フ')
フフフ
w(3)
www

以下の関数wは2つの引数ncを受け取り,文字cn回表示する.ただし,引数nが指定されなかったときはデフォルト値として3を,引数cが指定されなかったときはデフォルト値とし'w'を用いる.

def w(n=3, c='w'):
    for i in range(n):
        print(c, end='')
w(5, 'フ')
フフフフフ

nの引数を省略し,cの引数だけを指定するために,以下のような関数呼び出しを行うとエラーになる.

w(, 'フ')
  File "<ipython-input-11-7c77a825b8d7>", line 1
    w(, 'フ')
      ^
SyntaxError: invalid syntax

このような時は,引数を渡したい変数を明示的に指定して関数を呼び出せばよい.

w(c='フ')
フフフ

以下の関数呼び出しはすべて同じ結果になる.

w(5, c='フ')
フフフフフ
w(n=5, c='フ')
フフフフフ
w(c='フ', n=5)
フフフフフ

関数の戻り値

関数から値を返すにはreturn文を用いる.

def plus_one(x):
    x += 1
    return x

関数の戻り値を評価することで,関数の値が表示される.

plus_one(3)
4

先ほど説明した関数wは,関数の内部でprint関数を呼び出して文字を表示していたが,ここではincrement関数の戻り値が評価されることによって,関数の値が表示されている.以下のように,関数の戻り値を変数rに代入した段階では値が表示されず,変数rを評価してから値が表示される.

r = plus_one(0)
r
1

関数の戻り値として文字列を返すこともできる.

def measure_word(n):
    if n in (1, 6, 8, 10):
        return 'ぽん'
    elif n == 3:
        return 'ぼん'
    else:
        return 'ほん'
for i in range(1, 11):
    print(i, measure_word(i))
1 ぽん
2 ほん
3 ぼん
4 ほん
5 ほん
6 ぽん
7 ほん
8 ぽん
9 ほん
10 ぽん

関数の戻り値として,複数の値を返すこともできる(厳密には複数の値がタプルと呼ばれる1つのオブジェクトにまとめられてから返され,その戻り値が関数の呼び出し元で分解される).

def fg(x):
    return x ** 2 - 2, 2 * x
fx, gx = fg(0)
fx
-2
gx
0

以下の関数divisorは与えられた整数nに約数があれば,約数のなかで最小のものを返す.

def divisor(n):
    for a in range(2, n//2+1):
        if n % a == 0:
            return a
divisor(12)
2

nが素数の場合(約数がない場合)はreturn文が実行されないため,値を返さない.

divisor(11)

より正確には,Noneと呼ばれる特殊な定数が返されている.

a = divisor(11)
type(a)
NoneType

値がNoneかどうかは,オブジェクトの同一性を検査する演算子isを用いる(divisor(n) == Noneとは書かない).これは慣習だと思ったほうがよい(正確な理由はオブジェクトの比較がカスタマイズされたとしてもNoneとの比較が正しく行われるようにするためである).

n = 11
if divisor(n) is None:
    print(n, 'は素数')
else:
    print(n, 'は素数ではない')
11 は素数

ドキュメンテーション文字列

関数宣言の直後に文字列を書くと,ドキュメンテーション文字列として扱われる.他のプログラミング言語ではコメントとして記述することが多いが,ドキュメンテーション文字列はインタプリタ上で参照可能であるうえ,Sphinxなどのドキュメンテーション生成ツールなどで用いられる.

def gcd(x, y):
    """
    Find the greatest common divisor of the given numbers.
    
    >>> gcd(60, 48)
    12
    
    >>> gcd(17, 53)
    1
    """
    while y != 0:
        (x, y) = (y, x % y)
    return x

ドキュメンテーション文字列はhelp関数で参照できる.

help(gcd)
Help on function gcd in module __main__:

gcd(x, y)
    Find the greatest common divisor of the given numbers.
    
    >>> gcd(60, 48)
    12
    
    >>> gcd(17, 53)
    1

Jupyterでは関数名に続けて?を書くことで,ドキュメンテーション文字列を参照できる.

gcd?
Signature: gcd(x, y)
Docstring:
Find the greatest common divisor of the given numbers.

>>> gcd(60, 48)
12

>>> gcd(17, 53)
1
File:      /mnt/c/Users/okazaki/Documents/projects/python/<ipython-input-31-5209d8471bf3>
Type:      function

なお,ドキュメンテーション文字列に含まれている実行例を使って,関数のテストを実行することができる.gcd関数のドキュメンテーション文字列には以下の実行例が書かれている.実際に関数を実行したけっか,期待通りの値が返されるかどうか,テストできる.

>>> gcd(60, 48)
12

>>> gcd(17, 53)
1
import doctest
doctest.run_docstring_examples(gcd, globals(), verbose=True)
Finding tests in NoName
Trying:
    gcd(60, 48)
Expecting:
    12
ok
Trying:
    gcd(17, 53)
Expecting:
    1
ok

変数のスコープと関数

Jupyterのセルで定義された変数はグローバル変数として維持される.

x = 1

関数の中からグローバル変数の値を取得できる.

def f():
    print('関数f: x =', x)

f()
関数f: x = 1

関数の中で変数を定義すると,その変数は関数内のローカル変数として管理される.

def f():
    x = 0
    print('関数f: x =', x)

f()
print('関数外: x =', x)
関数f: x = 0
関数外: x = 1

グローバル変数の値をどうしても更新したい時は,global文を使う(普通は使わない).

def f():
    global x
    x = 0
    print('関数f: x =', x)

f()
print('関数外: x =', x)
関数f: x = 0
関数外: x = 0

関数内のローカル変数に別の関数からアクセスすることはできない.以下のコードでは,関数fの中で関数gで定義された変数zにアクセスできないため,エラーとなる.

def f():
    print(z)
    
def g():
    z = 1
    f()

g()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-39-ec0738dc4dae> in <module>
      6     f()
      7 
----> 8 g()

<ipython-input-39-ec0738dc4dae> in g()
      4 def g():
      5     z = 1
----> 6     f()
      7 
      8 g()

<ipython-input-39-ec0738dc4dae> in f()
      1 def f():
----> 2     print(z)
      3 
      4 def g():
      5     z = 1

NameError: name 'z' is not defined

関数の引数で渡された変数の値を関数内で変更しても,呼び出し元の変数の値に影響を与えない.ただし,後述するコレクション(リストや辞書など)を引数に渡したときには異なる挙動を示す.以下のコードでは,関数内でx += 1されるが,呼び出し元の変数の値は変更されない.

x = 1
def f(x):
    x += 1
    print('関数f: x =', x)

f(x)
print('関数外: x =', x)
関数f: x = 2
関数外: x = 1

関数オブジェクト

\(f(x) = x^2 - 2 = 0\)の解をNewton-Raphson法で求めるとき,\(f(x)\)\(f'(x)\)の値を,それぞれ,関数fとgの呼び出しで取得できるようにすると,プログラムの見通しが良くなる.

def f(x):
    return x ** 2 - 2

def g(x):
    return 2 * x

x = 1
while True:
    fx, gx = f(x), g(x)
    if -1e-8 < fx < 1e-8:
        break
    x -= fx / gx
print(x)
1.4142135623746899

これを,\(h(x) = x^2 +5x + 6 = 0\)の解を求めるプログラムに書き換えるには,例えば以下のような修正を行えばよい.

  • \(f(x)\)\(f'(x)\)の値を返す関数hfとhgを実装する

  • Newton-Raphson法の実装で関数fとgを呼び出していた箇所を,hfとhgに変更する.

ところが,この方針ではNewton-Raphson法の実装の多くの部分をコピー・ペーストすることになる.

def hf(x):
    return x ** 2 + 5 * x + 6

def hg(x):
    return 2 * x + 5

x = 1
while True:
    fx, gx = hf(x), hg(x)
    if -1e-8 < fx < 1e-8:
        break
    x -= fx / gx
print(x)
-1.9999999999999991

また,Newton-Raphson法を実行するときの初期値を変更するには,冒頭のx = 1の行だけを変更するだけであるが,やはりNewton-Raphson法の実装の多くをコピー・ペーストする必要がある.

x = -4
while True:
    fx, gx = hf(x), hg(x)
    if -1e-8 < fx < 1e-8:
        break
    x -= fx / gx
print(x)
-3.0000000002328306

そこで,Newton-Raphson法のアルゴリズムの実装を再利用できるようにするため,アルゴリズム本体を関数として実装する.以下のnewton_raphson関数は,

  • 第1引数(func_f)に解を求めたい二次多項式を表現した関数を指定する

  • 第2引数(func_g)に解を求めたい二次多項式の微分を表現した関数を指定する

  • 第3引数(x)にNewton-Raphson法の初期値を指定する(省略された場合は\(0\)とする)

def newton_raphson(func_f, func_g, x=0):
    while True:
        fx, gx = func_f(x), func_g(x)
        if -1e-8 < fx < 1e-8:
            return x
        x -= fx / gx

既に定義済みの関数hfに対応する方程式の解を求めるには,次のようにすればよい.

newton_raphson(hf, hg)
-1.9999999999946272

初期値を\(x=-4\)として,関数hfに対応する方程式の解を求める(もうひとつの解が求まる).

newton_raphson(hf, hg, -4)
-3.0000000002328306

異なる関数(既に定義済みの関数fとg)に対応する方程式の解を求めるには,newton_raphson関数の引数を変更するだけでよい.

newton_raphson(f, g, 1)
1.4142135623746899

newton_raphson関数のように,引数に関数を渡すことができるのは,変数も関数もオブジェクトへの参照として扱われているためである.

f
<function __main__.f(x)>
type(f)
function

関数fを別の変数qに代入すると,qも同じ関数として呼び出すことができる.

q = f
q(0)
-2
f(0)
-2

無名関数(lambda関数)を使うと,defキーワードで関数を定義せずに,簡単な関数を定義できる.無名関数は,以下のように記述する

lambda 引数のリスト: 返り値

手始めに,\(3x^2+6x-72\)をlambda関数として定義し,その関数オブジェクトを変数fに代入する.

f = lambda x: 3 * x ** 2 + 6 * x - 72

変数fは関数として使うことができる.

f(0)
-72
f
<function __main__.<lambda>(x)>
type(f)
function

関数を定義せずに\(3x^2+6x-72=0\)の解を求めるには,\(3x^2+6x-72\)と,その微分である\(6x+6\)をlambda関数として定義し,newton_raphson関数の引数に渡せばよい.

newton_raphson(lambda x: 3 * x ** 2 + 6 * x - 72, lambda x: 6 * x + 6)
4.000000000053722