[メモ]きれいなPythonプログラミング - クリーンなコードを書くための最適な方法

· ·

本紹介 🔗

きれいなPythonプログラミング ~クリーンなコードを書くための最適な方法」を読む

概要・対象読者 🔗

Python入門は終えた程度の方を対象とする、プログラムに関する本というよりはお作法とか、Pythonについて会話するときには基本知っておいてねというような内容

1章 エラーの取り扱いと質問の仕方 🔗

  • エラーメッセージの読み方とLinterについて
  • Pyflakes の紹介
  • その他様々なコミュニティ等で質問する際のお作法について

2章 環境設定とコマンドライン 🔗

  • Pythonのパス表記はOS等の環境が変わっても動作するように pathlib モジュールを利用すること
1
2
from pathlib import Path
Path('Documents')
  • Path.home() : ホームディレクトリ
  • Path.cwd()
  • os.chdir() : change directory。作業ディレクトリを変更してから Path.cwd() を行うとcurrent working directoryが変更される
  • subprocess : python内でシェルコマンド実行
  • LinuxでのPath追加 : PATH=/newFolder:$PATH 、永続的な追加は export PATH=/newFolder:$PATH 。毎回調べているような気がする

3章 Blackを使ってコードフォーマットを整える 🔗

  • Blackモジュール
  • 空行の挿入 : 関数は空白2つ、クラスも空行2つ、クラス内のメソッドは空行1つ。空行ルール知らなかったな…
  • moduleの読み込み順 : 標準ライブラリ > サードパーティモジュール > ローカルモジュール

4章 わかりやすいネーミング 🔗

  • クラス : PascalCase
  • 定数変数 : SNAKE_CASE
  • 関数名、メソッド名、変数 : snake_case
  • booleanを表す変数は ishas を使う

5章 怪しいコード臭 🔗

  • logging を利用したデバッグ
1
2
3
4
import logging

logging.basicConfig(filename-'log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(message)s')
logging.debug('This is a log message.')

6章 パイソニックなコードを書こう 🔗

  • The Zen of Python
    • 醜いコードよりも美しいコードを
    • 暗示よりも明示を
    • 単純は複雑よりよし。複雑は難解よりよし。
    • ネストは浅く
    • 適度な隙間を
    • 読みやすさは善
    • 特殊な状況は特例の理由にならない。しかし実用性は純粋さに勝る。
    • エラーは決して見過ごしてはならない - 意図的に隠されていない限りは
    • 曖昧な部分に出くわしても、憶測で片付けてはならない
    • 誰が見てもわかる書き方は必ずあるはず。それが唯一なら尚よし。
    • 「オランダ人でないとわからないかもしれないけどね。」
    • 何もないより、なにかある方がマシだ。しかし、慌ててなにかするより何もしない方がマシな場合は多い
    • 実装の説明が難しいのは悪いアイデア。実装の説明が簡単なら良いアイデアかもしれない
    • 名前空間は素晴らしい。積極的に使うべし。
  • 辞書型にキー指定でデータを取得する場合にKeyErrorを発生させないためにgetを使いデフォルト値を一緒に設定する。またキーが存在しない場合にデフォルト値を設定したい場合には numberOfPets.setdefault('cats', 0) としてセットする。
1
2
numberOfPets = {'dogs': 2}
print('I have', numberOfPets.get('cats', 0), 'cats.')
  • collections.defaultdict クラスを利用することでデフォルト値を設定した辞書を作成できる
1
2
3
4
import collections

scores = collections.defaultdict(int)
scores['A1'] += 1 # エラーにならない
  • if-elif-elseの代わりに辞書を使った構文例。seasonのキーが存在しない場合、デフォルト値である Personal day off が返る。
1
2
3
4
holiday = {'Winter': 'New Year\'s Day',
'Spring': 'May Day',
'Summer': 'Juneteenth',
'Fall': 'Halloween'}.get(season, 'Personal day off')
  • 三項演算子 : message = valueIfTrue if condition else valueIfFalse
  • 値をまとめて代入 : spam = eggs = bacon = 'string'
  • 値をまとめて比較 : spam == eggs == bacon == 'string'
  • 変数の値が複数の値のどれかと等しいか調べる : spam in ('cat', 'dog', 'moose')

7章 プログラミングの専門用語 🔗

  • 用語集

8章 Pythonのよくある落とし穴 🔗

  • リストのループ中に元リストへの値追加や削除は絶対にしないこと
  • 参照コピーではなくオブジェクトコピーは copyモジュールを使う。ただしリストの中にリストが含まれている場合は含まれている内部のリストは参照コピーになるため copy.deepcopy() を利用すること。プログラムの処理速度は低下するが毎回 copy.deepcopy() を使っておけば安心。
