LoginSignup
3
3

Effective Python 第2版 を自分なりにまとめてみる part2

Last updated at Posted at 2022-06-14

はじめに

こちらの書籍のまとめになります

Effective Python 第2版 ――Pythonプログラムを改良する90項目 (Brett Slatkin 著、黒川 利明 訳、石本 敦夫 技術監修)

  • 全てのパートをまとめているわけではありません

  • 個人的に難しくて理解できていなかったり腑に落ちていない箇所は省いています

  • もしくは新たな気づきは特にないなと感じたところも省略しています

  • コードに関しては書籍のものを丸々掲載するでなく、改変しています(その過程も個人的に有意義な時間でした)

  • そのような理由からこのブログでは多くの部分を削ってしまっています。オリジナルの書籍はかなり勉強になるなと思いました。興味ある人は是非読んでください。

  • このページでは本書の4〜5章をまとめています。他の章はこちらを参照ください

第四章 内包表記とジェネレータ

代入式を使い内包表記での繰り返しをなくす

  • ifが絡んでくると、式の判定と値の代入という同じ記述を重ねてすることになるので冗長になりがち、ミスも起こりやすい
get_items = ['Apple', 'Amazon', 'Netflix']

dic = {
    'Apple': 3,
    'Google': 8,
    'Amazon': 10,
    'Facebook': 5
}

# 代入式を使わない場合
ans = {
    key: dic.get(key, 0) - 4
    for key in get_items
    if (dic.get(key, 0) - 4) > 0
}
print(f'before: {ans}')

# 代入式を使う場合
ans = {
    key: value
    for key in get_items
    if (value := dic.get(key, 0) - 4) > 0
}
print(f'after: {ans}')
[out]
before: {'Amazon': 6}
after: {'Amazon': 6}

リストよりもジェネレーターを検討した方がいいこともある

  • メモリに全ての入出力を保持する必要がないので効率的
  • isliceで任意の長さの入力に対しても出力のシーケンスを生成できる
from itertools import islice


def get_period_pos(text):
    for idx, char in enumerate(text):
        if char == '':
            yield idx


text = ' 山路を登りながら、こう考えた。 智に働けば角が立つ。情に掉させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。'
it = get_period_pos(text)
print(next(it))
print(next(it))
print(next(it))

print()

# 任意の長さも可能
it = get_period_pos(text)   # Init
result = list(islice(it, 1, 3))
print(result)
[out]
15
26
37

[26, 37]

引数にイテレータを使うと変な挙動になることがある

## イテレーターが結果を一度しか生成しないので変な挙動になる
def sample_func(numbers):
    total = sum(numbers)
    print('total', total)
    for i, value in enumerate(numbers):
        print(i, value / total)


def get_numbers(n):
    for i in range(n):
        yield i


# 引数にイテレーターを渡した場合
numbers = get_numbers(3)
sample_func(numbers)

print()

# 引数にコンテナを渡した場合
numbers = get_numbers(3)
sample_func(list(numbers))
[out]
total 3

total 3
0 0.0
1 0.3333333333333333
2 0.6666666666666666
  • より安全なのは型チェックをしてしまう方法
from collections.abc import Iterator


def sample_func(numbers):
    if isinstance(numbers, Iterator):
        raise TypeError

    total = sum(numbers)
    print('total', total)
    for i, value in enumerate(numbers):
        print(i, value / total)

メモリを大量に消費する場合内包表記にはジェネレータ式を考える

# 長い配列だとメモリを食う
sample_list = [i / 3 for i in range(10**7)]

# ジェネレータだとメモリにやさしい
sample_itr = (i / 3 for i in range(10**7))

print('large', sample_list.__sizeof__())
print('small', sample_itr.__sizeof__())
[out]
large 89095144
small 96
  • 連鎖ジェネレータはドミノ倒し式のように処理される
    • つまり片方の処理が進むともう片方も進む
  • 連鎖ジェネレータはPythonでは極めて高速に処理される
sample_itr2 = (i / 2 + 1 for i in sample_itr)

