94
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

gloopsAdvent Calendar 2016

Day 11

これまでに世に送り出してしまって後から猛省したPythonのパターン

Last updated at Posted at 2016-12-11

こんにちは。インフラグループでソフトウェアエンジニアをしている微妙な立ち位置の鍋田です。いま11日の夜(22:00)なのでかなり焦ってこの記事を書いています。時間ギリギリまで見つけては書き続けようと思います。時間がないときほど落ち着いてキーボードを叩くんだ、ルーク。

リリース最優先

リリースは相当ヤバイ時以外は基本的に待ってくれないため、「えいやー!」と世に送り出してしまったケースは誰しも経験があることかと思います。また既存のコード含めたリファクタリングまで時間が取れなかったり、また実力不足なためにアレなコードを書いてしまったり・・・。そんな後に反省し、結局直したりしたパターンをいくつか紹介します。とにかく時間が無いので頑張って記事のスペースを埋めていこう!

オブジェクト生成時、__data__に放り込まない

sample1.py
class Api(object):
    def __init__(self, init_data):
        self.init_data = init_data
        self.prepare()
        
    def prepare(self)
        for k, v in self.init_data.items():
            self.__data__[k] = v

init_dataは辞書です。これ、Apiに食わせるinit_dataの中が変更されても,Apiの実装を変えなくて済むし、メタプログラミングっぽくてカッコいいと思いながらよく使ってたんですが、後から読むとinit_dataの内部実装を、Apiオブジェクトを用いて実装を行う他のクラスやメソッドも知っておかないといけなくて、かなり辛い。init_dataをコードで生成してたりするとホントやばくて読む気が失せます。

めんどうでも全てのkey, valueを定義した辞書を用意したり、それをラップするようなクラスを作ったほうが後から読みやすかったです。

sample2.py
class Api(object):
    def __init__(self, init_data):
        self.name = name
        self.a = init_data['a']
        self.b = init_data['b']
        ...

for, whileループの後のelseは使わない

Pythonは、ループのブロックの後ろにelseを置くことが出来ます。

sample3.py
for i in range(5):
    print('Count %d' % i)
else:
    print('ELSE BLOCK!!')

これ、なんとループが終了した直後に実行されるんですよね。全然else感が無い。更にこいつはbreakするとスキップされるので、もはやelseの意味がわからない。

sample4.py
>>> for i in range(5):
...     if i == 3:
...         break
...     print(i)
... else:
...     print('ELSE!') # unreachable!
0
1
2

じゃあ何のためにこいつあるのかなって思ったんですが、ループ中に条件を満たしたものに対して処理する場合には少し有効なんじゃないかなと。ただ、その場合はそのための関数を作った方が読みやすいです。elseって名前は誤解を招くと思うので使わないほうがいいです。

以下、走査して探していたものが見つかったかどうかの結果を返してあげるやつです。

sample5.py
# こっちのほうが良い
def power_up_if(users):
    found = False
    for user in users:
        if user == 42: # 探している条件が見つかり次第返す
            found = True
            break
    return user, found

users = [1,2,3,41,42,5]
uid, found = power_up_if(users)
if found:
    print('uid:', uid, 'is power up!')

デフォルト引数が動的に設定されるかのような書き方をしない

静的ではないキーワード引数を関数やメソッドのデフォルト値に使いたい場合が稀によくあると思います。

sample6.py
>>> def log_out(msg, when=datetime.datetime.now()):
...       print('%s %s ' % (when, msg))
... 
... log_out('Good bye!!')
... log_out('Good bye!!')
2016-12-11 21:21:20.928785 Good bye!!
2016-12-11 21:21:20.928785 Good bye!!

これはPythonをそれなりに書いていると必ず知る(踏む)ことになるだろう仕様だと思います。デフォルト引数は関数が定義された時の一度しか評価されません。つまりモジュールのロードが行われた時点でデフォルト引数の値は決定され、以降二度と評価されることはありません。私もこの仕様をキッチリ踏み、問題解決まで多大なるコストを払ったことがあります。回避策としては