1
2
3
4
import copy

bacon = [2,4,8,16]
ham = copy.copy(bacon)
  • 関数のデフォルト引数に可変値を使用しない。リストや辞書をデフォルト引数として使用する必要がある場合、デフォルト引数をNoneに設定するべき。可変オブジェクトは関数が呼ばれるたびに作成されるのではなく、def文実行時に一度作成されるだけ。
1
2
3
4
5
def addIngredient(ingredient, sandwich=None):
    if sandwich is None:
        sandwich = ['bread', 'bread']
    sandwich.insert(1, ingredient)
    return sandwich
  • 文字列連結で文字列は作らず、リストを作成してから結合する方が処理が早い。
1
2
3
4
5
finalString = []
for i in range(100000):
    finalString.append('spam ')

finalString = ''.join(finalString)
  • sort() はASCIIコード順でのソートのためアルファベット順のソートにならない。アルファベット順にソートしたい場合は sort(key=str.lower) を利用すること
  • 浮動小数点は厳密ではないが、正確な精度が必要な場合はdecimalモジュールを利用する。decimalモジュールは有効桁数を変更することで精度レベルを調整できる(デフォルト28)
1
2
3
4
import decimal

d = decimal.Decimal('0.1')
decimal.getcontext().prec = 2
  • 比較演算子の != を連鎖させないこと
  • 単要素のタプルでは必ず最後にカンマを入れること

9章 Pythonの要注意コード 🔗

  • all() はリストなどのシーケンス値を受け取り、そのシーケンス内のすべての値が真であればTrueを返す。リスト内包表記と一緒に利用したりする。
1
2
eggs = [43, 44, 45, 46]
all([i > 42 for i in eggs])

10章 よい関数の書き方 🔗

  • リストを個々の要素でprint()へ渡すには*構文を利用する
1
2
args = ['cat', 'dog', 'moose']
print(*args)
  • 辞書などのマッピング型のデータを個別のキーワード引数として渡す場合は **構文を利用する
1
2
kwargsForPrint = {'sep': '-'}
print('cat', 'dog', 'moose', **kwargsForPrint)
  • 可変長引数の関数例とキーワード引数の関数例
1
2
3
4
5
6
7
8
9
def product(*args):
    result = 1
    for num in args:
        result *= num
    return result

def formMolecules(**kwargs):
    if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and kwargs['oxygen'] = 1:
        return 'water'
  • * と ** を利用したラッパー関数作成。以下はprint関数をラップ
1
2
3
4
5
def printLower(*args, **kwargs):
    args = list(args)
    for i, value in enumerate(args):
        args[i] = str(value).lower()
    return print(*args, **kwargs)
  • ラムダ関数。1つ目の例は単純にlambda関数を変数に代入するもの。2つ目の例はソートのキーに計算結果を入れたもの
1
2
rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2)
sorted(rects, key=lambda rect (rect[0] * 2) + (rect[1] * 2))

11章 コメント、docstring、型ヒント 🔗

  • 型ヒントはコロンを利用する。関数の戻り値は矢印を使う
1
2
3
4
5
def describeNumber(number: int) -> str:
    if number % 2 == 1:
        return 'An odd number. '

myLuckyNumber: int = 42
  • クラスから作られたインスタンスはクラス名を型として使用する
1
2
3
4
5
6
7
8
9
import datetime
noon: datetime.time = datetime.time(12, 0, 0)

class CatTail:
    def __init__(self, length: int, color: str) -> None:
        self.length = length
        self.color = color

zophieTail: CatTail = CatTail(29, 'grey')
1
2
3
from typing import Union

spam: Union[int, str, float] = 42
  • こちらも非推奨ではあるが、Noneの型がありえる場合はOptionalを利用する
1
2
3
from typing import Optional

lastName: Optional[str] = None
  • リストや辞書型に型ヒントを設定する場合
1
2
3
4
from typing import List, Union

catNames: List[str] = ['Zophie', 'Simon']
numbers: List[Union[int, float]] = [42, 3.14]

