Python operator 使ったことないけど試験には出る 使い所は?

概要

operatorモジュールは標準的な演算(算術演算、比較演算、論理演算など)を関数の形で提供するモジュールである。「標準的な機能を関数の形で提供する」というものなので、あまりメリットを感じられるモジュールではないのですが、「メリットを感じられない」という部分から「じゃあ、いつ使うのがベストなんだろう?」と試行錯誤をしていたところ、「コードを書く上で考えるべきことは何だろう?」ということを考えさせられた実は奥の深いモジュールでした。

そんなパッと見では使い所が感じられない地味なモジュールoperatorをもとに、「なぜ標準的な機能を関数の形で提供したのか?メリットは?」という部分が伝えられたらなと思います。

かと言って「operatorモジュールは便利だ!使うべきだ!」と極端な発想にはならないほうが良いという部分もあるため、記事の後半では使わない方がいいというケースも紹介します。

地味なモジュールoperator

今回はoperatorモジュールの中でもitemgetter, attrgetterメソッドを紹介します。

operatorモジュールにはadd, sub, eqメソッドなどもありますが、これは標準的な機能を関数にしただけです。メソッドの機能を学ぶよりも、「使うメリット」を中心に紹介したいため、メリットを伝えやすいitemgetter, attrgetterに絞りました。

※map()、reduce()、sorted()などと組み合わせて使う際にoperatorモジュールは便利だと思いました。

itemgetterについて

まずは、operatorモジュールの関数を使った場合と使わなかった場合を比較してみます。

itemgetterを使わない場合

import operator
li = [1, 2, 3, 4, 5]

# 上記のリストから2を取り出すときは

li[1]   2

itemgetterを使った場合

import operator


li = [1, 2, 3, 4, 5]
print(operator.itemgetter(1)(li))

複数の要素を取り出す場合は、itemgetterに複数の引数を渡す

li = [1, 2, 3, 4, 5]
print(operator.itemgetter(1, 3)(li))

itemgetterを使わずに書くと

li = [1, 2, 3, 4, 5]
print(li[1], li[3])

itemgetterは、リストの要素を取り出すだけでなく、辞書の値を取り出すこともできます。 複数の要素を取り出す場合は、itemgetterに複数の引数を渡す

dic = {'a': 1, 'b': 2, 'c': 3}
print(operator.itemgetter('a', 'c')(dic))

itemgetterを使わずに書くと

dic = {'a': 1, 'b': 2, 'c': 3}
print(dic['a'], dic['c'])

あれ?メリット感じられない?メリットはなんだろう?

上記の場合、あまりにも単純すぎてメリットを感じられないので、もう少し複雑なものにしてみます。

例えばデータセットが複数あって、それぞれのデータセットから特定のインデックスの値を取得したい場合itemgetterを使うことで、読みやすいコードが書けます。


import operator
sample_1 = [1, 2, 3, 4, 5]
sample_2 = [6, 7, 8, 9, 10]
sample_3 = [11, 12, 13, 14, 15] 

# 例えばデータセットが複数あって、それぞれのデータセットから特定のインデックスの値を取得したい場合

# itemgetterを使うと複数のインデックスから値を取得できる
first_and_last = operator.itemgetter(0, -1)
third_and_forth = operator.itemgetter(2, 3)
midian = operator.itemgetter(2)


for sample in [sample_1, sample_2, sample_3]:
    print(first_and_last(sample))
    print(third_and_forth(sample))
    print(midian(sample))

使わなかった場合、最後のループ部分が以下のようになります。

for sample in [sample_1, sample_2, sample_3]:
    print(sample[0], sample[-1])
    print(sample[2], sample[3])
    print(sample[2])

上記よりかはoperator.itemgetterを使うことで幾分か読みやすくなっている。

相性の良い他の関数

ソートや他の関数と組み合わせる場合 operator.itemgetter は、sorted や min、max などの関数と組み合わせると便利です。
たとえば、リスト内の辞書を特定のキーでソートする場合などです。

from operator import itemgetter

