25
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonの達人を目指して~Effective Pythonで学ぶコーディングテクニック~(前編)

Posted at

はじめまして、中山と申します。

業務ではディープラーニングのモデルを主にPythonを使って実装しています。

先日、別のメンバーが 「ロバストPython」の勉強会についての記事 を投稿をしましたが、

それとは別に、Effective Python 第2版という本の勉強会を行っておりました。

この記事では、この本から勉強したPythonのコーディングテクニックのうち、本の前半(1~5章)で取り上げられているものをいくつか紹介します。

理解しやすく、割とすぐに使えそうなものをピックアップしました。

もちろん本を全部読んで理解するに越したことはありませんが、ここで紹介したことを知るだけでも、Pythonの実装スキルがだいぶ上がるのではないかと思います。

Effective Python(第2版) について

JavaやC++のプログラムを書いている方は、Effective JavaEffective C++という本をご存じの方もいらっしゃるかと思いますが、

Effective Pythonも同様の位置づけで、基本的な文法・書き方を理解した人が、さらにレベルアップするためのテクニックが90個紹介されています。

決して入門書ではないので、やや難しい内容もありますが、Pythonを仕事で扱うなら一度は読んでおくべき本ではないでしょうか。

コーディングテクニックの紹介

ここからは、具体的なテクニックを紹介していきます。

見出しの「(項目xx)」は、それぞれのテクニックが紹介されている Effective Python 第2版の項目番号を表しています。

以下で紹介するコードは、Python 3.11.11で動作確認しています。

1. enumerate(項目7)

リストなどをループでまわして処理するとき、次のように、インデックスと値の両方が欲しいということがよくあります。このようなケースでenumerateは有効です。

names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
for i in range(len(names)):
    print(f'{i+1}: {names[i]}')

と書く代わりに、

names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
for i, name in enumerate(names):
    print(f'{i+1}: {name}')

と書くことができます。

ほかのプログラミング言語でもぜひ欲しい機能ですね。

2. zip(項目8)

1回のループ内で複数のリストを並列に扱いたいことがよくあると思います。そんなときにzipが役に立ちます。

たとえば、

names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
scores = [90, 65, 70, 85, 80]  # 各人のテストの点数

という2つのリストがあって、1行に一人の名前と点数を出力したいとき、

for i, name in enumerate(names):
    score = scores[i]
    print(f'{name}: {score}')

と書くよりは、zipを使って、

for name, score in zip(names, scores):
    print(f'{name}: {score}')

と書いたほうが簡単です。

補足
Python 3.10からstrict引数が追加され、これをTrueにすると、要素数が異なるときにValueErrorが発出されます。

上記の例は、

for name, score in zip(names, scores, strict=True):
    print(f'{name}: {score}')

と書くのがよさそうです。

3. catch-allアンパック(項目13)

test_score = ('Alice', 90)

というタプルがあるときに、次のように書くと、タプルの要素を別々の変数に格納することができます。これを アンパック と呼びます。

name, score = test_score  # name = 'Alice', score = 90

catch-allアンパック は、これの発展形で、「一部と残り」という形でアンパックできます。

たとえば、名前のリストを並べ替えて、最初と最後だけ取り出したいときに、catch-allアンパックを使って次のように書くことができます。

name_list = ['Eve', 'Bob', 'Dave', 'Alice', 'Carol']
first, *other, last = sorted(name_list)   # first = 'Alice', other = ['Bob', 'Carol', 'Dave'], last = 'Eve'
print(f'{first} | {last}')   # 出力: Alice | Eve

4. defaultdict(項目17)

辞書を使って「ある文書内の単語の出現頻度を調べる」のようなことをする場合に、

word_count_dict = {}
for word in text:
    if word in word_count_dict:  # "word"のキーがあるかどうか調べる
        word_count_dict[word] += 1  # すでに"word"のキーがあれば、キーの値に1を加算したものを代入する
    else:
        word_count_dict[word] = 1  # "word"のキーがなければ、新しくキーを作って、1を代入する

のように、辞書にキーがすでにあるかないかで処理を分ける必要がでてきます。

このようなとき、defaultdictにしておくと、

word_count_dict = defaultdict(0)  # この0がデフォルト値
for word in text:
    word_count_dict[word] += 1  # "word"のキーがあればキーの値に1を加算、なければデフォルト値0に1を加算して代入

というように、キーが辞書にない場合を考慮せずに書くことができます。

(なお、Pythonには、このようなユースケースで使える Counter という便利なクラスが存在します。)

5. 位置引数とキーワード引数(項目22~25)

位置引数とは、キーワード引数とは

Pythonの関数の引数には、 位置引数キーワード引数 があります。

ほかのプログラミング言語ではなかなか見かけない、かなりユニークな特徴ではないでしょうか。

位置引数は、決められた順番にしたがって引数を渡します。多くのプログラミング言語がこの形ではないかと思います。

一方、キーワード引数は、name=valueのように、名前と値をセットで引数を渡します。

たとえば、次のような関数があったとき、

def func(arg1, arg2, arg3=10) :
    ...

呼び出す側は、引数を位置引数として渡すことも、キーワード引数として渡すこともできます。組み合わせもOKです。

キーワード引数の場合は、引数の順番は問われません。

func(1, 2, 3)  # 位置引数として渡す (arg1=1, arg2=2, arg3=3)
func(arg3=5, arg2=2, arg1=0)  # キーワード引数として渡す (arg1=0, arg2=2, arg3=5)
func(0, arg3=5, arg2=2)  # 位置引数とキーワード引数の組み合わせ (arg1=0, arg2=2, arg3=5)