12章 Gitでプロジェクト管理 🔗

  • pythonに関するファイルやフォルダを自動作成するcookiecutterモジュールがオススメ
  • GUIを利用したdiffツールmeld( sudo apt-get install meld )、kompare( sudo apt-get install kompare )
  • リポジトリからファイルを削除する場合は単純な rm ではなく git rm を行う事
  • リポジトリ内のファイル名変更および移動も git mv を利用してから git commit すること
  • git log --oneline で、長いログではなくコミットハッシュと各コミットメッセージの最初の行のみ表示することができる
  • コミット前の git add のみを行った状態を戻す場合は git restore <ファイル名>
  • ステージング解除は git restore --staged <ファイル名>
  • 3個のコミットをもとに戻す : git revert -n HEAD~3..HEAD
  • 1ファイルだけ元に戻す : git show <ハッシュ名>:<ファイル名>
  • リポジトリから情報を削除して復元できないようにするには git filter-branch コマンドか BFG Repo-Cleaner というツールを使うと良い

13章 パフォーマンスの測定とオーダー記法 🔗

  • コードを何千回、何百万回実行し平均実行時間を計測する timeit
  • timeittimeit.timeit() で渡した文字列内のコード以外の部分で書かれた変数や関数にアクセスすることができないため、アクセスする必要がある場合は timeit.timeit('print(spam)', number = 1, globals=globals()) として globals を入れる
  • timeitが小さなコードの計測を行うのに対し、関数やプログラム全体の分析には cProfile モジュールを利用する
1
2
3
4
5
6
7
8
import time, cProfile

def addUpNumbers():
    total = 0
    for i in range(1, 1000001):
        total += i

cProfile.run('addUpNumbers()')
  • アムダールの法則 : ${タスク全体の高速化率} = \frac{1}{1 - p} + \frac{p}{s}$
  • $s$ : 構成要素に加えられた高速化の割合
  • $p$ : プログラム全体に占めるその構成要素の割合
  • ビッグオーからコードの計算量を決定する。以下の例の場合…
1
2
3
4
5
6
7
def readingList(books):
    print('Here are the books I will read: ')
    numberOfBooks = 0
    for book in books:
        print(book)
        numberOfBooks += 1
    print(numberOfBooks, 'books total.')
  • 関数ではnは殆どの場合、パラメータに基づくためbooksのサイズが $n$
  • for book in books: を除き各行を1ステップとして扱う
  • for文の中には2ステップ存在するため、for文全体は $2n$ ステップ存在する
  • したがって全体として $2n + 3$ となる。 $2n + 3$ は線形時間 $O(2n)$ と定数時間 $O(3)$ 。したがって線形時間となる(nの増加に比例して実行時間も増加する)
  • オーダーの一般的ルール
    • どのデータにもアクセスしない場合は $O(1)$
    • データをループする場合は $O(n)$
    • ネストした二重ループがあり、それぞれがデータをいてレートする場合は $O(n^2)$
    • 関数呼び出しは1ステップではなく、関数内のコードの総ステップ数としてカウント
    • データを繰り返し半分にする分割統治のステップがあれば $O(\log{n})$
    • データの要素ごとに1回ずつ行われる分割統治のステップがあるコードの場合は $O(n\log{n})$
    • n個のデータの中で可能なすべての値の組み合わせを調べるとしたら $O(2^n)$
    • データの順列をすべて調べた場合は $O(n!)$
    • データのソートを含む場合は少なくとも $O(n\log{n})$

15章 オブジェクト指向プログラミングとクラス 🔗

  • コード中のいくつかの関数が同じデータ構造を操作する場合、通常はクラスのメソッドや属性としてまとめるのがベスト。(複数の関数で同じ変数を受け取っている状況)

16章 オブジェクト指向プログラミングと継承 🔗

  • super() 関数では、オーバーライドするメソッドが親クラスの元のメソッドを呼び出せる。
    • 通常classを継承して親class、子classが存在していた場合、外部から子class経由で親classのmethodが呼び出せるが、子class内で親classメソッドをそのまま呼び出したい場合に使うということか。
    • 以下は通常の継承パターン。この例だとあり得ないが ChildClass 内で親classの printHello methodを利用したい場合は someMethod = super().printHello() として再利用する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ParentClass:
    def printHello(self):
        print('Hello, world!')

class ChildClass(ParentClass):
    def someNewMethod(self):
        print('ParentClassのオブジェクトにはこのメソッドがない。')

child = ChildClass()
child.printHello()      # 有効
child.someNewMethod()   # 有効
  • is a 関係は継承、 has a 関係は包含
  • 継承すると親classの変更が子classにも影響を与える
  • 包含例。包含することで両方のクラスの将来の設計変更に柔軟性をもたらし、より保守性の高いコードにつながる
1
2
3
4
5
6
import wizcoin