print(next(sample_itr2))   # (0 / 3) / 2 + 1
print(next(sample_itr))    # (1 / 3)
print(next(sample_itr2))   # (2 / 3) / 2 + 1
print(next(sample_itr))    # (3 / 3)
[out]
1.0
0.3333333333333333
1.3333333333333333
1.0

itertoolsにある便利な関数たち

  • takewhile
    • 要素がFalseを返すまでイテレータの要素を返す
  • dropwhile
    • 要素がFalseを返すまでイテレータの要素をスキップする
import itertools


values = list(range(0, 11))

it1 = itertools.takewhile(lambda x: x < 5, values)
it2 = itertools.dropwhile(lambda x: x < 5, values)

print(list(it1))
print(list(it2))
[out]
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9, 10]
  • この辺はAtCoderとかで定番
    • accumulate
      • 累積を返す
    • product
      • 直積を返す
      • forのネストが深くなりすぎるのを防ぐ
    • permutations
      • 要は順列 $mPn$
    • combinations
      • 要は組み合わせ $mCn$
    • combinations_with_replacement
      • 要は重複組み合わせ $mHn$
import itertools


red = [0, 1, 2]
white = [0, 1]

for x in itertools.accumulate(red):
    print(x, end=' ')   # -> 0 1 3

for x, y in itertools.product(red, white):
    print((x, y), end=' ')   # -> (0, 0) (0, 1) (1, 0) (1, 1) (2, 0) (2, 1)

for x, y in itertools.permutations(red, 2):
    print((x, y), end=' ')   # -> (0, 1) (0, 2) (1, 0) (1, 2) (2, 0) (2, 1)

for x, y in itertools.combinations(red, 2):
    print((x, y), end=' ')   # -> (0, 1) (0, 2) (1, 2)

for x, y in itertools.combinations_with_replacement(red, 2):
    print((x, y), end=' ')   # -> (0, 0) (0, 1) (0, 2) (1, 1) (1, 2) (2, 2)

第五章 クラスと継承

プロパティが入れ子になって複雑なクラスは分割することを考える

  • 辞書やリスト、タプルなどのコンテナのネストが深いとややこしくなる
  • 各々を計算をするときにどうしても複雑なコードになりがち
  • そのようなときはクラスに一つの巨大クラスでなく、細かく分割して管理することを考える
# Before
class WeightedGradebook:
    """クラスに属する生徒のテスト成績管理簿
    """
    def __init__(self):
        self._grades = {}   # 親

    def add_stutent(self, name):
        """生徒の追加を行う
        name: 生徒の名前

        self._grades['Ted'] = {[]}
        """

    def report_grade(self, name, subject, score, weight):
        """生徒のテスト成績をupdateする
        name: 生徒の名前
        subject: 教科名
        score: 点数
        weight: 点数に対する重み

        self._grades['Ted'] = {
            'math': [(score, weight), (score, weigt), ...].
            'physics': [(score, weight), (score, weigt), ...],
        }
        """

    def get_avarage_grade(self, name):
        """特定の生徒の平均点を求める
        name: 生徒の名前
        """
        by_subject = self._grade[name]

        score_sum, score_cnt = 0, 0
        for subject, scores in by_subject.items():
            subject_avg, total_weight = 0, 0
            for score, weight in scores:
                ...
                ...
                ...


# After
class Subject:
    """一つの科目の点数群を管理するクラス
    """
    def __init__(self):
        self._grade = []

    ...
    ...
    ...


class Student:
    """1人の生徒の科目群を管理するクラス
    """
    def __init__(self):
        self._subjexts = defaultdict(Subject)

    ...
    ...
    ...


class WeightedGradebook:
    """クラスに属する生徒のテスト成績管理簿
    """
    def __init__(self):
        self._stutendes = defaultdict(Student)

    ...
    ...
    ...

コンポーネント間の単純なインターフェースは関数やクラスの__call__メソッドで済ます

  • Pythonでは関数もファーストクラス(第一級オブジェクト)であり、下記のような形で引数として使える
scores = ['Taro', 'Hanako', 'Takashi', 'Jiro', 'Ai']

scores.sort()
print(scores)

scores.sort(key=len)
print(scores)

