LoginSignup
0
0

More than 3 years have passed since last update.

Effective Python一人輪読会(Item 1 to 18)

Last updated at Posted at 2020-08-22

Table of Contents

  1. はじめに
  2. Chapter 1: Pythonicな考え方
    1. Item 1: どのバージョンのPythonを使っているか意識せよ
    2. Item 2: PEP 8のスタイルガイドに従え
    3. Item 3: bytesとstrの違いを理解せよ
    4. Item 4: C言語ライクなフォーマット文字列やstr.formatよりもF-Stringsを使え
    5. Item 5: 複雑なexpressionsでなくヘルパー関数を書け
    6. Item 6: インデックスよりもunpackingを使え
    7. Item 7: range()よりもenumerate()を使え
    8. Item 8: iteratorsを並列で処理するためにzipを使え
    9. Item 9: forやwhileループの後のelseは使うな
    10. Item 10: Walrus operator (:=) を使って繰り返しを避けよ
  3. Chapter 2: リストとディクショナリ
    1. Item 11: シーケンスをどうスライスするか知っておけ
    2. Item 12: strideとsliceを同時にするな
    3. Item 13: スライスするよりcatch-allのunpackingを使え
    4. Item 14: 複雑な条件のソートではkeyパラメータを使え
    5. Item 15: ディクショナリに追加する順番が守られるとは限らない
    6. Item 16: 欠けているディクショナリのキーを扱うのに、inとKeyErrorよりもgetを使え
    7. Item 17: 内部状態に欠けている項目を扱うには、setdefaultよりもdefaultdictを使え
    8. Item 18: __missing__()メソッドを使って、キー依存のデフォルト値を作る方法を知っておけ

はじめに

この記事はEffeitive Python第2版(英語版)の読書メモ(その1)です。私のブログ https://achiwa912.github.io/ からの転載になります。ブログの方にはその2(以降)も先行して載せてありますので、よかったらお願いします。

以前、Effective Javaの輪講に参加したことがありますが、あまりの難易度に消化不良のままフェードアウトしてしまいました。今回は同じ轍を踏まないよう、背景知識をネットで調べながらじっくりと読み進めたいと思います。

私のPythonの知識は、入門書2冊(Python Crash Course, Introducing Python)を読んだ程度ですので、同じような初中級レベルの方の参考になる、、、かもしれません。

Chapter 1: Pythonicな考え方

Item 1: どのバージョンのPythonを使っているか意識せよ

最初は特に難しくありません。

Item 2: PEP 8のスタイルガイドに従え

はい。emacsをPythonのIDE化するelpyを導入する際に、py-autopep8, blackenを入れたので大丈夫でしょう。

Item 3: bytesとstrの違いを理解せよ

unicodeとUTF-8の関係がよくわからなくなっていたので、少し整理しました。

  • Python3のstringsは全てunicode stringsである。unicodeを使ってASCII以外の文字が表現できる。¥u + 4桁の数字。
  • stringをファイルにセーブしたりする時に、bytesにエンコードする。このときUTF-8等を使う
  • エンコードされたbytesをstringに戻すときにはデコードする
  • UTF-8は文字列をエンコード、デコードする最も標準的なスキームである

len(bytes)するとバイト数が出る。len(str)は文字数をカウントする。
unicodeの1文字は2バイト以上の場合がある。日本語など。

Item 4: C言語ライクなフォーマット文字列やstr.formatよりもF-Stringsを使え

はい。F-Stringsは直感的で一番使いやすいので、こればかり使っています。

Item 5: 複雑なexpressionsでなくヘルパー関数を書け

Effectiveシリーズの本領発揮です。
いきなり、意味がわらかない部分がありました。

from urllib.parse import parse_qs
my_values = parse_qs('red=5&blue=0&green=',
                 keep_blank_values=True)
print(repr(my_values))
>>>
{'red': ['5'], 'blue': ['0'], 'green': ['']}

まず、parse_qs()メソッドがわかりませんので調べます。
こちらより、

urllib.parse.parse_qs(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None)¶

Parse a query string given as a string argument (data of type application/x-www-form-urlencoded). Data are returned as a dictionary. The dictionary keys are the unique query variable names and the values are lists of values for each name.

なるほど、parse_qs()は、URLに埋め込まれるクエリータイプの文字列をパースして、ディクショナリにして返してくれるメソッドでした。うーん、これを説明なしで書くとは、これまでの入門書とはひと味違います。

