意外と知らなかったPythonの便利な書き方集

2021-10-20
2021-10-20

エキスパート Python プログラミング改訂 3 版を読んだので個人的に新たに使えそうと思ったことをまとめます。 プラスで自走プログラマーで学んだことも含めておきます。

2 冊とも業務で Python を使う身としてはめちゃめちゃ勉強になる本だったのでおすすめです。

使用する Python バージョンは本家と同じく Python 3.9 にします。

環境は Docker ではなく pyenv と venv で構築しました。

python -m venv .venv
source .venv/bin/activate

Python 便利な書き方集

自分がよく使う、または使えそうと思ったこと中心です。 有名どころな書き方もちらほらありますが、普段使う時も結構ググったりしてるのでこの機会にまとめます。

リスト内包表記・辞書内包表記

まずはリスト内包表記から紹介します。 C 言語などの for 文に慣れてるとちょっと初見ではとっつきにくいですが、慣れてくるとコードの記述量も減り、綺麗になるので便利です。

どんなもの?

  • iterable な要素を使ってリストを作る書き方
  • 普通に for 文と append などを使って書くよりも綺麗にかける
  • 条件式もかけるので汎用性が高い

使いどころ

  • あるリストの要素を別のリストに変換する時などに使う
  • 具体的には glob でリスト化したパスをある規則で抽出したいときなどに使える

具体例

numbers = list(range(0,10))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 2倍にする
twice = [i * 2 for i in numbers]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# if文バージョン
even = [i for i in numbers if i % 2 == 0]
# [0, 2, 4, 6, 8]

if 文を使う場合は、if の条件式が後ろに着くのがポイント。

続いて辞書内包表記です。

基本的にはリスト内包表記と同じです。違いとしては、キーと値それぞれ処理する点がリスト内包表記と異なります。

具体例は以下です。

even_squares = {num: num**2 for num in range(10) if num % 2 == 0}
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

ラムダ関数

続いてラムダ関数です。無名関数などと呼ばれたりするものです。

どんなもの?

  • lanbda <引数>: <式>の形で記述
  • 通常、関数はdef <関数名>:のように定義して使うが、ラムダ関数は上記のような記述で書ける
  • 簡単な関数かつ、他で再利用されないものならばラムダ関数の方が簡潔にかけて綺麗

使いどころ

  • 後述するmapfilterと組み合わせることでちょっと複雑なデータ変換やデータ抽出を簡潔に書ける
  • 関数を引数にとるもの(組み込み関数のmaxなど)がたまにあるのでそれらに対する引数として渡す

具体例

リストの全ての要素の空白文字を_に変換します。

names = ["taro sato", "yuki tanaka", "hanako yamada"]
replaced_names = list(map(lambda x: x.replace(" ", "_"), names))
# ['taro_sato', 'yuki_tanaka', 'hanako_yamada']

ここで出てきたmapは組み込み関数であり、iterable の各要素に対して関数を適用します。 使い方はmap(<関数>, <iterable>)のように使います。

mapとよく一緒に出てくるものにfilterがあります。 こちらはmapと異なり、適用した関数にマッチしたものだけ返します。

filterを使って同じ苗字だけ抽出してみます。

names = ["taro sato", "yuki tanaka","yuriko sato","hanako yamada"]
satos = list(filter(lambda x: "sato" in x, names))
# ['taro sato', 'yuriko sato']

ちなみに上記の例はリスト内包表記で書くと以下のようになります。

replaced_names = [x.replace(" ", "_") for x in names]

この例だとリスト内包表記と差別化できないので前述した組み込み関数maxの例を紹介します。

