12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Effective Python 第2版 を自分なりにまとめてみる part1

Last updated at Posted at 2022-06-07

はじめに

こちらの書籍のまとめになります

Effective Python 第2版 ――Pythonプログラムを改良する90項目 (Brett Slatkin 著、黒川 利明 訳、石本 敦夫 技術監修)

  • 全てのパートをまとめているわけではありません

  • 個人的に難しくて理解できていなかったり腑に落ちていない箇所は省いています

  • もしくは新たな気づきは特にないなと感じたところも省略しています

  • コードに関しては書籍のものを丸々掲載するでなく、改変しています(その過程も個人的に有意義な時間でした)

  • そのような理由からこのブログでは多くの部分を削ってしまっています。オリジナルの書籍はかなり勉強になるなと思いました。興味ある人は是非読んでください。

  • このページでは本書の1〜3章をまとめています。他の章はこちらを参照ください

第一章 Pythonic思考

Zen of Python

import this
[out]
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

アンパック代入 enumerate

  • アンパック代入を積極的に使う
  • indexを取りたいときはrangeよりenumerateを使う。その方が直感的
    `python

bad

animals = (('mouse', 'S'), ('dog', 'M'), ('human', 'L'))
for i in range(len(animals)):
key = animals[i][0]
value = animals[i][1]
print(f'{i} {key}: {value}')

print()

good

animals = (('mouse', 'S'), ('dog', 'M'), ('human', 'L'))
for i, (key, value) in enumerate(animals):
print(f'{i} {key}: {value}')

print()

第二引数でカウントの開始を制御

animals = (('mouse', 'S'), ('dog', 'M'), ('human', 'L'))
for i, (key, value) in enumerate(animals, 10):
print(f'{i} {key}: {value}')
`

[out]
0 mouse: S
1 dog: M
2 human: L

0 mouse: S
1 dog: M
2 human: L

10 mouse: S
11 dog: M
12 human: L

zip

  • zipの挙動に注意
    `python

異なるな長さのイテレータの場合、エラーを出さず出力を最短で停止する

colors = ['red', 'blue', 'yellow', 'black']
names = ['taro', 'jiro', 'saburo']

for c, n in zip(colors, names):
print(c, n)

print()

停止させたくない場合はこうする

import itertools

for c, n in itertools.zip_longest(colors, names):
print(c, n)
`

[out]
red taro
blue jiro
yellow saburo

red taro
blue jiro
yellow saburo
black None

for/whileの後のelse

  • Pythonではfor文、またはwhile文が最後まで正常に終了した場合にelseのブロックが実行される特別な構文があるが、誤解を生みやすいので使用しない
for i in [1, 2, 3]:
    pass
else:
    print('pattern1')

print()

i = 0
while i < 3:
    i += 1
else:
    print('pattern2')

print()

# これは実行されない
for i in [1, 2, 3]:
    if i == 2:
        break
else:
    print('pattern3')
[out]
pattern1

pattern2

代入式

  • 代入式は3.8から導入された、walrus(セイウチ)演算子を使う
stock = {
    'red': 1,
    'blud': 2,
    'green': 3
}

# これだと一見対象のcountがどこでどう使われているか明白でない
count_r = stock.get('red', 0)
count_b = stock.get('blue', 0)
count_g = stock.get('green', 0)
if count_r:
    pass
elif count_b >= 2:
    pass
elif count_g >= 1:
    pass
else:
    pass


# こう書くことでスッキリするし、対象のcountがどこで使われているか明白になる
if count := stock.get('red', 0):
    pass
elif (count := stock.get('blue', 0)) >= 2:
    pass
elif (count := stock.get('green', 0)) >= 1:
    pass
else:
    pass
  • Pythonは他言語のようなdo/whileといった繰り返しの後判定がなく前判定しかない、冗長になりがち
  • walrus演算子をうまく使えば回避できる

before
python stock = pickup() # 条件判定の対象 while stock: # 何かしらの処理 stock = pickup() # 条件判定の対象を更新

after
python while stock := pickup(): # 条件判定の対象を更新 # 何かしらの処理

第二章 リストと辞書

リストのシーケンス

  • 代入するスライスの長さは代入先の長さと一致しなくてもいい
a = list(range(10))
print(a)

a[2:8] = ['x', 'x']
print(a)
[out]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 'x', 'x', 8, 9]
  • スライスして得られた結果は値渡し
  • スライスせずそのまま代入した場合、参照渡し
a = list(range(10))

b = a[:]
print(a == b, a is b)
b[0] = 100
print(a)

c = a
print(a == c, a is c)
c[0] = 100
print(a)
[out]
True False
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
True True
[100, 1, 2, 3, 4, 5, 6, 7, 8, 9]

catch-allアンパック

  • シンプルかつ美しい
a = list(range(10))

# シーケンスだと冗長
first = a[0]
second = a[1]
others = a[2:]

# こうする
first, second, *others = a
print(first, second, others)