sample7.py
>>> def log_out(msg, when=None):
...     """ タイムスタンプを含めたログを出力する
...
...     Args:
...         msg: ログメッセージ
...         when: ログ出力の日時なければ現在日時を設定する
...     """
...     when = datetime.datetime.now() if when is None else when
...     print('%s %s ' % (when, msg))

>>> log_out('hoge')
2016-12-11 21:28:58.293714 hoge

>>> log_out('bye')
2016-12-11 21:29:02.566320 bye

>>> log_out('bye', datetime.datetime(2016,11,1))
2016-11-01 00:00:00 bye

のように、デフォルト引数の値をNoneにしておき、関数内でwhen = datetime.datetime.now() if when is None else whenのイディオムによって決定するのが良いと思っています。あれば設定するし、なければこっちでキメるよ、といった感じです。

privateよりpublicを選ぶ

Pythonには自分が知る限りは絶対的なprivateは存在しません。

sample8.py
>>> class A(object):
...     def __aa(self):
...         print('private')
>>>
>>>
>>> a = A()
>>> a._A__aa()
private
>>> class A(object):
...     def __init__(self, a):
...         self.__a = a

>>>
>>> a = A(123)
>>> print(a.__dict__)
{'_A__a': 123}

なので、上記のようなコンパイラによる変換規則を知っている誰もがプライベート属性に対してアクセスすることが可能です。ただ、クラスの可視性としてはprivate, publicは存在しているので、実際に上記のようなアクセスが必要なケースは無いし、もしそうなら設計でミスっていると思っています。

では、なぜみんなpublic likeにアクセスすることが可能なのかというと、これはもう性善説に基づいているとしかおもえない。そういうところがPythonの超カワイイとこなのか思うんですけど、おそらくprivateであることによって得られるだろう利益よりも、publicであることによって得られるそれのほうが高いと思っているのではないでしょうか。これは正しいのかどうかわからないのですが、最近はprivateにする(__始まりの属性を与える)のケースは以下にほぼ限定しています。

  • 親クラスであること
  • 小クラスで名前空間が衝突しそうな属性であること
    • nameやval等
sample9.py
>>> class Student(object):
...     def __init__(self):
...         self.__name = "Anonymous"
...
...     def get_name(self):
...         return self.__name
...
... class ChildStudent(Student):
...     def __init__(self):
...         super().__init__()
...         self._name = "Child"
...
... c = ChildStudent()
... print(c.get_name(), c._name)
Anonymous Child

getやsetメソッドを定義しない

これは定義することが悪とかそういうことでは決してなくて、ゲッターやセッターを明示的に実装しなくてもいいじゃんってことです。

sample10.py
>>> class OtherLangStyle(object):
...     def __init__(self, val):
...         self._val = val
...
...     def get_val(self):
...         return self._val
...
...     def set_val(self, new_val):
...         self._val = new_val

>>> ols = OtherLangStyle(1)
>>> ols.get_val()
1
>>> ols.set_val(2)
>>> ols.get_val()
2
>>> ols.set_val(ols.get_val() + 2)
>>> ols.get_val()
4

Pythonっぽくない。カプセル化してる感はすごくあるのですが。。。僕は前述のように単純にpublicで実装しちゃっても何も問題ないと思っていて、常にpublicでガンガン実装してます。

sample11.py
>>> class PythonStyle(object):
...     def __init__(self, val):
...         self.val = val
... ps = PythonStyle(1)
... ps.val = 3
>>> ps.val
3
>>> ps.val += 4
>>> ps.val
7

この属性に対するフックが必要になった場合

もし後から、フックが必要になった場合、Pythonには@propertyデコレータという超便利なものがありまして、@property, setterを実装すれば済む話なのです。