キーワード引数は辞書型で渡すこともできます。

args = {'arg1':0, 'arg2':2, 'arg3':5}
func(**args)  # ** で辞書型をキーワード引数にアンパックしている

補足
辞書型でキーワード引数を渡す場合、辞書に余計なキーワードを入れることはできません。

args = {'arg1':0, 'arg2':2, 'arg3':5, 'arg4':10}
func(**args)  # 'arg4'が余計なのでエラー

*args, **kwargs

Pythonのコードを読んでいると、func(*args, **kwargs)というスタイルの関数を良く見かけます。

これは、位置引数として渡された引数はargsに、キーワード引数として渡された引数はkwargsに格納されることを意味します。

次のコードでは、func関数が呼ばれたとき、args引数に[1, 2]kwargs引数に{'kwarg1':10, 'kwarg2': 20}が格納されます。

def func(*args, **kwargs):
    print('args = ', args)
    print('kwargs = ', kwargs)
func(1, 2, kwarg1=10, kwarg2=20)

位置専用引数、キーワード専用引数

関数定義で引数に/*を入れることで、引数の渡し方に制限をかけることができます。

  • /よりも前の引数は、位置引数としてのみ受け付ける(位置専用引数
  • *よりも後ろの引数は、キーワード専用引数としてのみ受け付ける(キーワード専用引数

位置専用引数はなかなか使い道が思いつかないですが、キーワード専用引数は、どの引数を何番目に指定するのかを関数の利用者に意識させずに済み、混乱を減らすのに役に立ちそうです。

次の例では、arg1が位置専用引数、arg3がキーワード専用引数です。arg2はどちらでも渡せます。

def func(arg1, /, arg2, *, arg3):
    # 関数の中身は省略

func(1, 2, arg3=3)
func(1, arg2=2, arg3=3)
func(1, 2, 3)  # 'arg3'はキーワード専用引数で、位置引数として渡すことができないため、エラー
func(arg1=1, arg2=2, arg3=3)  # 'arg1'は位置専用引数で、キーワード引数として渡すことができないため、エラー

6. 内包表記(項目27)

こちらもPython独特の書き方で、最初は非常にとっつきづらいと思います(私もそうでした)が、慣れるとコードが短くて読みやすくて便利な文法です。
C#がわかる方は、LINQのクエリ構文を思い出していただけると、イメージがわくと思います。

たとえば、ある数値のリストが与えられて、「そのリストから奇数だけ取り出して2倍したリストを作る」という操作したいとき、愚直に書くと次のようなコードになりますが、

value_list = [32, 17, 23, 6, 45, 10]
new_list = []
for value in value_list:
    if value % 2 != 0:
        new_list.append(value * 2)

内包表記を使うと次のように書けます。

value_list = [32, 17, 23, 6, 45, 10]
new_list = [value * 2 for value in value_list if value % 2 != 0]

セットや辞書でも同じように書けます。
「ある辞書の値が100以上のものを取り出して新しい辞書に格納する」ということをやるには、次のように書きます。

new_dict = {key: value for key, value in org_dict.items() if value >= 100}

ただし、本にも項目28に書かれていますが、過度な多用は禁物です。
内包表記の中にさらに内包表記を入れ子にすると、一気にコードが読みにくくなります。

追加テクニック
ちょっとトリッキーで読みづらい書き方ですが、内包表記でこんなコードを書けます。

names = ['Alice', 'Bob', 'Carol']
combinations = [(x, y) for x in names for y in names if x != y]
print(combinations)

上記のコードの実行結果は以下です。

[('Alice', 'Bob'), ('Alice', 'Carol'), ('Bob', 'Alice'), ('Bob', 'Carol'), ('Carol', 'Alice'), ('Carol', 'Bob')]

7. ジェネレータ(項目30)

たとえば、ある数値データをファイルから読んで返すとき、次のような実装が考えられます。

def load_value(path):
    value_list = []
    with open(path, 'r') as f:
        for line in f:
            value_list.append(int(f))
    return value_list

# load_value関数を使って1個ずつ値を処理する
for value in load_value(file_path):
    # なんらかの処理

この実装では、読んだデータを全部リストに格納して返します。
データの個数が数十個や数百個ならあまり気にならないかもしれませんが、数億とか数十億とかいう単位になると、それがすべてメモリに載らないかもしれません。

そんなときに、ジェネレータを使ってデータを1個ずつ読んで返すことで、メモリ効率を上げることができます。

C#にも yield return がありますね。

def load_value_gen(path):
    value_list = []
    with open(path, 'r') as f:
        for line in f:
            yield int(line)

呼び出し方法は load_value のときと変わりません。

# load_value_gen関数を使って1個ずつ値を処理する
for value in load_value_gen(file_path):
    # なんらかの処理

8. call (項目38)

特殊メソッド__call__を定義すると、インスタンスを呼び出し可能にすることができます。

ディープラーニング関係のPythonのコードでは、forward処理(データをモデルに渡して出力を得るところ)で割と良く見かけるやり方です。

class Counter():
    def __init__(self):
        self.count = 0
    
    def __call__(self):
        self.count += 1

    def __repr__(self):
        return str(self.count)

counter = Counter()
counter()   # インスタンスを関数のように呼び出し
counter()
counter()
print(counter)  # 出力: 3

まとめ

ここでは、Effective Python 第2版の前半(1~5章)で触れられているテクニックを8個紹介しました。

後半(6~10章)については、改めて別の記事で紹介したいと思います。

25
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?