scores.sort(key=lambda x: ord(x[0]))
print(scores)
[out]
['Ai', 'Hanako', 'Jiro', 'Takashi', 'Taro']
['Ai', 'Jiro', 'Taro', 'Hanako', 'Takashi']
['Ai', 'Hanako', 'Jiro', 'Taro', 'Takashi']
  • 以下の例では関数log_missingをdefaultdictに対して使用することでログ出力と本質的な処理を切り出した例である
    • defaultdictとは? → 辞書に対象のkeyが存在しない場合、初期化時に特定の関数を実行する
from collections import defaultdict


def log_missing():
    """
    辞書にkeyの欠損があった場合に呼び出される
    欠損情報を出力しデフォルト値として0を返す
    """
    print('Add!!')
    return 0

current = {
    'Apple': 5,
    'Amazon': 3,
    'Google': 2,
}
my_bag = defaultdict(log_missing, current)
print(f'Before: {dict(my_bag)}')
print()

add = {
    'Google': 5,
    'Facebook': 3,
}
for k, v in add.items():
    my_bag[k] += v
print(f'After {dict(my_bag)}')
[out]
Before: {'Apple': 5, 'Amazon': 3, 'Google': 2}

Add!!
After {'Apple': 5, 'Amazon': 3, 'Google': 7, 'Facebook': 3}
  • Pythonクラスの__call__メソッドを使用するとクラスのインスタンスを通常の関数と同じ形で呼び出すことが可能になる
class CountMissing:
    def __init__(self):
        self.added = 0

    def __call__(self):
        """
        Callされるたびにaddedに追加すると共に、default値として0を返す
        """
        self.added += 1
        print('Add!!')
        return 0


current = {
    'Apple': 5,
    'Amazon': 3,
    'Google': 2,
}
counter = CountMissing()
my_bag = defaultdict(counter, current)
print(f'Before: {dict(my_bag)}')
print()

add = {
    'Google': 5,
    'Facebook': 3,
}
for k, v in add.items():
    my_bag[k] += v
print(f'After: {dict(my_bag)}')
print(counter.added)
[out]
Before: {'Apple': 5, 'Amazon': 3, 'Google': 2}

Add!!
After: {'Apple': 5, 'Amazon': 3, 'Google': 7, 'Facebook': 3}
1

スーパークラスの初期化にはsuperを使う

  • supurを使わないやり方には2つの問題がある
    • 基底クラスの呼び出し順序がよくわからないことになりがち
    • ダイヤモンド継承において、それぞれ共通のスーパークラスを持っている場合に共通クラスの__init__メソッドが何度も呼び出されてしまう
  • ただしsuperを使う際も基底クラスの呼び出し順には注意が必要
class A:
    def introduce(self):
        print('A')

class B(A):
    def introduce(self):
        super().introduce()
        print('B')

class C(A):
    def introduce(self):
        super().introduce()
        print('C')

class D1(B, C):
    def introduce(self):
        super().introduce()
        print('D1')

class D2(C, B):
    def introduce(self):
        super().introduce()
        print('D2')