class WizardCustomer:
    def __init__(self, name):
        self.name = name
        self.purse = wizcoin.WizCoin(0, 0, 0)   # class WizCoinを含む属性purseを作る
  • isinstance() issubclass() 関数でオブジェクトの型調査。以下はすべて True
1
2
3
isinstance(42, (int, str, bool))
isinstance(parent, ParentClass)
isinstance(child, ParentClass)
  • クラスメソッドではselfではなくclsを利用する
  • 多重継承例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Airplane:
    def flyInTheAir(self):
        print()

class Ship:
    def floatOnWater(self):
        print()

class FlyingBoat(Airplane, Ship):
    pass
  • 多重継承した場合、複数classで同じmethodが存在していた場合のmethod解決順序を確認するには mro() を利用する。上の例であれば FlyingBoat.mro()

17章 パイソニックなオブジェクト指向: プロパティとダンダーメソッド 🔗

  • 属性をプロパティにすることでprivateであって欲しい属性が誤って変更されることなどを防ぐことができる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ClassWithProperties:
    def __init__(self):
        self.someAttribute = 'ある初期値'

    @property
    def someAttribute(self):            # getterメソッドに相当
        return self._someAttribute      # private属性に変更している点がポイント

    @someAttribute.setter               # この修飾子が大事
    def someAttribute(self, value):     # setterメソッドに相当
        self._someAttribute = value     # 外部からは obj.someAttribute = '別の値' とするだけでsetterが呼ばれる

    @someAttribute.deleter              # この修飾子が大事
    def someAttribute(self):            # deleterメソッドに相当
        del self._someAttribute         # 外部からは del obj.someAttribute とするだけでdeleterが呼ばれる
  • setterを経由して値をセットされた際のエラーチェック例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class WizCoinException(Exception):
    pass

class WizCoin:
    def __init__(self, galleons, sickles, knuts):
        self.galleons = galleons
        self.sickles = sickles
        self.knuts = knuts

    @property
    def galleons(self):
        return self._galleons

    @galleons.setter
    def galleons(self, value):
        if not isinstance(value, int):
            raise WizCoinException('error message' + value.__class__.__qualname__)
        if value < 0:
            raize WizCoinException('error message' + value.__class__.__qualname__)
        self.galleons = value
  • getSomeAttribute()setSomeAttribute() のようなメソッドが必要になったら、プロパティを使うかもしれないサイン。実行時間を要したり、他属性やオブジェクトへの副作用のある場合などは除く
  • 自分で作成したすべてのclassでダンダーメソッドの __repr__()__str__() を作成しておくと良い。ただしrepr関数では機密情報がデータに入っていないようにすること
1
2
3
4
5
def __repr__(self):
    return f'{self.__class__.__qualname__}({self.galleons}, {self.sickles}, {self.knuts})'

def __str__(self):
    return f'{self.galleons}, {self.sickles}, {self.knuts}'
  • 自分で作ったWizCoinクラスから作成したオブジェクト同士を演算子で足したい場合、 addWizCoin() methodを書く代わりに、ダンダーメソッド __add__() を使う
1
2
3
4
5
def __add__(self, other):
    if not isinstance(other, WizCoin):      # 必ず型チェック!
        return NotImplemented

    return WizCoin(other.galleons + self.galleons, other.sickles + self.sickles, other.knuts + self.knuts)
  • 数値演算ダンダーメソッド
  • a + bb + a のように可換式が同じ結果となる場合は数値演算ダンダーメソッドと反射型数値演算ダンダーメソッドの2つをセットで定義する必要がある
1
2
def __rmul__(self, other):          # 掛け算の反射型数値演算ダンダーメソッド
    return self.__mul__(other)      # 左から数値を掛けられても右から掛けた数値演算ダンダーメソッドへ変換する
  • 代入型ダンダーメソッド : += -> __iadd__() 、 *= -> __imul__() 、代入型ダンダーメソッドは殆どの場合 return self となる
  • 比較ダンダーメソッド。一部省略しつつ、こんな感じのイメージ。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import operator, collections.abc

def _comparisonOperatorHelper(self, operatorFunc, other):
    if isinstance(other, WizCoin):
        return operatorFunc(self.total, other.total)
    elif operatorFunc == operator.eq:
        return False
    elif operatorFunc == operator.ne:
        return True
    else:
        return NotImplemented

def __eq__(self, other):
    return self._comparisonOperatorHelper(operator.eq, other)

def __ne__(self, other):
    return self._comparisonOperatorHelper(operator.ne, other)
comments powered by Disqus