Contents
スポンサーリンク
概要
「きれいなPythonプログラミング ~クリーンなコードを書くための最適な方法」を読んだので感想を書こうと思います。
技術書のセールとおすすめ書籍を紹介しています。合わせてご覧ください。
スポンサーリンク
書籍概要
どんな本?
誰にでも読みやすく。Clean Code を実践しよう
自分の書いたコードに自信を持てるプログラマーになろう
[誰にでも読みやすい 広く公開できるコードを書こう]
本書ではきれいなコード(Clean Code)を書くために、コマンドライン、コード整形、型チェッカー、リンター、バージョン管理 などのその道のプロが利用しているツールを詳解し、Pythonプログラミングスキルを向上させる方法を学びます。[Clean Codeを実践するツールを活用できるようになろう]
開発環境のセットアップ、変数の命名方法、読みやすさ向上のための最適な方法 を紹介します。[オブジェクト指向設計を理解し アルゴリズムを活用しよう]
コードの公開に必要となるドキュメントの作成や書式の統一、またパフォーマンスの測定、オブジェクト指向プログラミング、コーディングインタビューで一般的に使用されるオーダー記法(Big O)について説明します。本書の後半では2つのコマンドラインのゲーム「ハノイの塔(ロジックパズル)」と「四目並べ(タイル落としゲーム)」を作りますが、書いたゲームのコードが本書の「最適な方法」でプログラミングされているかを確認してみましょう。
- Al Sweigart (著), 岡田佑一 (翻訳)
- 2022年02月 発行
- 384ページ
- 定価3,608円(税込)
スポンサーリンク
内容のまとめと感想
タイトルの通り、きれいなPythonコードを書くためのノウハウや手法を説明する書籍です。
コード自体に関する説明だけではなく、クリーンなコードを書くために必要になる、各種ツールや考え方もカバーした幅広い内容を取り扱っているのが特徴です。
とりあえずコードを書ける状態から卒業して、綺麗なでクリーンなコードを書くためにどういったことが必要なのかを総合的に学べるようになっています。
特徴
Pythonの基本文法は理解している前提
基本的にPythonをある程度書けるエンジニアを想定して書かれているので、基本的な文法や構文に関する説明はありません。
Python自体を学びたいという人は、別途入門書等を読むことをオススメします。
インタラクティブシェルでの実行
コードの実行方法に関しては、基本的にインタラクティブシェルによる対話的な実行になっています。
ですのでIDEや環境に依存せずに極力汎用的な形での紹介になっています。
幅広いテーマ
コードの書き方だけではなく、エラー(トレースバック)の読み方やエラー内容に関しての、インターネット上のコミュニティでの質問方法など、プログラマーとして知っておきたい基本的なノウハウに関しても書かれていて面白いです。
その他にも、リンター、Docstring(ドキュメント)などなど、クリーンで他者が理解できるコードを書くためのツールに関してもカバーしています。
その他にもGitによるバージョン管理に関しても1つの章を割いていたりします。(個人的にはちょっといらない気が・・・。)
初級者〜中級者向け
本書でも書かれていますが、より深い内容を学びたい場合は「Effective Python」や「Fluent Python」といった評判の高い書籍を挙げています。
さらなるステップアップを目指したい場合にはこういった書籍に取り組むのが良いと思います。
オブジェクト指向関係の話題は後半に集約
クラスを使用したオブジェクト指向プログラミングに関しての話題は、後半の数章に集約されています。
それまでは基本的に関数ベースのシンプルなサンプルになっているので、クラスベースのPythonに慣れていないという人でも読むのに困らないようになっています。
具体的なコードを使用したリファクタリング
「ハノイの塔」と「四目並べ」という具体的なゲームを用いて、本書で学んだノウハウを適応してどうコードを綺麗にしたら良いかを学ぶことができます。
挙げているゲームのコード自体もシンプルでコード量も多くないので、理解するのには良い教材だと思いました。
まとめ
最初はPython版クリーンコード的なものをイメージしていましたが、コードの書き方だけでなく各種関連ツールやパフォーマンス向上、オブジェクト指向など、非常に幅広いテーマを取り扱っていて面白かったです。
取り扱う内容自体が幅広くなりすぎて、個々の説明が少し薄くなってしまっている感もありますが、Pythonというテーマでエンジニアとして知るべき各種ノウハウが凝縮されていて良い本だと思いました。
個人的なメモ
怪しいコード集
関数やモジュールにすべきクラス
Pythonでもクラスによる実装が可能なため、単純な実装でもわざわざクラスによる実装になりがちであるが、簡単に関数で実装できるなら関数で実装すべき。
下記はサイコロによるランダムな数値を返すクラスによる実装。
1 2 3 4 5 6 7 8 9 10 11 |
import random class Dice: def __init__(self, sides=6): self.sides = 6 def roll(self): return random.randint(1, self.sides) d = Dice() print("You roled a", d.roll()) |
実際に必要なのは1~6のランダムな数値を返すということなので、関数で実装すれば下記の1行で済む。
1 |
print("You rolled a", random.randint(1, 6)) |
内包表記の中に内包表記がある
配列をループ処理する際に、pythonでは内包記法を用いることで、1行でシンプルに書くことができる。
5の倍数の数値以外を文字列として格納する例:
1 2 3 4 5 6 7 8 9 10 11 |
# リスト内包記法を使用しない場合 spam = [] for number in range(100): if number % 5 != 0: spam.append(str(number)) # リスト内包記法を使用した場合 spam = [str(number) for number in range(100) if number % 5 != 0] # {}を使用することで辞書にもできる spam_dic = {str(number): number for number in range(100) if number % 5 != 0} |
ただし、ネストした配列にたいして書くと、逆に読みにくくなってしまうのでやめるべき。
片側はいったんforループで回した上で、内包記法を用いるのがわかりやすい
1 2 3 4 5 6 7 |
# ネストしたリストに対して両方とも内包記法を使用 nestedIntList = [[0,1,2], [3], [4,5]] nestedStrList = [[str(i) for i in subList] for subList in nestedIntList] # 片側だけ内包記法を使用 for subList in nestedIntList: nestedStrList.append([str(i) for i in subList]) |
パイソニックなコードを書こう
range()ではなくenumerate()を使う
forループ内でindexへのアクセスが必要となる際には、rangeによる記法の代わりに、enumerate()を用いることですっきりとしたコードになる。
1 2 3 4 5 6 7 8 9 |
# インデックスを取り出す方法(読みにくい) animals = ['dog', 'cat', 'moose'] for i in range(len(animals)): print(i, animals[i]) # パイソニックな例 animals = ['dog', 'cat', 'moose'] for i, animal in enumerate(animals): print(i, animal) |
バックスラッシュが多い文字列の場合にはraw文字列を使う
パスを文字列で扱う場合などに、通常の文字列だとエスケープのために¥¥といった形で表現しなければいけないが、rを先頭に付与してraw文字列として扱うとそのまま記載できてすっきりとする。
1 2 3 4 |
# エスケープ print("C¥¥temp¥¥abcd") # raw文字列(エスケープ不要) print(r"C¥temp¥abcd") |
switchの代わりに辞書を使う
pythonにはswitch文がないため、複数の条件をifを用いて書くと冗長になる。
代わりに辞書を使って書くとシンプルなコードになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# 複数条件の実装例(冗長) season = "" holiday = "" if season == "Winter": holiday = "New years day" elif season == "Spring": holiday = "May day" elif season == "Summaer": holiday = "Juneteeth" elif season == "Fall": holiday = "Hallowen" else: holiday = "Personal" # パイソニックな書き方 holiday = {"Winter": "New years day", "Spring": "May day", "Summaer": "Juneteeth", "Fall": "Hallowen", }.get(season, "Personal") |
代入演算子と比較演算子の連結
範囲条件を指定したい際に、andで2つの条件を記載するのではなく、比較演算子の連鎖させる書き方を用いることでシンプルになる。
1 2 3 4 5 6 7 |
# パイソニックでない if 42 < spam and spam < 99: pass # パイソニック if 42 < spam < 92: pass |
下記のような複数の値が同一かのチェック処理も連鎖を用いることでシンプルに書ける
1 2 3 |
# パイソニック if spam == egg == bacon == "string": pass |
Pythonのよくある落とし穴
copy.copy(),copy.deepcopy()を使用せずに可変型の値をコピーしない
他のプログラム言語でも同じだが、値型でない変数を=で代入すると、アドレスのコピーとなる。
そのため、そのまま値変更すると代入元となる値も変化してしまう。
copy.copy()で参照ではなく、値自体をコピーすることができる。
1 2 3 4 5 6 7 8 9 10 11 |
# 参照のコピー bacon = [2, 4, 6] ham = bacon # bacon[0]の値も9になってしまう ham[0] = 9 # コピー bacon = [2, 4, 9] ham = copy.copy(bacon) # bacon[0]の値は2のままで変わらない ham[0] = 9 |
ただし、リスト内に値型以外のものが格納されていると、そちらはアドレスのコピーが入ってしまう。
そのようない場合には、copy.deepcopy()を使用することで、中の要素も含めて完全にコピーが作成される。
パフォーマンスは落ちるが、わざわざ要素が値型以外もあるのかを判断するのは大変なのでdeepcoyを常に使用するのが楽。
1 2 3 4 5 6 7 8 9 10 11 |
# リストのリストは参照コピーになる bacon = [[1, 2], 4, 9] ham = copy.copy(bacon) # ham[0][0]の値も変わってしまう bacon[0][0] = 9 # ディープコピー bacon = [[1, 2], 4, 9] ham = copy.deepcopy(bacon) # ham[0][0]の値は変わらない bacon[0][0] = 9 |
文字列連結で文字列を作らない
他のプログラム言語でもよく上がる話題だが、文字列の連結を大量に繰り返す場合には文字列は連結しないほうが良い。
文字列自体は実は代入の度に新しい文字列オブジェクトが生成されるため、大量に繰り返すと文字列オブジェクトを大量に作ることになってしまい、パフォーマンス上よろしくない。
リストに格納しておいて、最後にまとめて結合する方法が良い。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 悪い例 finalStr = "" for i in range(10000): # 毎回新しい文字列オブジェクトが生成されてしまう finalStr += "spam" # 良い例 finalStrs = [] for i in range(10000): finalStrs.append("spam") # 最後に1度だけ文字列オブジェクトを作成する finalStrs = "".join(finalStrs) |
良い関数の書き方
可変長関数を作成するために*を使う
関数の引数に*を付与することで、可変長の引数を定義できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# *をつけると可変調引数となる def product(*args): result = 1 # forループで列挙 for num in args: result *= num return result product(3, 3) product(2, 1, 3) # 可変長引数を用いない実装で同様なことをやるにはarrayを渡す必要がある product([3, 3]) |
*args自体はタプルとして扱われる。
可変長引数を使わなくても、arrayを引数として渡せば同様なことを実現はできるので、どのような使い分けをすべきか?
使う側が自然に使えるように意識して使い分ける。
例1:sum関数はarrayの方が望ましい。sum(2, 3, 6)と書くよりも、sum(array)といった形で配列を渡す方が使い方として多いと推測される。
例2:print関数は可変長の方が望ましい。print("ABC", "AAA")と書く方が一般的で、表示するのにわざわざarrayの文字列を生成するのは不自然。
どちらにも対応させる方法も可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 可変長と配列のどちらでも受け取る方法 def product(*args): result = 1 # 要素巣が1ならば配列として仮定 if len(args) == 1: values = args[0] else: values = args if len(values) == 0: raise ValueError("args is empty seqeunce") for num in values: result *= num return result |
**を使って可変長の関数を作る
*がタプルなのに対して、**とすることで辞書(dict)と同じ可変長の引数として扱われる。
呼び出し側はdict形式で渡さなくても良いので、便利である。
1 2 3 4 5 6 7 |
# 可変長引数(dict) def test(**kwargs): for k in kwargs: print(k, kwargs[k]) # 引数自体はdictでなくて良い test(a=10, b=20) |
コメント、docstring、型ヒント
リストや辞書型に型ヒントを設定する
Listなどに対して型ヒントを使用するにはtypingモジュールを利用する。
1 2 3 4 5 6 7 8 9 |
from typing import List, Union # list (中の要素に関するチェックされない) spam: list = [1, "a", 2] # 型を指定 catNames: List[str] = ["a", "b"] # Unionを使えば複数の指定 numbers: List[Union[int, float]] = [1, 10.1, 3] |
パフォーマンスの測定とオーダー記法
cProfileプロファイラー
関数の処理時間を簡単に計測することができる。
1 2 3 4 5 6 7 8 9 |
import time, cProfile # 計測対象のメソッド def addUpNumbers(): total = 0 for i in range(1, 10000): total += i # 計測対象を呼び出す cProfile.run("addUpNumbers()") |
実行結果っとして、各関数の実行時間などが表示される。
1 2 3 4 5 6 7 8 9 |
4 function calls in 0.005 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.005 0.005 <string>:1(<module>) 1 0.005 0.005 0.005 0.005 Main.py:6(addUpNumbers) 1 0.000 0.000 0.005 0.005 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} |
オブジェクト指向プログラミングと継承
多重継承
多くのプログラミング言語は多重継承ができないが、Pythonでは多重継承(ミックスイン)が可能になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class AirPlan: def fly(self): print("空を飛んでいます") class Ship: def flot(self): print("浮かんでいます") # 多重継承 class FlyingBoad(AirPlan, Ship): pass # 継承元の2つのメソッドを呼び出す flyingBoad = FlyingBoad() flyingBoad.fly() flyingBoad.flot() |
継承元でメソッドが重複した場合には左側に書かれた継承クラスの処理が実行される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class AirPlan: def fly(self): print("空を飛んでいます") def flot(self): print("空を浮かんでいます") class Ship: def flot(self): print("浮かんでいます") # 多重継承 class FlyingBoad(AirPlan, Ship): pass class BoatFlying(Ship, AirPlan): pass # AirPlanのflotがよばれる flyingBoad = FlyingBoad() flyingBoad.flot() # Shipのflotがよばれる flyingBoad = BoatFlying() flyingBoad.flot() |
パイソニックなオブジェクト指向
プロパティ
Pythonにはアクセススコープが無いため、クラス内の属性に関しても自由にアクセスできる。
そのため、一般的に_から始まる属性やメソッドを作ることでスコープを示すことになる。
ただしプロパティを使用することで、属性の直接的なアクセスによって想定外の動作を防ぐことができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Person: def __init__(self, name): self._name = name # getter @property def name(self): return self._name # setter @name.setter def name(self, value): self._name = value tom = Person("Tom") print(tom.name) tom.name = "Bob" print(tom.name) |
Setterを使用することで、エラーチェック処理を実装することができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Person: def __init__(self): self._name = "" # getter @property def name(self): return self._name # setter @name.setter def name(self, value): print("setter") if not isinstance(value, str): raise Exception("name is only str value ia available") if value.strip() == "": raise Exception("name is not allowed empty str.") self._name = value |