そしてこれ:

red = my_values.get('red', [''])[0] or 0
green = my_values.get('green', [''])[0] or 0
opacity = my_values.get('opacity', [''])[0] or 0

何ですか、これは。getメソッドの二つ目の引数['']がわかりません。そしてgetメソッドの戻り値に[0]が付いているのも謎です。ディクショナリーのgetメソッドについてネットで調べてみます。ここによると、

Syntax
ditionaru.get(keyname, value)
 <snip>
 value - Optional. A value to return if the specified key does not exist.
      Default value None

二つ目の引数は、指定したキーが存在しなかったときに返す値、だそうです。

それでは[0]は? 手元のPythonインタープリターで見てみます。

>>> my_values.get('red', [''])
['5']
>>> my_values.get('green', [''])
['']
>>> my_values.get('opacity', [''])
['']

どれも、stringを一つだけ持つリストを返していました。確かにparse_qsの説明をよく読むと、

...and the values are lists of values for each name.

リストを返すと書いてありました。なので、[0]を付けるとリストの最初(かつ、この場合唯一)のメンバーを返すのですね。

>>> my_values.get('red', [''])[0]
'5'

確かにそうなりました。

。。。すっきりしたところで読み進めていったら、説明が書いてありました。がーん。

複雑なexpressionの例:

red = int(my_values.get('red', [''])[0] or 0)

このようなexpressionを書く代わりに、ヘルパー関数を作ります。

def get_first_int(values, key, default=0):
    found = values.get(key, [''])

    if found[0]:
    return int(found[0])
    return default

そして、次のように書けばOK。

red = get_first_int(my_values, 'red')

Item 6: インデックスよりもunpackingを使え

知らない関数が説明無しで出てきました。

for rank, (name, calories) in enumerate(snacks, 1):
    print(f'#{rank}: {name} has {calories} calories')

enumerate()って、何でしたっけ? 特に二つ目の引数。。。

ここによると、何かをiterateして、更にカウンターまでつけてくれる便利なビルトインの関数だそうです。二つ目の引数はカウンター初期値でデフォルトは0。おー、これは便利かも。

Item 7: range()よりもenumerate()を使え

ここではビルトイン関数enumerate()の詳しい説明が。。。Item 6と7を逆にした方が読みやすいのでは。。。後で説明することを当たり前のように前のItemで使うのが、Effecitveシリーズの伝統なのでしょうか。

range()はインデックスを使って自分でiterateするので複雑だし間違いやすいし読みづらい。enumerate()は自動でiterateして、更にインデックスまで付けてくれる。

Item 8: iteratorsを並列で処理するためにzipを使え

zip()忘れていました。複数のiterators(eg, リスト)から次の値をそれぞれ持ってきてタプルにしてくれる(lazy generatorと言うそうです)。zip()はその繰り返し回しか扱わないので、例えば無限長のリストに対して使ってもメモリを使い切る心配は無い。

Item 9: forやwhileループの後のelseは使うな

はい、使いません。本の指摘通り、「breakで抜けなかった時だけelseを実行する」という意味が紛らわしいので。

この本のKindle版にはClick here to view code imageというリンクが、code snip部分によく出てきます。Kindleデバイスの画面では見づらいからなのかと思っていましたが、本文のcode snipにはとんでもない欠陥がたまにあることがわかりました。例えばこの項目にある例です。

def coprime_alternate(a, b)
    is_coprime = True
    for i in range(2, min(a, b) + 1):
    if a % i == 0 and b % i == 0:
        is_coprime = False
        break
    return is_coprime

これ、いかにもおかしいです。Click here to view code imageをクリックするとわかりますが、if文のインデントが間違っています。正しくは、

def coprime_alternate(a, b)
    is_coprime = True
    for i in range(2, min(a, b) + 1):
        if a % i == 0 and b % i == 0:
            is_coprime = False
            break
    return is_coprime

インデントでブロックの範囲を指定するPythonとKindleの相性は悪いです。おや?と思ったら、Click here to view code imageリンクを押さないといけません。みなさん、紙の本かPDF版を買いましょう。でも、Kindle版が一番安いんですよね。。。

  • ペーパーバック $41、Kindle版 $23.49 (@amazon)
  • PDF版 $31.99 (@informIT)

うーん、この値段ならPDF版を買うべきだったかも。。。

Item 10: Walrus operator (:=) を使って繰り返しを避けよ

