Python 関数の応用

関数内関数

関数内関数は関数の中に定義された関数のこと

# 関数定義
def outer(a, b):

    # 関数内関数定義
    def plus(c, d):
        return c + d

    # 関数内関数呼び出し
    r = plus(a, b)

    # 結果表示
    print(r)

# 実行
outer(1, 2)

クロージャー

# 関数定義
def outer(a, b):

    # 関数内関数定義
    def inner():
        return a + b

    # 関数内関数のオブジェクトを返す
    return inner

# 実行
# 関数内関数のオブジェクトが返る
# この時点では関数内関数は実行されていない
f = outer(1, 2)

# 関数内関数を実行
r = f()
print(r)

outer(1,2)でfにinner()オブジェクトが返る
この時、inner()内のaとbには引数で渡された1, 2が保存されている
f()でinner()が実行され、計算結果がrに返る
このような関数内関数のことをクロージャーという

クロージャーの使い方

クロージャーを使うと外側の関数に渡す引数で関数内関数の状態を変えることができる。

# 関数定義
def circle_area_func(pi):
    
    # クロージャー
    # 外側関数の引数piがクロージャー内で保持される
    # クロージャーの引数radiusは実行時に渡される
    def circle_area(radius):
        return pi * radius * radius
    return circle_area # 関数内関数のオブジェクトを返す

# 実行
# 関数内関数のオブジェクトが返ってくるだけで未だ実行されない
# 引数piのそれぞれ違う値を与える
# このpiは関数内関数(クロージャー)のpiに保持される
ca1 = circle_area_func(3.14)
ca2 = circle_area_func(3.14159)

# 関数内関数の実行
# 引数10は関数内関数circle_area(radius)のradiusに渡される
print(ca1(10))     # 314.0
print(ca2(10))     # 3.14159

関数呼び出し時にpiに渡した引数がクロージャで保持される。
戻り値(関数内関数のオブジェクト)はそれぞれ渡されたpiの値を持っている。
実行時、それぞれのオブジェクトが保持しているpiの値を使って計算される。

デコレーター

デコレーターは関数やクラスの前後に特定の処理を追加できる機能です。
デコレーターは関数オブジェクトを受け取って、処理を追加したラッパー関数のオブジェクトを返します。

デコレーターを使わずに前後に”start”, “end”を出力する

# 足し算関数
def add_num(a, b):
  return a + b

# 関数実行
print('start')        # 関数実行前に'start'を出力
r = add_num(10, 20)   # 関数実行
print('end')          # 関数実行後に'end'を出力

print(r)              # 結果表示
start
end
30

デコレーターを使って前後に”start”, “end”を出力する

# デコレーター関数
# 処理を加えたい関数のオブジェクトを引数で渡す
def print_info(func):

    # 関数内関数(ラッパー)
    # 引数は外側関数の引数(関数のオブジェクト)に渡される
    # どんな引数でも受け取れるように*args, **kwargsにしている
    # *args 位置引数を何個でも(タプルやリスト)
    # **kwargs キーワード引数を何個でも(辞書)
    def wrapper(*args, **kwargs):
      print('start')                  # 'start'を出力
      result = func(*args, **kwargs)  # 関数を実行
      print('end')                    # 'end'を出力
      return result                   # 関数実行結果を返す

    # ラッパーのオブジェクトを返す
    return wapper

# 足し算関数
def add_num(a, b):
   return a + b

# 実行
# デコレーター関数に足し算関数オブジェクトを渡す
# ラッパー関数のオブジェクトが返る
f = print_info(add_num)

# 返ってきたオブジェクトを実行
# 引数10, 20は足し算関数に渡す引数
r = f(10, 20)

# 結果表示
print(r)
start
end
30

実行時のコードを簡単にする

@print_infoと書くと、コードを簡略化できる

# デコレーター関数
# 処理を加えたい関数のオブジェクトを引数で渡す
def print_info(func):

    # 関数内関数(ラッパー)
    # 引数は外側関数の引数(関数のオブジェクト)に渡される
    # どんな引数でも受け取れるように*args, **kwargsにしている
    # *args 位置引数を何個でも(タプルやリスト)
    # **kwargs キーワード引数を何個でも(辞書)
    def wrapper(*args, **kwargs):
      print('start')                  # 'start'を出力
      result = func(*args, **kwargs)  # 関数を実行
      print('end')                    # 'end'を出力
      return result                   # 関数実行結果を返す

    # ラッパーのオブジェクトを返す
    return wapper

# 足し算関数定義
@print_info              # デコレーターを指定
def add_num(a, b):       # 足し算関数
   return a + b

# 実行
# デコレーター関数を呼ばなくても良い
r = add_num(10, 20)
print(r)

# @print_infoを使わない場合の呼び出し
r = print_info(add_num)
r = f(10, 20)
print(r)

はじめにデコレーターを定義してしまえば、別の関数を作成した際にも関数の上に@で記述するだけで済む。複数の関数内に同じ処理を書く必要がなくなる。

デコレーターを複数実行する

関数名の上に重ねて@で記述するとデコレーターを複数実行できる

# 関数名や引数、戻り値を表示させるデコレーター
def print_more(func):
    # 関数内関数(ラッパー)
    def wrapper(*args, **kwargs):
        print('func:', func.__name__)      # 関数名表示
        print('args:', args)               # 引数表示
        print('kwargs', kwargs)            # 引数表示
        result = func(*args, **kwargs)     # 関数実行
        return result                      # 関数実行結果を返す
    
    # ラッパーのオブジェクトを返す
    return wrapper

