Edited at

Effective Pythonはその名の通りEffectiveだった

More than 1 year has passed since last update.


あらまし

Effective Pythonをかいつまんで読みました. とても良い本だと思います. 自分はPython初級者だと思っているのですが, その自分がある程度じっくり考えて腑に落ちるレベルの事柄が載っていることから, 初級者と中級者の橋渡しをするという役目を十二分に果たしていると思います. 筆者はGoogleで大規模な開発に従事している方なので, 小さいコードばかり組んでいる自分には直感的に利便性が理解しづらい説明もいくらかあります. でもそれはBrett Slatkinのせいではありません. 自身の至らぬが故.

読んだだけでは忘れてしまいそうなので. 感心したポイントの一部をまとめておきたいと思います. ここで使用しているコードはGithub1にあります.


感心ポイント要約(販促)

一部抜粋して載せているので文脈が捉えづらいところもあると思いますが, 是非買って読んでみてください.


ジェネレータ式を使いなさい!

ジェネレータ式を使って大量の入力を逐次的に処理することができます:

import random

with open('my_file.txt', 'w') as f:
for _ in range(10):
f.write('a' * random.randint(0, 100))
f.write('\n')

it = (len(x) for x in open('my_file.txt'))
print(next(it))
print(next(it))

これをリストやタプルで一度に行うと大量のメモリを消費してしまいます. 割とよく知られている方法だとは思いますが, エキスパートPythonプログラミングでも同様にジェネレータ式が激推しされているのが印象的でした.


for...else, while...elseは使うべからず

使いどころがいまいちピンと来ないfor...elseですが, 結局使わないのがベストプラクティスのようです. 理由は, 一般的なelseと文脈が異なるから. 前のブロックが失敗したらelseに飛ぶのがふつうですが,for...elsefor文が正常に(breakされずに)終了したら else ブロックを実行します. これが可読性を損なうとのことです.

最初読んだ時は「なんだその程度のこと...」と思いましたが, 「Effective...」では一貫して「ジェネリックであること」に重きを置いています. プログラミング言語一般におけるelseの意味を損なうような用法は避けるべきだという主張は, 一通り読み終えた今, 当然のことのように感じます. こういう思考が身につくことに「Effective...」を読む意義があると思います.


クロージャとはこれ如何に?

クロージャとは状態を保持する仕組みです, と言ってもよくわからないので例をば:

def closure():

flag = False

def getter():
print(flag)
return getter

func = closure()
func() # False

func()flagという変数を保持していることになります. このテクニックを用いるとグローバル変数を減らすことができたりと便利です.

注意しなければならないことは代入が絡むと悲しいことが起きることです:

def closure():

flag = False

def setter():
flag = True
setter()
return flag

print(closure()) # False

setter()flag = Trueを試みていますが, closure()の戻り値はFalseです. これはsetter()内のスコープにはflagが存在しないために代入が変数定義として扱われてしまったのです. クロージャを活用したいときは代入にはよくよく注意しなければなりません. この問題を解決するためにはnonlocalを用います:

def closure():

flag = False

def setter():
nonlocal flag
flag = True
setter()
return flag

print(closure()) # True

これで一つ上のスコープを参照してくれるようになります. この問題は, Pythonでは変数定義と代入が同じ文法であるということが根源です. これを解決するために変数定義をJavascriptのようにvar flag = Falseとすることも提案されたようですが, 互換性のことも考えてnonlocalを採用したようです.

クロージャは便利ですがnonlocalを大きい関数で使うと把握が難しくなるので, そういうときはクラスを使ったほうが良いそうです.


キーワード引数の位置に注意

細かいことですが, キーワード引数は位置引数の後ろで指定しなければなりません:

def remainder(number, divisor):

return number % divisor

remainder(20, 7) # OK
remainder(20, divisor=7) # OK
remainder(number=20, divisor=7) # OK
remainder(divisor=7, number=20) # OK
remainder(number=20, 7) # NG


辞書を入れ子にするべからず!クラスを使うべし