Python 3.8で導入されたwalrus operatorですよ。流石、2019年末に改訂された2nd editionです。

.. these assignment statements are written a := b and 
pronounced "a walrus b" (because := looks like a pair
of eyeballs and tusks).

うーむ。ジョークなのか本気なのか。。
(walrus - セイウチ。tusks - 牙(キバ))

ここで出てくるloop-and-a-halfイディオムって何でしょう?

ここによると、while Trueなどで無限ループを作っておいて、そのループを抜ける条件をあらかじめsentinel(見張り、歩哨)の値として決めておくパターンをこう言うらしいです。以下のような感じ。これならよく使いますよね。

SENTINEL = -1
while True:
    ...
    if num == SENTINEL:
        break

これをwalrus operatorを使ってよりシンプルにします。

SENTINEL = -1
while (num := xxx) != SENTINEL:
    ...

walrus operatorの本質は、変数にアサインするだけでなく、アサインした変数をevaluateすることで、繰り返しを減らせる、ということのようです。定義だけ見ても、軽く読み飛ばしそうですが、後半のevaluateするというところがとても大事です。

Chapter 2: リストとディクショナリ

Item 11: シーケンスをどうスライスするか知っておけ

これ大事ですけど、使いこなせていませんでした。為になります。

  • somelist[start:end]startは含み、endは含まない
  • b = a[:]とすると、`bにはaのコピーがまるまる入る。abの実体はそれぞれ別。a == bだけどa is not b`
  • b = aとすると、baの実体を指す。a == bかつa is b

Item 12: strideとsliceを同時にするな

somelist[start:end:stride]というやつです。同時にする必要があるなら、まずstrideだけして、その結果に対してsliceするように2段階で行う。最初にstrideしてiteratorを可能な限り短くすることがポイント。こんな感じ:

x = somelist[::stride]
y = x[start:end]

そもそもiterator, itarableって、名詞と形容詞以外の違いはあるんでしょうか。混乱してきたので整理します。

ここがわかりやすく説明していました。

  • iteratorは、それをiterateする(1個ずつたどる)ことができるオブジェクト
  • 具体的にPythonでは、__iter__()__next__()(=iterator protocol)を実装している
  • リスト、タプル、ディクショナリ、セットは全てiterableオブジェクト
  • これらはiterableな入れ物で、iter()メソッドを使ってiteratorを取り出すことができる

うーむ、これまで厳密に区別していませんでした。

itertoolsisliceって何でしょう? ここに説明がありました。

itertools.islice(iterable, stop)

itertools.islice(iterable, start, stop[, step])

Make an iterator that returns selected elements from the iterable. If start is non-zero, then elements from the iterable are skipped until start is reached. Afterward, elements are returned consecutively unless step is set higher than one which results in items being skipped. If stop is None, then iteration continues until the iterator is exhausted, if at all; otherwise, it stops at the specified position. Unlike regular slicing, islice() does not support negative values for start, stop, or step.

「指定されたiterableから選択された要素を返すiteratorを作る。」

これって、iterable[start:stop:stride]と何が違うんでしょうか???
Item 36にリンクが張ってあるので、そちらをチラ見してみます。

Use islice to slice an iterator by numerical indexes without copying.

おー、なるほど。大事なのはwithout copyingのところでしたか。
たくさんメモリを使いませんよ、ということですね。

。。。後から考えてみると、itertools.islice()が返すのはiteratorで、next()等を使って回さないと一通りの結果がが得られないのに対し、iterable[…]の方は一式結果の揃ったリストが得られるので、その違いは明らかでした。

Item 13: スライスするよりcatch-allのunpackingを使え

こんなやつです。残りをリストにして返します。

largest, second, *others = [20, 19, 15, 9, 8, 5, 3, 1, 0]
print(largest, second, others)
>>>
20 19 [15, 9, 8, 5, 3, 1, 0]

catch-allの*othersは先頭や真ん中に来てもよい。

largest, *others, smallest = [20, 19, 15, 9, 8, 5, 3, 1, 0]
(largest, others, smallest)
>>>
20 [19, 15, 9, 8, 5, 3, 1] 0

Item 14: 複雑な条件のソートではkeyパラメータを使え

おっと、出ました。anonymousなlambda関数。

lambda x: x.name

のように使うのでした。この例ではxが引数。

