zerebom / paper-books

@zerebom が読んだ技術書、論文をまとめています。推薦システム系が多いです。
https://github.com/zerebom/paper-books/issues
2 stars 0 forks source link

Effective Python #29

Open zerebom opened 4 years ago

zerebom commented 4 years ago

オライリーの本。Pythonプログラムを改良する59項目が書いてある。 使えそうなのだけメモる。

9 メモリが大きな内包表記にはジェネレータ式を使う

ジェネレータはリスト内包表記と同じ構文で周囲を()で囲えば良い。 また、ジェネレータは組み合わせをすることができる。

it = (len(x) for x in open('my_file.txt'))
roots = ((x,x**0.5) for x in it)
print(next(roots))

13 try/except/else/finallyを使って例外処理する

14 関数でNoneを返さない。

Pythonにおける評価式ではNoneの他に0や空文字もFalseになってしまうので、 関数はNoneを返すより、raiseで例外を返した方が良い

16 ジェネレータによるコード整備

空白文字の位置を返す関数を考える。 こういう時はindexを集めたリストを返すより、そのままジェネレーターでindexを返した方がわかりやすい しかも、メモリクラッシュを防げる

def index_words(text):
    result = []
    for index,letter in enumerate(text):
        if letter == ' ':
            result.append(index)
    return result

def index_words_iter(text):
    '''
    result = list(index_words_iter(text))
    '''
    for index,letter in enumerate(text):
        if letter == ' ':
            yield index

18 イテラブルなコンテナの定義

ジェネレータは呼び出しに対して初回しか値を返さない。 しかもエラーを吐かないので、意図しない挙動をすることがある。

複数回呼び出したいかつ、メモリクラッシュを防ぎたい時は

例えば、txt内の文章(数値)を全てreadし、100%に正規化したい場合は以下のようにすると良い。

def normalize(numbers):
   # ジェネレータを弾く
    if iter(numbers) is iter(numbers):
        raise TypeError('Must supply a container')

    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result

class ReadVisits(object):
    def __init__(self,data_path):
        self.data_path = data_path

    def __iter__(self):
        with open(self.data_path) as f:
            for line in f:
                yield int(line)

visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)

18 可変長引数

同じ処理をする引数が0~nこある時は、可変長引数を用いると良い

def log(message, *values):
    if not values:
        print(message)
    else:
        values_str = ','.join(str(x) for x in values)
        print(message,values_str)

ただし、新たな位置引数の追加などは難しいので要注意

20 動的なデフォルト値をキーワード引数に与える

関数のデフォルト値はimport 時の一回しか参照しない。 したがって、動的な引数を取りたい時はNoneを一度与え、関数内で定義する。 この時しっかりドキュストリングを書いた方が良い。

def log(message,when=datetime.now()):
    print(when,message)

def log(message,when=None):
    when = datetime.now() if when is None else when
    print(when,message)

21 キーワード専用引数

キーワード引数を、デフォルト値または、関数定義時に絶対指定させるといった用法で使いたい時。 つまり、位置引数で書き換えて欲しくない時に使う。

例えばbool値など、どっちがどっちの引数を表しているか間違えやすい時などに有効。 *を加えて、キーワード専用引数の開始を指定する。

def save_division_c(num,divisor,*,ignreo_overflow=False,ignore_zero_division=False)

# 位置引数だとエラーになる
safe_division_c(1,2,False,True)

22 辞書の要素が辞書になる時などはヘルパークラスを使おう

ex)ある生徒がある科目のテストを複数回受講する。各テストには点数と重みが与えらえている。 任意の生徒に対してそれぞれ受けたテスト、ある科目の全テストの重み付き平均などを求められるようにしたい。

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

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

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

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

        return total / total_weight

class Student:
    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:
    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')
math = albert.subject('Math')
math.report_grade(80, 0.10)
...
print(albert.average_grade())

23 単純なインターフェースにはクラスの代わりに関数を使う。

Pythonには、関数を引き渡すことによって振る舞いをカスタマイズできる組み込みAPIが多くある。 (フックと呼ぶ) ex)list型のsort,defalutdictの引数など こういう時に、クラスのように内部状態を保持させたい&関数のように呼び出しを簡単にしたい状況が起こり得る。 そういう時はクラスにcallメソッドを実装すると、良い。 これはオブジェクトが関数のように呼び出されるのを許す。


class BetterCountMissing:
    def __init__(self):
        self.added = 0 

    def __call__(self):
        self.added +=1
        return 0

counter = BetterCountMissing()
#オブジェクトが関数(=フック)として振る舞える
result = defaultdict(counter,current)
for key,amount in increments:
    result[key] += amount

print(counter.added)

24 @classmethodを使い、オブジェクトをジェネリックに構築

参考: https://blog.pyq.jp/entry/Python_kaiketsu_190205

下記のように、関数内部で特定のクラスを呼ぶ時、普通に実装してしまうと 全くジェネリックで無くなってしまう。InputDataのサブクラスで別のものを使いたい時に 書き換える必要があるからだ。 サブクラスを引数にとるという方法もあるが、この場合全てのサブクラスのinitを共通のものにするという制約がついてしまう。この時、@classmethodを使うと良い。 こうすると、クラスをオブジェクト化する必要がなく特定のメソッドが使えるようになる。 (つまりinitを省くことができる?) よく、そのクラス自身を作る時に使われる。

class InputData:
    def read(self):
        raise NotImplementedError

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

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

class Worker(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

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

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

def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir, name))

def create_workers(input_list):
    workers = []
    for input_data in input_list:
        workers.append(LineCountWorker(input_data))
    return workers

def mapreduce(data_dir):
    inputs=generate_inputs(data_dir)
    workers = create_workers(inputs)
    return workers

class GenericInputData:
    def read(self):
        raise NotImplementedError

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

class PathInputData(GenericInputData):
    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_list:
            workers.append(cls(input_data))
        return workers

class LineCountWorker(GenericWorker):
    # 同じ
    pass

def mapreduce(worker_class, input_class, config):
    workers = worker_class.create_workers(input_class,config)
    return workers

mapreduce(LineCountWorker,PathInputData,config)

To be continued...