4. 関数#

4.1. 関数の定義と引数#

関数を定義するには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, 'フ')
フフフ

4.1.1. キーワード引数#

関数に値を渡すとき、変数名を明示することもできる(キーワード引数)。

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

キーワード引数を使うと、関数定義時の順番に関係なく引数を渡すことができる。

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

関数定義時の順番による引数(この例ではn)とキーワード引数(この例ではc)を混ぜて関数を呼び出すこともできる。

w(3, c='フ')
フフフ

キーワード引数の後に順番による引数を配置することはできない(もともと、関数wの2番目の引数はcを取ることになっていたが、キーワード引数n=5を渡してしまったので、cを順番による引数として渡すことができなくなった)。

w(n=5, 'フ')
  File "/tmp/ipykernel_2572/1557430336.py", line 1
    w(n=5, 'フ')
           ^
SyntaxError: positional argument follows keyword argument

同じ引数を2回以上渡すことはできない(以下の例では引数cが2回渡されている)。

w(c='フ', n=5, c='w')
  File "/tmp/ipykernel_2572/1632735227.py", line 1
    w(c='フ', n=5, c='w')
                    ^
SyntaxError: keyword argument repeated

4.1.2. 引数のデフォルト値#

以下の関数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 "/tmp/ipykernel_2572/3711912412.py", line 1
    w(, 'フ')
      ^
SyntaxError: invalid syntax

このような時は、キーワード引数で関数を呼び出せばよい。

w(c='フ')
フフフ

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

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

4.2. 関数の戻り値#

関数から値を返すには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 は素数

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

関数宣言の直後に文字列を書くと、ドキュメンテーション文字列として扱われる。他のプログラミング言語ではコメントとして記述することが多いが、ドキュメンテーション文字列はインタプリタ上で参照可能であるうえ、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:      /tmp/ipykernel_2572/1813389976.py
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

4.4. 変数のスコープと関数#

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)
/tmp/ipykernel_2572/2461913939.py in <module>
      6     f()
      7 
----> 8 g()

/tmp/ipykernel_2572/2461913939.py in g()
      4 def g():
      5     z = 1
----> 6     f()
      7 
      8 g()

/tmp/ipykernel_2572/2461913939.py 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

4.5. 関数オブジェクト#

\(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

fやgといった関数を定義せずに\(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

4.6. 可変長引数#

(このセクションの内容を理解するには、タプル辞書の知識が必要である)

4.6.1. 可変長引数リスト#

以下の関数decimalは任意の長さ(可変長)の引数をとり、対応する10進数を返す。

def decimal(*args):
    v = 0
    for arg in args:
        v *= 10
        v += arg
    return v
decimal(3, 2, 4)
324
decimal(6, 5, 5, 3, 4)
65534

関数に渡された引数はタプルとして変数argsに格納されている。

def func(*args):
    print(type(args), repr(args))
func(3, 2, 4)
<class 'tuple'> (3, 2, 4)

関数decimalに可変長の引数を渡す代わりに、タプルを渡すことを意図したが、arg = ((3, 2, 4),)となってしまうので、期待通りの動作にはならない。

V = (3, 2, 4)
decimal(V)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_2572/399528650.py in <module>
      1 V = (3, 2, 4)
----> 2 decimal(V)

/tmp/ipykernel_2572/979405978.py in decimal(*args)
      3     for arg in args:
      4         v *= 10
----> 5         v += arg
      6     return v

TypeError: unsupported operand type(s) for +=: 'int' and 'tuple'

タプルやリストを関数の引数として展開するには、*を付ける。

decimal(*V)
324

なお、print関数の引数としてVを渡すとき、*の有無で実行結果が変化する(以上の理屈から考えてみよう)。

print(V)
(3, 2, 4)
print(*V)
3 2 4

decimal関数が10進数ではない引数を受け付けるようにするため、底(base)を引数に指定できるように改良した。

def decimal(base, *args):
    v = 0
    for arg in args:
        v *= base
        v += arg
    return v

以下の関数呼び出しは分かりにくいかもしれないが、最初の引数である2は変数baseに格納され、以降の引数は可変長引数としてargsにタプルとして格納されるので、2進数の1010、すなわち10が返される。

decimal(2, 1, 0, 1, 0)
10

関数呼び出しの分かりにくさを軽減するため、baseをキーワード引数として関数を呼び出してみたが、キーワード引数の後に可変長引数を配置することができないため、エラーになる。

decimal(base=2, 1, 0, 1, 0)
  File "/tmp/ipykernel_2572/3667318082.py", line 1
    decimal(base=2, 1, 0, 1, 0)
                    ^
SyntaxError: positional argument follows keyword argument

baseをキーワード引数として渡したい場合は、可変長引数の後ろに配置すればよい。

def decimal(*args, base):
    v = 0
    for arg in args:
        v *= base
        v += arg
    return v
decimal(1, 0, 1, 0, base=2)
10

4.6.2. 可変長キーワード引数#

以下のencode関数は引数srcの各文字を、キーワード引数で指定された文字列に置換したものを返す。

def encode(src, **kwargs):
    dst = ''
    for c in src:
        dst += kwargs.get(c, '')
    return dst
encode('abc', a='0', b='10', c='110', d='1110', e='1111')
'010110'

関数に渡された引数は辞書として変数kwargsに格納されている。

def func(src, **kwargs):
    print(type(kwargs), repr(kwargs))
func('abc', a='0', b='10', c='110', d='1110', e='1111')
<class 'dict'> {'a': '0', 'b': '10', 'c': '110', 'd': '1110', 'e': '1111'}

関数encodeに可変長キーワード引数を渡す代わりに、辞書を渡すことを意図したが、そのままでは辞書を可変長キーワード引数として扱わないため、エラーになる。

D = {'a': '0', 'b': '10', 'c': '110', 'd': '1110', 'e': '1111'}
encode('abc', D)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_2572/2598886634.py in <module>
----> 1 encode('abc', D)

TypeError: encode() takes 1 positional argument but 2 were given

辞書を関数の可変長キーワード引数として渡すには、**を付ける。

encode('abc', **D)
'010110'

以下は、関数encodeがキーワード引数unkを取り、さらに可変長キーワード引数を受け取る例である。

def encode(src, unk='?', **kwargs):
    dst = ''
    for c in src:
        dst += kwargs.get(c, unk)
    return dst

キーワード引数unkを省略して呼び出す例である。

encode('abcz', a='0', b='10', c='110', d='1110', e='1111')
'010110?'

順番による引数よりも後であれば、キーワード引数unkをどこに書いてもよい。

encode('abcz', unk='#', a='0', b='10', c='110', d='1110', e='1111')
'010110#'
encode('abcz', a='0', b='10', c='110', d='1110', e='1111', unk='!')
'010110!'

以下の実行結果からも明らかなように、キーワード引数unkは辞書kwargsに含まれない。

def func(src, unk='?', **kwargs):
    print(type(kwargs), repr(kwargs))
func('abcz', a='0', b='10', c='110', d='1110', e='1111', unk='!')
<class 'dict'> {'a': '0', 'b': '10', 'c': '110', 'd': '1110', 'e': '1111'}

可変長引数リストと可変長キーワード引数を同時に受け付けることも可能である。

def encode(*args, unk='?', **kwargs):
    s = ''
    for arg in args:
        s += kwargs.get(arg, unk)
    return s
encode('a', 'b', 'c', 'z', a='0', b='10', c='110', d='1110', e='1111')
'010110?'
encode('a', 'b', 'c', 'z', a='0', b='10', c='110', d='1110', e='1111', unk='!')
'010110!'