自作クラスのオブジェクトがメンバーであるリストをソートする時など、どのようにソートすればよいかPythonが判断できない場合には、ソートするkeyとして関数を渡してやります。

tools.sort(key=lambda x: x.name)

とすると、名前でソート。

tools.sort(key=lambda x: x.weight)

とすると、重さでソート。

Item 15: ディクショナリに追加する順番が守られるとは限らない

知りませんでした。Python 3.7から、ディクショナリに追加した順序を維持するようになったそうです。だからと言って、常にそうなることを前提にしてはいけない、という項目。ディクショナリの代わりにディクショナリライクな別のデータ構造が使われていた場合に、この前提が通用しないことがあります。

3番目のアプローチの意味がわかりません。よく読んで考えてみると、type annotationsとは何か、知りませんでした。ここに説明がありました。例えば以下の場合、

def combine(a: str b: str, time: int) -> str:
    return (a + b) * times

引数の後に付く「: <type>」は、その引数のタイプを示唆しています。示唆と書いたのは、これは純粋に読みやすさのためであって、実行時に強制しないという意です。更に -> <type>で、戻り値のタイプも示唆します。

そして、実行時に以下のように--strictを付けると、この示唆を強制するようです。

$ pytyon3 -m mypy --strict example.py

Item 16: 欠けているディクショナリのキーを扱うのに、inとKeyErrorよりもgetを使え

見慣れない表現がありました、

votes[key] = names = []

これは、votes[key]name両方に[]を代入する、ということでした。
よく見るのは a = b = 100のような表現ですが、少し変わっただけでわからなくなりました。

ディクショナリにキーが無かったときに初期化するやり方として、getを使うのが一番よいとのこと。次は、ディクショナリの指定キーの値を+1し、もしキーが存在しない場合はキーを追加した上で行うパターン。

count = counters.get(key, 0)
counters[key] = count + 1

別のパターンでも。値がリストになっていて、キーが存在しない時には空のリストを追加するもの。

if (names := votes.get(key)) is None:
    votes[key] = names = []
names.append(who)

Item 17: 内部状態に欠けている項目を扱うには、setdefaultよりもdefaultdictを使え

「内部状態に欠けている項目」って何でしょう?
例として、行ったことのある国と、訪れた街をvalueとして街のセットを持つディクショナリとして表現するとします。

{'Japan': {'Kyoto', 'Yokohama'}, 'France': {'Paris'}}

のような感じで。ここに'England' - 'London'を追加するとき、まだ'England'は登録されていない=欠けているという意味で使っているようです。「内部状態」もわかりにくいです。このデータ構造の中身のことをそう言っているのでしょうが。一つ前のitemと似ていますね。

  1. 指定キーが存在しなければ追加&値を初期化した上で、
  2. 指定キーに対する値を+1したり、リストを追加したり、etc. する

このユースケースでは次のようなヘルパー関数を書け、と言っています。

from collections import defaultdict

class Visits:
    def __init__(self):
    self.data = defaultdict(set)

    def add(self, country, city):
    self.data[country].add(city)

defaultdict(default_factory)ここによると、ディクショナリライクなオブジェクトを返す関数で、もし追加しようとするキーが無かったら、default_factory関数がキーに対するデフォルトの値を提供します。上の例ではdefaultdict(set)とあるので、空のセットが設定されます。

defaultdictの説明がありませんが、この本はかなり高度な知識を前提としているか、調べながら読め、ということなのでしょうか。

このヘルパー関数を使うことで、やりたいことが短く書けます。

visits = Visits()
visits.add('Japan', 'Kyoto')
visits.add('Canada', 'Calgary')

getdefaultdictの使い分け:

  • 自分でディクショナリを作るならdefaultdictを使うことを考える
  • 既存のディクショナリに対して追加するなら、getを使う

Item 18: __missing__()メソッドを使って、キー依存のデフォルト値を作る方法を知っておけ

dictsetdefaultメソッドもdefaultdictも使えないとき、dictのサブクラスを作って__missing__メソッドを用意すればよい。

class Pictures(dict):
    def __missing__(self, key):
    value = open_picture(key)
    self[key] = value
    return value

pictures = Pictures()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()

上記の例では、pathpicturesに登録されていなかったときに__missing__メソッドが呼ばれます。open_picture(path)は、ここでは定義を省略しますが、指定パスのファイルをオープンしてハンドルを返す関数。オープンに失敗したら例外を上げます。

0
0
2

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
0
0