sample12.py
>>> class Student(object):
...     def __init__(self, name, score):
...         self._name = name
...         self._score = score
...
...     @property
...     def score(self):
...         return self._score
...
...     @score.setter
...     def score(self, score):
...         before = self.score
...         self._score = score
...         self.notify_score_up(before)
...
...     @property
...     def name(self):
...         return self._name
...
...     def notify_score_up(self, before):
...         print('%s score is up. %d to %d.' %
...               (self.name, before, self.score))
...
... s = Student('nabetama', 0)
... s.score = 3
nabetama score is up. 0 to 3.

このようにsetterを実装することで、渡された値についてのチェックも行うことができますね!

デコレータを作る時はfunctools.wrapsを使う

Python書いてると誰しもがデコレータを一度は書いてみたくなります。関数の処理時間をザックリと知りたいときなど、

sample13.py
>>> def checktime(func):
...     @wraps(func)
...     def wrapper(*args, **kwargs):
...         import time
...         start = time.time()
...         res = func(*args, **kwargs)
...         end = time.time() - start
...         print('--------------------------------------------------')
...         print(end)
...         print('--------------------------------------------------')
...         return res
...     return wrapper
...
... @checktime
... def wait1():
...     time.sleep(1)
...     print('owata')
...
... wait1()
owata
... print(wait1)
<function wait1 at 0x1119a0d90>    # wait1とそのメタデータが返っている
--------------------------------------------------
1.003669023513794
--------------------------------------------------

のようなデコレータを用意しておいて、関数をwrapするようなことをやったりします。なぜ、functools.wrapsを使うのかというと、デコレータが返す値(つまり関数)は自分自身が呼び出し元メソッドだと解釈しないからです。上記のコードからwraps()を除いてみます。

sample14.py
>>> def checktime(func):
...     def wrapper(*args, **kwargs):
...         import time
...         start = time.time()
...         res = func(*args, **kwargs)
...         end = time.time() - start
...         print('--------------------------------------------------')
...         print(end)
...         print('--------------------------------------------------')
...         return res
...     return wrapper
...
... @checktime
... def wait1():
...     time.sleep(1)
...     print('owata')
...
... wait1()
... print(wait1)
owata
--------------------------------------------------
1.0004959106445312
--------------------------------------------------
<function checktime.<locals>.wrapper at 0x1118b5ae8>  # ・・・?

functools.wrapsを使うと、デコレータでラップされたとしても内部関数のメタデータを外部関数にコピーしてくれます。

組み込みのアルゴリズムやデータ構造を使う

辞書のソート

Pythonの辞書はソート済みではありません。ただ現実のプログラムでは辞書のキーの挿入順にソートしたいってことがままあります。そんな時はcollection.OrderedDictが便利です。

sample15.py
>>> names = ['s', 'p', 'l', 'g']
>>> o = OrderedDict()
>>> for name in names:
...     o[name] = 1

>>> o
OrderedDict([('s', 1), ('p', 1), ('l', 1), ('g', 1)])

>>> oo = dict()
>>> for name in names:
...     oo[name] = 1
>>> oo
{'p': 1, 'l': 1, 'g': 1, 's': 1}

デフォルト辞書

キーが存在しない場合に、事前に決定したデフォルト値を返してくれます。

sample16.py

>>> from collections import defaultdict
>>> rec = defaultdict(int)
>>> rec['ore']
0
>>> rec
defaultdict(<class 'int'>, {'ore': 0})

おまけ

最近、Sphinxでドキュメント書くの楽しいんですが、.git/hooks

sample.sh
#!/bin/sh
#
make html

しておくと毎回自動でhtmlのビルド走ってくれて幸せです。

まとめ

大して量もないだろうなと思いながら、今年書いたPythonのプロジェクトを読み返したりしていたら出てくるわ出てくるわ。まだまだ出てくるだろうし、しかも12日になってしまったのでこのへんで筆を置かせて頂きます。続きというか、他にもたくさんあるので、自分のブログなりで書こうと思います。

思いつき次第挙げていきましたが、この記事も後で反省することになるんだろうなと思いながら2016年に別れを告げようと思います。

94
70
4

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
94
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?