# アスタリスクの位置で色々コントロールできる
first, *others, last = a
print(first, others, last)
[out]
0 1 [2, 3, 4, 5, 6, 7, 8, 9]
0 [1, 2, 3, 4, 5, 6, 7, 8] 9

複雑なソート

  • keyを使う
class Student:
    def __init__(self, name, height):
        self.name = name
        self.height = height

    def __repr__(self):
        return f'Name:{self.name} Height:{self.height}'


students = [
    Student('D', 180),
    Student('C', 170),
    Student('A', 180),
    Student('B', 160),
]

print(students)

students.sort(key=lambda x: x.name)
print(students)

students.sort(key=lambda x: x.name, reverse=True)
print(students)

students.sort(key=lambda x: x.height)
print(students)

students.sort(key=lambda x: -x.height)
print(students)
[out]
[Name:D Height:180, Name:C Height:170, Name:A Height:180, Name:B Height:160]
[Name:A Height:180, Name:B Height:160, Name:C Height:170, Name:D Height:180]
[Name:D Height:180, Name:C Height:170, Name:B Height:160, Name:A Height:180]
[Name:B Height:160, Name:C Height:170, Name:D Height:180, Name:A Height:180]
[Name:D Height:180, Name:A Height:180, Name:C Height:170, Name:B Height:160]
  • 脇道にそれるがPythonのtupleには各位置の要素について実装されている特殊メソッドで順に比較できるという性質がある
a = (4, 177)
b = (4, 174)
c = (1, 168)

# 先頭が違うのでこうなる
print(b > c)   # True

# 先頭が同じ場合次の項で比較する
print(b > a)   # False
  • この性質を利用することで以下のように複数キーでソートできる
students = [
    ('D', 180),
    ('C', 170),
    ('A', 180),
    ('B', 160),
]
students.sort(key=lambda x: (-x[1], x[0]))
print(students)

students = [
    Student('D', 180),
    Student('C', 170),
    Student('A', 180),
    Student('B', 160),
]
students.sort(key=lambda x: (-x.height, x.name))
print(students)
[out]
[('A', 180), ('D', 180), ('C', 170), ('B', 160)]
[Name:A Height:180, Name:D Height:180, Name:C Height:170, Name:B Height:160]

辞書の欠損キーの処理にはgetを使う

counters = {
    'A': 3,
    'B': 2,
    'C': 0
}

# 良くない例
if 'D' in counters:
    counters['D'] += 1
else:
    counters['D'] = 1

# 良い例、なかったときのアクセス数も少なくて済むし余計なインデントもいらない
counters['E'] = counters.get('E', 0) + 1

missing

  • 特殊メソッド__missing__を使うことでkeyが存在しない場合の挙動を制御することができる
class MyDict(dict):
    """
    dict型を継承
    keyが存在しない場合の挙動を新たに定義
    """
    def __missing__(self, key):
        return key


dic = dict()
my_dic = MyDict()

print(dic['C'])      # KeyError
print(my_dic['C'])   # C

第三章 関数

Noneでなく例外を返す

  • エラーハンドリンングでいつも適当に書きがちな自分には「関数が返した先で処理ではなく、関数からエラーを返す」という発想は目から鱗でした
  • 以下は良くない例
    `python
    def func(a, b):
    try:
    return a / b
    except ZeroDivisionError:
    return None

result = func(0, 1)

確かにこう書けば意図してることはできるかもだけど、、、

if result is None:
print('Error') # 出力されない

こう書いちゃって間違うかもしれない

if not result:
print('Error') # 出力されてしまう
`

  • 上よりはマシだけど面倒だし、間違いやすい例
def func(a, b):
    try:
        return True, a / b
    except ZeroDivisionError:
        return False, None


# 関数の仕様が良く分からん人だとこうやっちゃうかも
_, result = func(0, 1)
  • なので関数はシンプルにエラーを返して返した先でハンドリングする
  • docstringを丁寧にかく、あらかじめ予想されるエラーについてしっかりかく
def func(a, b):
    """Divides a by b.

    Raises:
        ValueError: When the inputs cannot be devided.
    """
    try:
        return a / b
    except ZeroDivisionError:
        raise ValueError


x, y = 0, 0
try:
    result = func(x, y)
except ValueError:
    print('Invalid inputs')
else:
    print(result)

x, y = 0, 2
try:
    result = func(x, y)
except ValueError:
    print('Invalid inputs')
else:
    print(result)
[out]
Invalid inputs
0.0

クロージャーと変数スコープ

  • 以下は外側のスコープから値を参照している例
def func():
    flag = False
    def func2():
        print(flag)
    func2()
    return flag

print(func())
[out]
False
False
  • 参照と違い代入の場合、スコープ内で新たに変数flagが定義されたものとして処理される
def func():
    flag = False
    def func2():
        flag = True
        print(flag)
    func2()
    return flag

print(func())
[out]
True
False