辞書はコンテナとして非常に優秀ですが, 辞書の中に辞書が入っているような入れ子構造にすると保守が非常に辛くなります. そうなる前にクラスに分割しましょう:

import collections

Grade = collections.namedtuple('Grade', ('score', 'weight'))

class Subject(object):
def __init__(self):
self._grades = []

def report_grade(self, score, weight):
self._grades.append(Grade(score, weight))

def average_grade(self):
total, total_weight = 0, 0
for grade in self._grades:
total += grade.score * grade.weight
total_weight += grade.weight
return total / total_weight

class Student(object):
def __init__(self):
self._subjects = {}

def subject(self, name):
if name not in self._subjects:
self._subjects[name] = Subject()
return self._subjects[name]

def average_grade(self):
total, count = 0, 0
for subject in self._subjects.values():
total += subject.average_grade()
count += 1
return total / count

class Gradebook(object):
def __init__(self):
self._students = {}

def student(self, name):
if name not in self._students:
self._students[name] = Student()
return self._students[name]

book = Gradebook()
albert = book.student('Albert Einstein')
math = albert.subject('Math')
math.report_grade(80, 0.10)
math.report_grade(80, 0.10)
math.report_grade(70, 0.80)
gym = albert.subject('Gym')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)
print(albert.average_grade())

ちょい長いですが, 頑張って読んでみましょう. 「保守・拡張が容易なクラス設計とは如何なるものか」という問いに対するひとつの答えになっているように思えます.

「1.成績表 → 2.生徒 → 3.科目 → 4.点数」という層構造を, 各オブジェクトのインスタンス変数(辞書)が下の階層のオブジェクトを保持することで実現しています. 各クラスはインスタンス変数に下位クラスのインスタンスをセットするためのメソッドを持ち, さらに平均点を計算するメソッドもあります. じっくり眺めると, このコードが高い拡張性を持つことがわかるでしょう. もしこれを単一の辞書で実現しようとするとヤバいです.

上のコードは直感的にすぐ理解できるかどうかはわかりません. 自分はすぐにはわかりませんでした. しかしわかってしまえばとてもシンプルなアイデアであることが納得できました. 一方で, 理解できてなお「なんでこんな面倒くさい実装を!?」と感じるコードもあります. 理解が容易なコードが良いコードだとは限りません. 説明が容易なコードが良いコードなのです(多分). Zen of Pythonでもそんなことを言ってます:


Although that way may not be obvious at first unless you're Dutch.

そのやり方は一目見ただけではわかりにくいかもしれない。オランダ人にだけわかりやすいなんてこともあるかもしれない。

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.

コードの内容を容易に説明できるのなら、おそらくそれはよい実装である。


from プログラマが持つべき心構え (The Zen of Python)


@classmethodってどこで使うの?

自身のオブジェクトを生成するメソッドに使うとよいみたいです. 以下のような親クラス・子クラスを考えます:

from tempfile import TemporaryDirectory

class GenericInputData(object):
def read(self):
raise NotImplementedError

@classmethod
def generate_inputs(cls, config):
raise NotImplementedError

class PathInputData(GenericInputData):
def __init__(self, path):
super().__init__()
self.path = path

def read(self):
return open(self.path).read()

@classmethod
def generate_inputs(cls, config):
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))

class GenericWorker(object):
def __init__(self, input_data):
self.input_data = input_data
self.result = None

def map(self):
raise NotImplementedError

def reduce(self, other):
raise NotImplementedError

@classmethod
def create_workers(cls, input_class, config):
workers = []
for input_data in input_class.generate_inputs(config): # 引数に取ったクラスのインスタンスをつくる
workers.append(cls(input_data)) # 自身のインスタンスをつくる
return workers # 自身のインスタンスを返す

class LineCountWorker(GenericWorker):
def map(self):
data = self.input_data.read()
self.result = data.count('\n')

def reduce(self, other):
self.result += other.result

