Python

【Python 11】関数(追補編)

環境

macOS Big Sur
Python3.8

関数オブジェクト

関数の呼び出しは「関数名()」としますが、()を省略し「関数名」とすることで、オブジェクトとして関数を使用することができます。

関数オブジェクトは、主に関数の引数や戻り値として使用されます。

# 関数の定義。
>>> def func(a):
...     return a + 2
... 

# 関数の呼び出し。
>>> b = func(1)
>>> b
3

# 関数オブジェクトの代入。
>>> c = func
>>> c
<function func at 0x7fbfd21c8f70>
>>> c(1)  # 引数を指定することで呼び出しができます。
3

 

クラスの属性のメソッドについても、「インスタンス名.メソッド名」とすることでメソッドオブジェクトとして使用することができます。

>>> class MyClass:
...     def method(self):
...         return print('Hello world!')
... 
>>> i = MyClass()  # インスタンスiの生成。
>>> x = i.method  # メソッドオブジェクトをxに代入。
>>> x()
Hello world!

 

イテレータ

繰り返し可能な(for文で繰り返すことができる)オブジェクトを「イテラブル」といいます。また、イテラブルから生成される要素を順番に取り出すことができるオブジェクトを「イテレータオブジェクト」いいます。

イテレータオブジェクトは全ての要素をあらかじめ用意するのではなく、必要になった時に必要な分だけ要素を取り出す処理を行います。

イテラブルをイテレータオブジェクトに変換するには「iter()関数」を使用します。

また、イテレータオブジェクトから要素を順番に取り出すには「next()関数」を使用します。

>>> a = iter([1, 2, 3])  # イテラブルであるリストからイテレータオブジェクトを生成します。
>>> next(a) # 「next()関数」で順番に取り出します。
1
>>> next(a)
2
>>> next(a)
3
>>> next(a) # 取り出す要素がなくなるとエラーになります。
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 

実は、for文のループ処理は、「iter()関数」でイテレータオブジェクトを生成し、「next()関数」で要素を取り出すという処理を内部的に行っているにすぎません。

ジェネレータ

イテレータオブジェクトを関数で定義することもできます。関数で「return文」の代わりに「yield文」を使用することで、戻り値がイテレータオブジェクトとして返されます。このような仕組みを「ジェネレータ」といい、ジェネレータを実装した関数を「ジェネレータ関数」といいます。

ジェネレータ関数の場合、関数呼び出しでイテレータオブジェクトを取得することができます。この時、関数のブロック自体は実行されません。「next()関数」を使って値を取り出す時に関数ブロックは実行されます。

a = [1, 2, 3]


def func(b):
    for i in b:
        yield i ** 2


# ジェネレータ関数からイテレータオブジェクトを取得します。
c = func(a)

# イテレータオブジェクトから「next()関数」で値を取り出します。
print(next(c))
print(next(c))
print(next(c))


# 実行結果
1
4
9

 

高階関数

関数オブジェクトを引数に受け取る関数や戻り値として出力する関数を「高階関数」といいます。

# 関数オブジェクトを戻り値に受け取る高階関数の例。
# 関数内関数(関数のネストといいます。)
def outer(a):
    def inner(b):
        return a + b
    return inner


# 関数outerを呼び出すことで、関数オブジェクトinnerが返されます。
# 関数outerは、引数aを特定の状態でキープした関数オブジェクト(inner)を作ることができます。このような関数を「クロージャ」(closure)といいます。
a = outer(1)  # aには関数オブジェクトinnerが代入されます。
print(a(2))  # inner(2)の呼び出しと同様です。

print(outer(1)(2))  # 引数を並べて指定することもできます。


# 実行結果
3

 

なお、外側の関数(上記のouter()関数)から内側の関数(上記のinner()関数)内の変数にアクセスすることはできません。(※グローバル変数とローカル変数の関係と同様です。)

逆に、内側の関数から外側の関数内の変数は、「nonlocal文」で明示的に指定することでアクセスすることができます。(※「global文」と同様の考え方です。)

デコレータ

関数オブジェクトを引数に受け取り、新たな関数オブジェクトを返す高階関数を使用することで、ある関数(この場合、引数として受け取った関数)を変更することなく、機能を追加することができます。これを、「デコレーション」といいます。

追加する機能を実装したオブジェクトを「デコレータ」といいます。

# 関数func2をデコレーションする関数(デコレータ)です。
# 引数bを2乗する関数を引数に取り、さらに2乗する関数オブジェクトを返します。
def deco(func1):
    def inner(a):
        return func1(a) ** 2
    return inner


# 元の関数とします。
def func2(b):
    return b ** 2


c = deco(func2)  # cには関数オブジェクトinnerが代入されます。
print(c(2))


# 実行結果
16

 

デコレーションしたい関数の先頭に「@デコレータ名」を追記することで、デコレータからではなく、元の関数から直接実行することができます。

def deco(func1):
    def inner(a):
        return func1(a) ** 2
    return inner


@deco
def func2(b):
    return b ** 2


print(func2(2))


# 実行結果
16

 

lambda式

「lambda(ラムダ)式」を用いることで、その場限りの関数を定義することができます。lambda式は関数名を持たないため「無名関数」とも呼ばれます。

lambda式

lambda 引数1, 引数2, …: 戻り値の式

※このlambda式は以下の関数定義と同様の結果となります。
    def func(引数1, 引数2, …):
        return 式