可変長位置引数で見た目をスッキリ

  • 引数にいちいち[]を指定せずともOK
def print_ids(ids):
    print(' '.join(map(str, ids)))

print_ids([1, 2, 3])  # -> 1 2 3
print_ids([])         # -> (空文字)
print_ids()           # TypeError


def print_ids(*ids):
    print(' '.join(map(str, ids)))

print_ids(1, 2, 3)    # -> 1 2 3
print_ids()           # -> (空文字)

デフォルト引数の評価タイミング

  • デフォルト引数の値の決定モジュール読み込み時に行われる関数定義の際に1度だけ行われそこで値が決定してしまう
  • これは予期せぬエラーの元になる
  • 下記の場合、default={}が最初に定義されるが、bad1もbad2もその同じオブジェクトを代入しただけなので以下のようになってしまう
import json


def load_json(path, default={}):
    try:
        return json.loads(path)
    except:
        return default


bad1 = load_json('')
bad1['taro'] = 'tokyo'

bad2 = load_json('')
bad2['sport'] = 'baseball'

print(bad1)
print(bad2)
print(bad1 is bad2)
[out]
{'taro': 'tokyo', 'sport': 'baseball'}
{'taro': 'tokyo', 'sport': 'baseball'}
True
  • このような予期せぬエラーを防ぐためにデフォルト引数に何かしら存在しないような値を使いたいときはNoneを使用するのがベスト
def load_json(path, default=None):
    try:
        return json.loads(path)
    except AttributeError:
        if default is None:
            default = {}
        return default

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

  • 以下の例のように位置引数とキーワード引数が混ざってくると関数の呼び出し元の想定と異なる使い方をされエラーの元になりやすい
def my_func(name1, name2, is_man=False, jodoshi='です', period=''):
    print(f'{name1}{name2}{"男性" if is_man else "女性"}{jodoshi}{period}')


# 色んなパターンで呼び出せてしまう
my_func('山田', '花子')                          # 山田花子、女性です。
my_func(name2='太郎', is_man=True, name1='近藤') # 近藤太郎、男性です。
my_func('新田', name2='美子')                    # 新田美子、女性です。
my_func('鈴木', '三郎', True, '', '!')         # 鈴木三郎、男性だ!
  • キーワード引数で呼び出して欲しい(呼び出し先も引数の意味を考えて使って欲しい)のに位置引数で使われるとミスも起こりやすい。例えば以下の例

my_func('山田', '花子', False, '。', 'でございます') -> '山田花子、女性。でございます'

  • そこでキーワード専用引数を設定することで位置引数での使用を制限する
def my_func(name1, name2, is_man=False, *, jodoshi='です', period=''):
    """
    *より後ろはキーワード専用引数
    """
    print(f'{name1}{name2}{"男性" if is_man else "女性"}{jodoshi}{period}')


my_func('山田', '花子')
my_func('山田', '花子', False, period='', jodoshi='でございます')

# これはエラー
my_func('山田', '花子', False, 'でございます', '')
[out]
山田花子、女性です。
山田花子、女性でございます。
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [25], in <cell line: 12>()
      9 my_func('山田', '花子', False, period='。', jodoshi='でございます')
     11 # これはエラー
---> 12 my_func('山田', '花子', False, 'でございます', '。')

TypeError: my_func() takes from 2 to 3 positional arguments but 5 were given
  • またPython3.8からキーワード専用引数に加え、新たに位置専用引数も加った
  • 呼び出し元の都合で引数名が変更(name2->first_nameなど)になったときに、キーワード引数としてname2を使っているとエラーになってしまう
  • 呼び出し元で引数を明示的に使用することを想定していない場合、上記のようなエラーを防ぐために位置専用引数を使うのも手かも
def my_func(name1, name2, /, is_man=False, *, jodoshi='です', period=''):
    """
    /より前は位置専用引数
    *より後ろはキーワード専用引数
    """
    print(f'{name1}{name2}{"男性" if is_man else "女性"}{jodoshi}{period}')


my_func('佐々木', '二郎', is_man=True)

# これはエラー
my_func(name1='佐々木', name2='二郎', is_man=True)
[out]
佐々木二郎、男性です。
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [28], in <cell line: 10>()
      6     print(f'{name1}{name2}、{"男性" if is_man else "女性"}{jodoshi}{period}')
      9 my_func('佐々木', '二郎', is_man=True)
---> 10 my_func(name1='佐々木', name2='二郎', is_man=True)

TypeError: my_func() got some positional-only arguments passed as keyword arguments: 'name1, name2'
  • 尚、/*の間にある引数は位置でもキーワードでも渡せる
# どちらでもOK
my_func('坂本', '龍馬', jodoshi='ちや', is_man=True)
my_func('坂本', '龍馬', True, jodoshi='ちや')
[out]
坂本龍馬、男性ちや。
坂本龍馬、男性ちや。
12
14
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
12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?