maxはその名の通り、iterable な要素から最大のものを返してくれます。(参考:max

maxの引数にはタプルのリストも渡すことができます。例えばタプルの 2 番目の要素だけ評価対象にしたい場合はラムダ関数を使って以下のように書けます。

# (name, age)のリスト
person = [("taro sato", 10), ("yuki tanaka", 32), ("hanako yamada", 24)]
# リスト中でageが最大な要素を抽出したい
# lambda関数でタプルの位置を指定している
max_age = max(person, key=lambda x: x[1])
#('yuki tanaka', 32)

for...else 正常終了時の処理

続いて割と使いどころが多そうだと思った for..else の書き方を紹介します。

どんなもの?

  • for 文が break されずに正常に終了した場合の処理を書ける
  • break したかどうかのフラグのような変数が必要なくなる

使いどころ

  • break した場合としてない場合で違う処理を行いたいときに使う

具体例

あまりいい例ではないかもですが、以下のように使います。 この方法の欠点として、パッと見 if 文と対になってる else に見えてしまうことです。

names = ["taro sato", "yuki tanaka","yuriko sato","hanako yamada"]
#query = "takashi saito"
query = "taro sato"
for idx, name in enumerate(names):
    if name == query:
        print(f"{name} is included in names")
        break
else:
    print("takashi saito is not included in names")

for...else を使わない場合、print 文の出力を分けるには idx と len(names)を比較する if 文などが必要になります。

アプリケーション開発で使えそうな Python の書き方集

機械学習やちょっとしたコードを試す場合と、アプリケーションとして Python を書く場合は、若干気になるところが違います。 アプリケーションの場合は複数人で開発することや長期でメンテナンスすることを想定し、可読性や間違った使い方ができないようにすることも大事です。

ということで、ここではアプリケーション開発に役立ちそうな書き方をまとめます。

キーワードのみ引数

Python では関数を呼ぶ際に引数名を省略して関数を呼び出すことができます。 それはそれで便利なのですが、複雑な関数だったり、関数の意図が分かりづらい場合など、後から見たときに引数の役割が分からなくなることがあります。

そんな時に便利なのがキーワードのみ引数です。

どんなもの?

  • デフォルト引数を持つ関数呼び出しの際に引数名の記述を強制できる
  • キーワード指定のみで使いたい引数の前に*を書く。*以降の引数はキーワード指定でしか使えなくなる

使いどころ

  • bool 値など引数の値だけじゃ中の動作を推測しづらい時に使える
  • 場合によってはチームでルール化するとかもありかも

具体例

例えば以下のような デフォルト引数admin=True ならリストから情報を持ってくる関数があるとします。

def get_personal_info(name, age, gender, admin=True):
    # (name, age, gender)のリスト
    people = [("taro sato", 10, "man"), ("yuki tanaka", 32, "man"), ("yuriko sato", 60, "woman"), ("hanako yamada", 24,"woman")]
    if admin:
        target_info = list(filter(lambda x: name==x[0] and age==x[1] and gender==x[2] in x, people))
        return target_info
    return [()]

この関数は以下のように使えます。

get_personal_info("taro sato", 10, "man", True)

これを見ると先ほど述べた問題点がわかると思います。最後の引数のTrueが何を意味しているのかわかりません。

続いて、キーワード引数を用いて関数を定義してみます。

# キーワードのみの引数を指定
def get_personal_info(name, age, gender, *, admin=True):
    # (name, age, gender)のリスト
    people = [("taro sato", 10, "man"), ("yuki tanaka", 32, "man"), ("yuriko sato", 60, "woman"), ("hanako yamada", 24,"woman")]
    if admin:
        target_info = list(filter(lambda x: name==x[0] and age==x[1] and gender==x[2] in x, people))
        return target_info
    return [()]

この関数のadmin引数にに対して値を渡したい場合、以下のようにしか呼び出せません。

# admin=Falseのようにキーワードを指定しないとエラーになる
get_personal_info("tar sato", 10, "man", admin=False)
# 新たに引数を渡さない場合はOK
get_personal_info("tar sato", 10, "man")
# 以下はTypeErrorになる
# get_personal_info("taro sato", 10, "man", True)

dataclass

アプリ開発をしているとまとまった情報を辞書やクラスにまとめて次の関数に渡していくという処理が多々あります。

そんな時に使えるのこのdataclassです。

どんなもの?

  • dataclassは Python 3.7 から導入された情報をクラスにまとめる際に利用できるデコレータ
  • 通常のクラス定義ではinitに渡す引数が多くなると記述が結構大変ですが、dataclassだと短くかける
  • 特殊メソッドの__repr__()などのメソッドも自動で作ってくれる
  • 定義したクラスを immutable にもできる

使いどころ

  • データ格納用のクラスを使いたい時

具体例

Userクラスを定義することを考えます。 dataclassを使うと以下のようにかけます。

from dataclasses import dataclass
@dataclass
class User:
    id: int
    first_name: str
    last_name: str
    age: int
# 通常のクラスと同様に使える
user =User(id=0, first_name="taro", last_name="tanaka", age=10)

書いてみると分かりますが、圧倒的にdataclassの方が書きやすいです。 普通のクラス定義で書くと上記のUserクラスは」以下のようになります。

class User:
    def __init__(self, id:int, first_name:str, last_name:str, age:int) -> None:
        self.id = id
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

なんか長いですよね。self で引数を代入する部分が書くのめんどくさいです。

関数なども通常のように記述できます。

そして、さらに便利なのが frozen を True にすることで中の値を変更できなくできます。

具体的には以下のように書きます。

@dataclass(frozen=True) # frozen=Trueにすると値を新たにセットできなくなる
class User:
    id: int
    first_name: str
    last_name: str
    age: int
user = User(id=0, first_name="taro", last_name="tanaka", age=10)
user.age = 10 # エラーになる
# FrozenInstanceError: cannot assign to field 'age'