print(D1.mro())
print(D1().introduce())
[out]
[<class '__main__.D1'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
A
C
B
D1
None
print(D2.mro())
print(D2().introduce())
[out]
[<class '__main__.D2'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
A
B
C
D2
None

プライベート属性について

  • Pythonにおけるプライベート属性は強制的なものではない
  • Pythonのモットーは「みんな大人なんだから」、プライベートとして定義してるのだから無茶して変なことしないでくれ、空気読んでくれよってこと
  • 絶対にプライベートにアクセスできないようにすると面倒なこともある、なのでそれを犠牲にしてまで強制する必要ない。「みんな大人なんだから」
  • JavaやC++など他言語ではプライベート属性が積極的に使われる傾向があるが、Pythonはむしろその逆でPublicがより良いとされることが多い
  • プライベート属性については、手前味噌になってしまいますが2019年当時のQiita記事でも触れていました。この時と比べると自分の知識も少しずつ増えていることを感じられて少し嬉しかったです
class SuperClass:
    """superクラス
    """
    def __init__(self):
        self.__name = 'Qiita'


super_class = SuperClass()
print(super_class.__name)   # AttributeErrorでアクセスできない
  • 上記のような形でアクセスできなくとも下記のようなやり方であればアクセス可能です
# 実は_クラス名__プライベート属性 で定義されてる
print(super_class.__dict__)   # -> {'_SuperClass__name': 'Qiita'}

# なので直接参照も可能
print(super_class._SuperClass__name)   # -> Qiita
# もしくはこんな感じで値を参照するメソッドを追加するとかでも見れる
class SuperClass:
    """superクラス
    """
    def __init__(self):
        self.__name = 'Qiita'

    def get_name(self):
        return self.__name


super_class = SuperClass()
print(super_class.get_name())   # -> Qiita
  • またサブクラスは下記のように一見するとスーパークラスのプライベート属性にアクセスできなそうだが
class SubClass(SuperClass):
    def __init__(self):
        super().__init__()

    def get_name(self):
        return self.__name


sub = SubClass()
print(sub.get_name())   # AttributeErrorでアクセスできない
  • これはそもそものプライベート属性が_SubClass__nameでなく_SuperClass__nameで定義されていることによるエラーである
print(sub.__dict__)   # -> {'_SuperClass__name': 'Qiita'}
  • なのでこうしてしまえばアクセス可能
print(sub._SuperClass__name)   # -> Qiita
  • 結局色々ややこしくなるので可能であればSuperClassでプライベート属性を無闇に使い過ぎるのはやめた方がいい
  • どうしても使う時の指針だが、SubClassで定義されることが予想される属性と名前衝突が起こりそうという時に使うイメージ
  • またプライベート属性の書き換えも普通に可能
sub._SuperClass__name = 'Hatena'
print(sub._SuperClass__name)   # -> Hatena

おまけ

effective pythonで出ていたテーマではないのですが(getter setterの項でそれっぽい話は出てくると言えば出てくる)、プライベート属性に関することで以下も。これは2019年に書いた2019年当時のQiita記事の再掲です

  • 先ほどのアンダースコア2つと同様、よくみるルールとしてアンダースコア1つがあります
  • ただしこの方法、実はプライベートでもなんでもありません
  • そのまま工夫なしで使ってしまうと、普通に外部からアクセス可能になってしまいます。(開発者的には書き換えして欲しくないという意図があるので要注意)
class User3(User):
    def __init__(self, name=None, flag=True):
        super().__init__(name)
        self._flag = flag

user3 = User3(name='qiita')
print('user3 flag: ', user3._flag)   # user3 flag:  True

user3._flag = False
print('user3 flag: ', user3._flag)   # user3 flag:  False
  • 下記のようにデコレータとセットでプロパティを定義することで外部から参照可能だが、書き換えは不可というプロパティとしてflagを定義することが可能です
class User3(User):
    def __init__(self, name=None, flag=True):
        super().__init__(name)
        self._flag = flag

    @property
    def flag(self):
        return self._flag

user3 = User3(name='qiita')
print('user3 flag: ', user3.flag)   # user3 flag:  True

user3.flag = False   # AttributeError: can't set attribute
  • ただし、上記の場合も.flagではなく、._flagで呼び出すと、普通に書き換えできてしますので注意が必要です
  • また @プロパティ名.setterを利用することで、参照だけでなく、書き換えも可能になります
  • この場合ある条件が満たされた場合のみ書き換え可能としてifなどと一緒に利用されることが多いです
  • 下記コードではpswdプロパティが条件と合致する時だけ、flagプロパティが書き換え可能となっています
class User3(User):
    def __init__(self, name=None, flag=True, pswd=None):
        super().__init__(name)
        self._flag = flag
        self.pswd = pswd

    @property
    def flag(self):
        return self._flag

    @flag.setter
    def flag(self, new_flag):
        if self.pswd=='777':
            self._flag = new_flag
        else:
            pass

user3 = User3(name='qiita', flag=True, pswd='222')
user3.flag = False
print('user3 flag: ', user3.flag)   # user3 flag:  True  ->書き換わっていない
  • 上記例ではpassを使っていますが、例外処理を使ってエラーを起こさせる場合もあります
  • 上記例の場合、書き換えたつもりが書き換わっていないことによるバグの発生可能性について考慮する必要があります
3
3
0

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