リファクタリングをPythonで実践!基本手法とコード例・注意点まで
- 1.filter()関数とは
- 2.map()関数、reducce()関数との違い
- 3.filter()関数の使い方
- 4.filter()関数を使うときの注意点
- 5.さまざまなデータ型に使用する
- 6.組み合わせて利用する
- 7.リスト内包表記との比較
この記事ではPythonの組み込み関数のfilter()関数について解説します。
似た挙動の関数との比較や利用例を載せているのでぜひ最後までご覧ください。
1.filter()関数とは
filter()関数はPythonの組み込み関数の1つです。この関数は、リストや文字列といった複数の要素を持つデータ型オブジェクトから、関数の条件にあう要素のみ抽出するイテレータを返します。
2.map()関数、reduce()関数との違い
ここからはfilter()関数と混合しやすい関数との違いを見ていきましょう。
①map()関数は各要素に処理を行う
map()関数はすべての要素を引数として実行し、実行結果から新しいlistを作成します。
def test_plus(num):
return num + 10
map_list = [10, 20, 30]
map_list2 = map(test_plus, map_list)
print(map_list)
# [10, 20, 30]
for x in map_list2:
print(x)
# 20 30 40
map_listには変更を与えず、新しいリストを作成しました。
②reduce()関数はすべての要素を一つの値に
reduce()関数はすべての要素をまとめて1つの値にまとめます。
- 引数を2つ受け取る関数を作成
- 要素の1つ目、2つ目を受け取り結果を返す
- 2で出した結果と3つ目の要素を受け取り結果を返す
- 2、3を繰り返しすべての要素を処理する
こちらの説明では分かりにくいので、テストコードを見てみましょう。
from functools import reduce # Python3からfunctoolsモジュールに変更
def test_add(num1, num2):
return num1 + num2
reduce_list = [10, 20, 30]
print(reduce(test_add, reduce_list)) # test_add(test_add(10, 20), 30)
# 実行結果:60
test_addの第一引数にtest_add(10, 20)がきます。map()関数のように一気にオブジェクトの要素を取得せず、順番に取得するのが特徴です。
3.filter()関数の使い方
ここからはfilter()関数の使い方を紹介します。
①基本構文
filter()関数は一般的な関数とは異なり、第一引数に抽出条件を設定した関数、第二引数に抽出を実行するオブジェクトを指定します。
def check_10n(num):
return num % 10 == 0
check_list = [10, 20, 22, 25, 30, 40, 50]
checked_list = list(filter(check_10n, check_list))
print(checked_list)
# [10, 20, 30, 40, 50]
今回の例では10の倍数を抽出しました。
抽出条件に設定したcheck_10nでは数値が10で割ったときの値が0かを判定しています。
print(check_10n(11))
# False
returnで条件式を設定した場合、返ってくるのはTrueかFalseです。filter()関数はFalseで返された値を削除してくれます。
では第一引数に抽出条件でない関数を与えるとどうなるでしょうか。
def add_10n(num):
return num + 10
add_list = [10, 20, 22, 25, 30, 40, 50]
added_list = list(filter(add_10n, add_list))
print(added_list)
# [10, 20, 22, 25, 30, 40, 50]
削除できませんでした。前述のとおり、filter()関数はFalseの値を削除します。値を変更してもFalseにならないので、filter()関数が機能しません。
②パラメータ
・第一引数にNoneを指定する場合
listからNoneを削除したい場合、第一引数に抽出条件を設定する必要はありません。
test_list = [0, 1, 2, 3, None, 5, 6, 7, 8, None]
print(list(filter(None, test_list)))
# [1, 2, 3, 5, 6, 7, 8]
None、0が削除されました。0は条件式内ではFalseと同様に扱われるため、Noneとともに削除されます。
③戻り値
filter()関数の戻り値はgeneratorオブジェクトです。そのためlist()関数などを用いて変換する必要があります。
def add_10n(num):
return num + 10
add_list = [10, 20, 22, 25, 30, 40, 50]
gene_list = filter(add_10n, add_list)
added_list = list(filter(add_10n, add_list))
print(gene_list)
# <filter object at 0x000001AE748BBEE0>
print(added_list)
# [10, 20, 22, 25, 30, 40, 50]
先ほど紹介した例のlist()関数を挟んでいないオブジェクトをprintしてみました。そのままでは中身の要素が一目ではわからないgeneratorになっています。
このようなオブジェクトはlist()関数などでわかりやすい形式に変換しましょう。
4.filter()関数を使うときの注意点
ここからはfilter()関数を使うときの注意点を紹介します。
①戻り値がgeneratorオブジェクト
filter()関数の戻り値はgeneratorオブジェクトになります。generatorとは一度に値を取得せず、呼び出されるたびに値を返すオブジェクトです。簡単な例を見てみましょう。
def my_generator():
yield "test1"
yield "test2"
yield "test3"
g = my_generator()
s1 = next(g)
print(s1)
# test1
s2 = next(g)
print(s2)
# test2
s3 = next(g)
print(s3)
# test3
上の例では3つの要素を持ったgeneratorを作成しました。上記例のように都度next()関数を使って要素を取り出します。
普通のリストなどではリストを読み込んだタイミングですべての要素を読み込み、メモリを大量に利用しますが、generatorを利用すると都度要素を取り出せるので、一度に使用するメモリを削減できます。
5.さまざまなデータ型に使用する
filter()関数は多くのオブジェクトに対応しています。ここからは様々なオブジェクトに対する利用例を見てみましょう。
①list(リスト)から要素を抽出する
前の例でも使ってきたリストに使用する例です。
check_list = [10, 20, 22, 25, 30, 40, 50]
print(list(filter(lambda x: x % 10 == 0, check_list)))
# [10, 20, 30, 40, 50]
要素を順番に判定し、Falseの場合要素を削除します。
②dict(辞書)から要素を抽出する
map関数で使用した例のようにfilter()関数は辞書型にも利用できます。
test_dict = {"name": "Taro", "age": 25, "height": 175}
keys = ["name", "age"]
result = dict(filter(lambda dic: dic[0] in keys, test_dict.items()))
print(result)
# {'age': 25, 'name': 'Taro'}
今回の例ではキーを指定し、該当の要素を抽出しました。値のみ抽出したい場合は後述するmap()関数と組み合わせて利用しましょう。
③JSONから要素を抽出する
JSONとはJavaScriptのオブジェクト記法を用いたデータフォーマットです。JavaScriptの記法を用いていると説明しましたが、Pythonをはじめとした多くの言語に対応しています。(※JSONについてはこちらの記事でも解説しています)
今回の例で使用するtest.jsonです。
{
"Ichiro":{
"event": "baseball",
"age": 48
},
"Messi":{
"event": "soccer",
"age": 34
},
"Stephen Curry":{
"event":"basketball",
"age": 33
}
}
こちらをfilter()関数を用いて操作します。
import json
with open("test.json") as js:
jsn = json.load(js)
print(list(filter(lambda x: x == "Ichiro", jsn)))
# ['Ichiro']
js.close()
このように、指定したイチローの要素を取り出せました。
④strから要素を抽出する
文字列に対してもfilter()関数を使用できます。
before_str = "P1yt2ho3n"
after_str = filter(str.isalpha, before_str)
print("".join(after_str))
# Python
最初の文字列にはPythonという文字列に数字が混ざっています。
str.isalphaとはアルファベットならTrue、そうでないならFalseを返すメソッドです。このメソッドで数値のみを取り除くことでPythonという文字列にできました。
6.組み合わせて利用する
ここからは様々なメソッドと組み合わせて使う方法を紹介します。
①lambda式と組み合わせて利用する
lambda式とは無名関数と呼ばれるメソッドです。
lambda 引数 引数 ...:式
lambdaと無名関数の定義を宣言し、引数、式を設定します。このlambda式でfilter()関数の第一引数の抽出条件を設定できます。
check_list = [10, 20, 22, 25, 30, 40, 50]
print(list(filter(lambda x: x % 10 == 0, check_list)))
# [10, 20, 30, 40, 50]
こちらは先ほど基本構文で使用した例をlambda式で書き換えたものです。lamdba式宣言後、引数のxを用いて10の倍数のみを抽出しています。
②map()関数と組み合わせて利用する
他構造のオブジェクトを対象にfilter()関数を利用する場合、map()関数との組み合わせが実用的です。
classmates = [
{"name": "Taro", "japanese": 90, "math": 55 },
{"name": "Jiro", "japanese": 70, "math": 60 },
{"name": "Saburo", "japanese": 55, "math": 90 }
]
jp_over70 = list(map(lambda x: x["name"],(filter(lambda x: x["japanese"] >= 70,classmates))))
print(jp_over70)
# ['Taro', 'Jaro']
math_over60 = list(map(lambda x: x["name"], (filter(lambda x: x["math"] >= 60, classmates))))
print(math_over60)
# ['Jiro', 'Saburo']
result_over135 = list(map(lambda x: x["name"], (filter(lambda x: x["japanese"] + x["math"] >= 135, classmates))))
print(result_over135)
# ['Taro', 'Saburo']
上記の例は各条件に該当するクラスメイトの名前をリストで返すものです。map()関数の第一引数でnameを指定することでnameのみのリストを作成し、第二引数のfilter()関数で要素を抽出しています。
7.リスト内包表記との比較
filter()関数と似た挙動をリスト内包表記で表現できます。先ほどの基本構文で用いた例をリスト内包表記に書き換えると以下のようになります。
check_list = [i for i in [10, 20, 22, 25, 30, 40, 50] if i % 10 == 0]
print(check_list)
# [10, 20, 30, 40, 50]
このようにリスト内包表記で再現ができました。
ここからは挙動の似た2つの記法のメリット、デメリットを紹介します。
①リスト内包表記のメリット、デメリット
メリットを2点紹介します。
・戻り値について考える必要がない
リスト内包表記は関数でないので戻り値について考える必要がありません。関数の場合返り値の型などを考える必要があります。
・map()関数、reduce()関数の再現も可能
リスト内包表記の場合、式の後にif文や三項演算子を利用できるのでmap()関数やreduce()関数の再現も可能です。
デメリットを1点紹介します。
・filter()関数と比べて処理が遅い
リスト内包表記の唯一にして最大のデメリットが関数と比較して処理が遅いことです。
簡単なプログラムで利用する場合は大した差ではありませんが、大規模なソースコード、データを扱う場合はfilter()関数の方が適しています。
②filter()関数のメリット、デメリット
メリットを紹介します。
・さまざまなオブジェクトに同じ記法で対応可能
filter()関数は多くのオブジェクトに同じ記述で抽出できます。オブジェクトごとに方法を模索せずともソースコードの記述が可能です。
デメリットを紹介します。
・戻り値の型がさまざまで初心者には扱いにくい
filter()関数の返り値はgeneratorです。そのため、返り値をそのまま扱えず、list()関数などを挟む必要があります。