students = [
    {'name': 'Alice', 'age': 25},
    {'name': 'Bob', 'age': 20},
    {'name': 'Charlie', 'age': 23},
]

 'age'キーを基準にソート
sorted_students = sorted(students, key=itemgetter('age'))
print(sorted_students)
# 出力: [{'name': 'Bob', 'age': 20}, {'name': 'Charlie', 'age': 23}, {'name': 'Alice', 'age': 25}]

ここでは、itemgetter を使うことで、ソートの基準となるキーを簡単に指定できるため、コードがより読みやすくなります。

簡潔さ: 複数のインデックスやキーを一度に取得できる。
柔軟性: sorted などの関数と組み合わせることで、コードの可読性が向上する。
operator.itemgetter は、特にデータのフィルタリングやソートといった操作で便利に使えますが、単純なインデックスアクセスの場合は通常のインデックス操作で十分です

operatorとlistのsortメソッドを組み合わせる

data = [('A', 1), ('B', 3), ('C', 2)]
data.sort(key=operator.itemgetter(1))
print(data)   [('A', 1), ('C', 2), ('B', 3)]

もう少し複雑にする、リストの中に(名前、年齢、身長)のタプルが入っているとする

data = [('A', 1, 180), ('B', 3, 170), ('C', 2, 160)]
# 年齢でソート
print(sorted(data, key=operator.itemgetter(1)))   [('A', 1, 180), ('C', 2, 160), ('B', 3, 170)]
# 身長でソート
print(sorted(data, key=operator.itemgetter(2)))   [('C', 2, 160), ('B', 3, 170), ('A', 1, 180)]
# 名前でソート
print(sorted(data, key=operator.itemgetter(0)))   [('A', 1, 180), ('B', 3, 170), ('C', 2, 160)]

上記を解説すると、例えば、operator.itemgetter(1)は、リストの要素のインデックス1の要素を取り出す関数である。 sorted(data, key=operator.itemgetter(1)) は、dataリストの各要素に対して、 operator.itemgetter(1)で指定されたインデックスの要素を取り出し、それをソートキーとしてソートする。 したがって、年齢でソートされる。

名前でソートし、年齢でソートする print(sorted(data, key=operator.itemgetter(0, 1))) [('A', 1, 180), ('B', 3, 170), ('C', 2, 160)]

上記を解説すると、operator.itemgetter(0, 1)は、リストの要素のインデックス0と1の要素を取り出す関数である。 sorted(data, key=operator.itemgetter(0, 1)) は、dataリストの各要素に対して、 operator.itemgetter(0, 1)で指定されたインデックスの要素を取り出し、それをソートキーとしてソートする。 したがって、名前でソートし、名前が同じ場合は年齢でソートされる。

operator.attrgetterについて

operator.attrgetter は、オブジェクトの属性にアクセスするための関数です。
主に、オブジェクトのリストやタプルを特定の属性でソートしたり、特定の属性値を取得したりする場合に使われる。

from operator import attrgetter

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

students = [
    Student('Alice', 25),
    Student('Bob', 20),
    Student('Charlie', 23),
]

# 'age'属性を基準にソート
sorted_students = sorted(students, key=attrgetter('age'))
for student in sorted_students:
    print(student.name, student.age)
 出力: 
 Bob 20
 Charlie 23
 Alice 25

例えばimport datetimeとoperator.attrgetterを使って、datetimeオブジェクトのリストを日付でソートすることができる。

import datetime
from operator import attrgetter

dates = [
    datetime.datetime(2020, 1, 1),
    datetime.datetime(2020, 1, 3),
    datetime.datetime(2020, 1, 2),
]

# 日付でソート
sorted_dates = sorted(dates, key=attrgetter('day'))
print(sorted_dates)
>>>[datetime.datetime(2020, 1, 1, 0, 0), datetime.datetime(2020, 1, 2, 0, 0), datetime.datetime(2020, 1, 3, 0, 0)]


# 日付と年でソートする
dates = [
    datetime.datetime(2023, 1, 1),
    datetime.datetime(2020, 8, 3),
    datetime.datetime(2025, 5, 2),
]

sorted_dates = sorted(dates, key=attrgetter('year', 'day'))
print(sorted_dates)