# 関数実行前に'start'・'end'を表示するデコレーター
def print_info(func):
    def wrapper(*args, **kwargs):
        print('start')
        result = func(*args, **kwargs)
        print('end')
        return result
    return wrapper

# 足し算関数定義
@print_info    # デコレーター指定 'strat'・'end'
@print_more    # デコレーター指定 関数名・引数等表示
def add_num(a, b):
    return a + b

# @を使わない場合の呼び出し
r = print_info(print_more(add_num))
r = f(10, 20)
print(r)
start
func: add_num
args: (10, 20)
kwargs {}
result: 30
end
30

lambda(ラムダ式 無名関数)

lambdaは関数作成方法のひとつで無名関数とも呼ばれている。

defとlambdaの違い

defは宣言文であり事前に宣言が必要だが、lambdaは式なので簡潔に表現できる

# 2乗を求めるプログラム

# def文で記述
def square(x):
    result = x**2
    return result

y = square(2)    # 実行
print(y)         # 結果表示

# lambdaで記述
y = lambda x: x**2
print(y(2))     # 実行と結果表示

文法

lambda 引数:返り値
lambda: 返り値 # 引数なし

という形で書きます。
関数定義だと下記と同等です。

def func(引数):
return 返り値

def func():
return 返り値

lambdaの利用シーン

引数に関数のオブジェクトを渡す関数と一緒に使われることが多い。
filter(関数, リストなどのオブジェクト) 条件に合致するものを抽出する関数
map(関数, リストなどのオブジェクト) 複数要素データの要素に同じ処理を行う関数

# filter()を使ったサンプル
# 数値リストから90より大きい数値を抽出

nums = [100, 200, 50, 40]

# lambdaなし
results = []
for num in nums:
    if num > 90:
        results.append(num)

print(list(results))

# lambdaあり
results = filter(lambda num : num > 90, nums)
print(list(results))
# map()を使ったサンプル
# pre_listの名前すべてに敬称を付ける

pre_list = ['浅香', '市井', '井戸', '伊藤']

# def文で記述
def add_func(name):
    result_name = name + 'さん'
    return result_name

add_list = list(map(add_func, pre_list))
print(pre_list)
print(add_list)

# lambdaで記述
add_list = list(map(lambda name: name + 'さん', pre_list))
print(pre_list)
print(add_list)
# すでにある整数のlistにそれぞれ2乗して符号をそのままにする
# math.copysign(x,y)はx絶対値でyと同じ符号の浮動小数点を返す
from math import copysign

list = [2, -10, 5, 3, 6, 7, -8]

# def文で記述
def func(n):
    return int(copysign(n**2, n))

lis = list(map(func, list))

# lambdaで記述
lis = list(map(lambda x: int(copysign(x**2, x)), list))
# 名前と得点をtupleとして持っているlistを、得点でsortしたい
# (tupleを要素として持つlistを、それぞれのtupleの2番目の要素でsortしたい)
score = [("Adam",64),("Bob",82),("Charlie",21),("David",91)]

score.sort(key=lambda x: x[1])

ジェネレーター

イテレータ:要素を反復して取り出すことができるインターフェイス
ジェネレータ:イテレータの一種で、1要素を取り出そうとする度に処理を行い、よそをジェネレートするタイプのもの。

# イテレーター
l = ['Good morning', 'Good afternoon', 'Good night']
for i in l:
    print(i)

# ジェネレーターで同じ処理を記述
def greeting():
    yield 'Good morning'
    yield 'Good afternoon'
    yield 'Good night'

for g in greeting():
    print(g)

ジェネレーターは要素をどこまで生成したかを覚えているので、間に他の処理がはいっても問題なく次の要素を生成できる

def greeting():
    yield 'Good morning'
    yield 'Good afternoon'
    yield 'good night'

for g in greeting():
    print(g)

    for h in greeting():
        print('h', h)

# next関数で次の要素を取得する
g = greeting()
print(next(g))
print('@@@@@')
print(next(g))
print('@@@@@')
print(next(g))
print('@@@@@')
Good morning
h Good morning
h Good afternoon
h good night
Good afternoon
h Good morning
h Good afternoon
h good night
good night
h Good morning
h Good afternoon
h good night
Good morning
@@@@@
Good afternoon
@@@@@
good night
@@@@@

重たい処理を小分けにする

回数100万回などのとてつもなく多いループのような重たい処理を行う場合、一気に実行するとその間プログラムが応答しなくなります。これを避けるためにジェネレーターによって重たい処理を小分けして実行させることができる。

def greeting():
    yield 'Good morning'
    for i in range(10):
        print(i)
    
    yield 'Good afternoon'
    for i in range(10):
        print(i)

    yield 'Good night'
    for i in range(10):
        print(i)

g = greeting()
print('最初の呼び出し',next(g))
print('最初の呼び出し直後')
print('次の呼び出し', next(g))
print('次のの呼び出し直後')
print('最後の呼び出し',next(g))    
print('最後の呼び出し直後')
最初の呼び出し Good morning
最初の呼び出し直後
0
1
2
3
4
5
6
7
8
9
次の呼び出し Good afternoon
次のの呼び出し直後
0
1
2
3
4
5
6
7
8
9
最後の呼び出し Good night
最後の呼び出し直後

最初のyieldが呼び出された際には次のforループは実行されない。
次にジェネレーターが呼び出された際にforループが実行される。

コメント

タイトルとURLをコピーしました