with TemporaryDirectory() as tmpdir:
config = {'data_dir': tmpdir}
workers = LineCountWorker.create_workers(PathInputData, config)

これも長いですが, がんばって! 重要なポイントは2点あります.

一つめはインスタンスを作る責任をそのクラス自身のクラスメソッドが担っているということです. 新しい子クラスたちがさらに追加されると「そのクラスのインスタンスはどこで誰が作るのか」を管理するのが大変になります. 上の例ではwith以下でLineCountWorker.create_workers()が自身のインスタンスを作っています. さらにその中で PathInputData のインスタンスも作っています2. つまり「どこで」「誰がつくるか」が明確になっているのです.

二つめは一つめと密接に関わっています. それは「clsは呼び出されるクラスに置き換えられる」ということです. まあ当たり前のことに思えますが, とても重要です. 上のコードであれば, GenericWorker.create_workers()内で定義されたcls(input_data)は, その子クラスであるLineCountWorkerを通して呼ばれるとLineCountWorker(input_data)に化けるということです. このおかげで, GenericWorkerを親クラスとする派生クラスHogeCountWorkerを新たに作ったとしても, そのインスタンスを作るのはやはりHogeCountWorker.create_workers()であり, HogeCountWorker内で新たにcreate_workersメソッドを作る必要は無いわけです. 親クラスから継承したメソッドなのに, 派生クラスから呼び出すとその子専用のメソッドに早変わり! これが「Effective...」内で散々語られている「ジェネリック」ということです.


販促, 再び.

他にもイテレータ・ジェネレータ関連のtipsや@propertyの使いどころなど, 為になるいいことがたくさん書いてあります. 是非是非買って読んでみてください3.


あまり良くないところ

Effective Pythonは素晴らしい本ですが気になるところもあります. 翻訳の質です. 例えば...

例1

(原)
You'd want a similar abstract interface for the MapReduce worker that consumes the input data in a standard way.

(訳)
入力データを標準的に消費するMapReduceのWorkerにも同様の抽象インターフェースが欲しくなったとしましょう。

例2

(原)
Here, I extend the InputData class with a generic class method that's responsible for creating new InputData instances using a common interface.

(訳)
InputDataクラスを拡張して、共通のインターフェースを用いる、新たなInputDataインスタンスを作る責任を負う、ジェネリックなクラスメソッドを追加します。

うーん...微妙です. 本当に内容を理解して書いているのかが疑わしい箇所も多く感じました. 特に「3章 : クラスと継承」は辛かったです. 自分の理解力不足を翻訳のせいにしたくなる程でしたが...いや, まあ, 自分の頭が足りないのが悪いのです.

とはいえ内容そのものが素晴らしいことに変わりはありません. 英語が得意な人は原文で読むことをおすすめします.


おわりに

プログラミングに限らず, 勉強本で最も重要なのは「優れた入門書」だと思っています. 数学を諦める高校生やC言語で脱落する大学生を見るにつけ, 初心者と初級者の間にある壁が高いことは明らかですから.

有り難いことにPythonは入門書の数には事欠きません. 母数が多ければその中に優れた本もあることでしょう. 一方で初級者・中級者向けの和書はどうでしょうか? オライリーからPython本はたくさん出ていますが, 幅広い層をカバーする脱初級者本は「Effective Python」と「実践 Python3」, オライリーを除けば「エキスパートPythonプログラミング」くらいなのではないでしょうか4. その中のひとつである「Effective Python」がこの完成度をもって上梓されたことは本当に喜ばしいことです.


参考図書


  • Brett Slatkin, Effective Python(オライリー・ジャパン, 2016)

  • Brett Slatkin, Effective Python(O'Reilly, 2015)

  • 西尾泰和, コーディングを支える技術(技術評論社, 2013)





  1. This software includes the work that is distributed in the Apache License 2.0 



  2. 正確には PathInputData のインスタンスを, 自身である LineCountWorker のコンストラクタに渡している. 



  3. オライリーの回し者ではありません. 



  4. もっといろいろあったらすいません.