>>>[datetime.datetime(2020, 8, 3, 0, 0), datetime.datetime(2023, 1, 1, 0, 0), datetime.datetime(2025, 5, 2, 0, 0)]

並べ替えの順序であるが、例えば同じ年の場合は日付でソートするといった流れで並び返される。
今回の場合だと、2020年の8月3日、2023年の1月1日、2025年の5月2日となる。
まずは年でソートし、同じ年の場合は日付でソートするといった流れで並び返される。
今回は同じ年がないので、年でソートされた結果が返される。

注意 operator モジュールを無理に使わなくても良い

実際、多くの場面でoperator を使わなくてもコードを書くことができます。
以下に、operator を使わずに同じ処理を実現する方法を示します。

1. リストのソート(attrgetter を使わない場合)

datetime オブジェクトのリストを日付や年でソートする場合、sorted 関数に直接 lambda 関数を使うことで、operator.attrgetter を使わずに同じことができます。

import datetime

dates = [
    datetime.datetime(2020, 1, 1),
    datetime.datetime(2020, 1, 3),
    datetime.datetime(2020, 1, 2),
]

# 日付でソート (year, month, day の順)
sorted_dates = sorted(dates, key=lambda x: (x.year, x.month, x.day))
print(sorted_dates)
# 出力: [datetime.datetime(2020, 1, 1), datetime.datetime(2020, 1, 2), datetime.datetime(2020, 1, 3)]

2. 複数の基準でのソート

複数の基準(例えば、年と月)でソートする場合も同様に、lambda 関数を使えば十分です。

dates = [
    datetime.datetime(2023, 1, 1),
    datetime.datetime(2020, 8, 3),
    datetime.datetime(2025, 5, 2),
]

# 年と月でソート
sorted_dates = sorted(dates, key=lambda x: (x.year, x.month))
print(sorted_dates)
# 出力: [datetime.datetime(2020, 8, 3), datetime.datetime(2023, 1, 1), datetime.datetime(2025, 5, 2)]

なぜoperatorの紹介記事でoperatorを使わなくても書けることを紹介したか?

operator モジュールは特定の場面で便利ですが、実際には lambda 関数を使った書き方が好まれる場合もある。
特に、コードの可読性や他の人が理解しやすいコードを書くことを重視する場合、lambda を使う方が良い選択肢になると思われます。

私個人の話ですが、8年使ってるけど使ったことない・・・使ったことないというより、使わなくても代替方法があるので、使う機会がなかった。
もし、共同開発者がPythonベテラン者でPython育成団体の人とかで、Pythonマスターな方とかだったら使うかも。
そうでなければあまり使わないかもしれない。おそらく、operatorより他言語でも馴染みのあるラムダ式で書いた方が、可読性が良い場合があるかもしれない。
もし、共同開発者がPython以外にもRubyやC#,Javaなど多数の言語を扱っている人であれば、Pythonにしかないライブラリーで書くと、
相手はネットで調べる手間が出てくるかもしれないなと思った。

業務ではいろんな経歴の人と一緒に仕事をする。コードを書く上で大切にしているのは、自分以外の人でも読みやすいかということ。

それは今、顔を合わせている人だけではない。今、自分は開発しているかもしれないが、1ヶ月2ヶ月後、何かの事情で開発を離れるかもしれない。
それが明日かもしれない。そうなったときに、私が開発する予定だったところは代わりの人が担当することになるだろう。
その人に向けて書くことを意識してます。未来の誰かさんに向けてコードを書く(業務では

個人開発でも多少なりとも意識はする。3ヶ月後の自分が、自分のコードを読めるとは限らないので笑未来の自分にメッセージを残すつもりで書く!

まとめ

operator モジュールは一見地味で、「使わなくてもいいのでは?」と思われがちですが、複雑なデータ操作やソート、関数の再利用を考えると、適切な場面で強力なツールとなります。ですが、すべてのケースで無理に使う必要はなく、lambda とのバランスを見ながら、場面に応じて選択するのがベストです

この記事が気に入ったらサポートをしてみませんか?