※lambda式は単一の式しか持つことができません。

# 通常の関数定義。
def func(a):
    return a ** 2


print(func(3))

# lambda式
print((lambda a: a ** 2)(3))  # 引数を指定することもできます。


# 実行結果
9

 

lambda式は、関数オブジェクトを引数として使用する場合等に利用されます。

「map(関数オブジェクト, イテラブル)関数」は、イテラブルの全要素に対して関数を適用するイテレータです。lambda式を使用するとシンプルに記述することができます。

a = [1, 2, 3]


def func(b):
    return b ** 2


# map()関数でリストaの要素にfunc()関数を適用します。
# map()関数はイテレータオブジェクトを返すので、list()関数でリスト化します。
print(list(map(func, a)))


# lambda式を使用すると引数に直接関数を入れ込むことができます。
print(list(map(lambda b: b ** 2, a)))


# 実行結果
[1, 4, 9]

 

内包表記(コンプリヘンション)

for文を使用してリストや辞書、セットを生成する場合、「内包表記(コンプリヘンション)」を使用することで、式として定義することができます。

lambda式と同様に、内包表記は式ですので、そのまま関数の引数に渡すことができます。

リスト内包表記

リストの生成を内包表記で記述する場合、要素を作る式に続けて、for文のヘッダー部分を記述し、全体を角カッコで囲います。

また、ifに続けて条件式を追記することで、条件がTrueとなった場合のみ式の評価を行うようにすることができます。

リスト内包表記

[要素を作る式 for 変数 in イテラブル if 条件式]

# 複合文によるリストの生成。
a = []

for i in range(6):
    a.append(i**2)

print(a)


# リスト内包表記によるリストの生成。
a = [i**2 for i in range(6)]

print(a)


# 実行結果
[0, 1, 4, 9, 16, 25]


# ifによる条件式を追記した例。
# 複合文によるリストの生成。
a = []

for i in range(6):
    if i % 2 == 0:
        a.append(i**2)

print(a)


# リスト内包表記によるリストの生成。
a = [i**2 for i in range(6) if i % 2 == 0]

print(a)


# 実行結果
[0, 4, 16]

 

辞書内包表記

辞書の生成を内包表記で記述する場合、まずキーを作る式と値を作る式をコロンで繋げます。その後、for文のヘッダー部分を記述し、全体を波カッコで囲います。

また、リスト内包表記と同様に、ifに続けて条件式を追記することで、条件がTrueとなった場合のみ式の評価を行うようにすることもできます。

辞書内包表記

{キーを作る式:値を作る式 for 変数 in イテラブル if 条件式}

# 複合文による辞書の生成。
# キーと値を入れ替える処理です。
a = {1: 'Ⅰ', 2: 'Ⅱ', 3: 'Ⅲ'}
b = {}

for i in a:
    b[a[i]] = i

print(b)


# 辞書内包表記による辞書の生成。
a = {1: 'Ⅰ', 2: 'Ⅱ', 3: 'Ⅲ'}
b = {a[i]:i for i in a}

print(b)


# 実行結果
{'Ⅰ': 1, 'Ⅱ': 2, 'Ⅲ': 3}

 

セット内包表記

リスト内包表記の外側の角カッコを波カッコにすることで、セットの生成を内包表記で記述できます。セットですので、重複する要素は取り除かれます。

また、リスト内包表記と同様に、ifに続けて条件式を追記することで、条件がTrueとなった場合のみ式の評価を行うようにすることもできます。

セット内包表記

{要素を作る式 for 変数 in イテラブル if 条件式}

ジェネレータ式

リスト内包表記の外側の角カッコを丸カッコにすることで、ジェネレータを式で記述することができます。

また、ifに続けて条件式を追記することで、条件がTrueとなった場合のみ式の評価を行うようにすることもできます。

ジェネレータ式

(要素を作る式 for 変数 in イテラブル if 条件式)

# aはジェネレータ式からイテレータオブジェクトを取得します。
a = (i**2 for i in range(6) if i % 2 == 0)
print(next(a))
print(next(a))
print(next(a))


# 実行結果
0
4
16

 

三項演算子

「三項演算子」という記法を用いることで、if-else文を1行の式として記述することができます。

三項演算子

条件式がTrueの時の値 if 条件式 else 条件式がFalseの時の値

三項演算子を使用することで、lambda式や内包表記、ジェネレータ式に条件分岐を追加することができます。内包表記やジェネレータ式の場合、ifは最後に記述していますが、三項演算子の場合は「式」の部分で記述することになります。

なお、三項演算子でelifは使用できません。elifのような分岐を使用する場合は、ifをネストすることになります。

a = [i**2 if i % 2 == 0 else i for i in range(6)]
print(a)


# 実行結果
[0, 1, 4, 3, 16, 5]


# 三項演算子にelifはありません。
# ifのネストでelifと同様の条件分岐を表現できます。
# 可読性が悪いので、普通にif-elif-else文を使用すべきです。
a = [i if i==1 else i-1 if i==2 else i-2 if i==3 else i-3 for i in range(1, 5)]


# 実行結果
[1, 1, 1, 1]


# 上記の内包表記は以下と同様です。
a = []

for i in range(1, 5):
    if i == 1:
        a.append(i)
    else:
        if i == 2:
            a.append(i-1)
        else:
            if i == 3:
                a.append(i-2)
            else:
                a.append(i-3)
print(a)


# 実行結果
[1, 1, 1, 1]