LoginSignup
9
12

More than 3 years have passed since last update.

pythonっぽいコードを書く

Posted at

YoutubeでRaymond Hettingerさん(python core developper)がpythonでよくあるミスと正しい書き方を解説する動画を見つけたので、まとめます。
動画では、主にpython2を例に挙げてますが(2013年の動画なので)、ここではできるだけpython3に変換しました。今ではすでに古い書き方になっている場合があるので適宜調べながら使ってください。

Loop

  • できるだけiteratorを使う

シンプルなloop

だめな例
for i in [0, 1, 2, 3, 4, 5]:
  print(i**2)

リスト全体をメモリにおいてしまう。

良い例
for i in range(6):
  print(i**2)

rangeがiteratorとして一つずつ生成するので、メモリを無駄に使わない。

python2ではrangeはリスト、xrangeがitertor
python3ではrangeがiterator(python2のxrangeの名前が変わった)

リストのloop

だめな例
colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)):
  print(colors[i])

良い例
colors = ['red', 'green', 'blue', 'yellow']

for color in colors:
  print(color)

下の書き方のほうが速い

逆向きのloop

だめな例
colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)-1, -1, -1):
  print(colors[i])

良い例
colors = ['red', 'green', 'blue', 'yellow']

for color in reversed(colors):
  print(color)

インデックスも得たい

だめな例
colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)):
  print(i, '--->', colors[i])

良い例
colors = ['red', 'green', 'blue', 'yellow']

for i, color in enumerate(colors):
  print(i, '--->', color)

2つのリストを同時にloop

だめな例
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']

n = min(len(names), len(colors))
for i in range(n):
  print(names[i], '--->'. colors[i]

良い例
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']

for name, color in zip(names, colors):
  print(name, '--->', color)

長さの違うiteratorがzipに入力されると、短い方に揃う。
長い方に揃えるためにはitertools.zip_longest

python2ではzipはリストを生成(izipがiterator)、
python3ではzipがiteratorを生成

カスタムソート

だめな例
colors = ['red', 'green', 'blue', 'yellow']

def compare_length(c1, c2):
  if len(c1) < len(c2):
    return -1
  elif len(c1) > len(c2):
    return 1
  else:
    return 0

print(sorted(colors, cmp=compare_length)

良い例
colors = ['red', 'green', 'blue', 'yellow']

print(sorted(colors, key=len))

keyによるソートで十分か?
十分でない例もあるがほとんどの場合は大丈夫。(SQLはソートを大量にやるが、keyによるソートでやっている)

sentinel valueでループを止める

だめな例
blocks = []
while True:
  block = f.read(32)
  if block == '':
    break
  blocks.append(block)

良い例
blocks = []

for block in iter(functool.partial(f.read, 32), ''):
  blocks.append(block)

partialが気持ち悪いが、iteratorとして扱えることのメリットが大きい
sentinel value(終了を示す値)はさけたほうが良い

条件によってループから抜ける

だめな例
def find(seq, target):
  found = False
  for i, value in enumerate(seq):
    if value == target:
      found = True
      break
  if not found:
    return -1
  return i

フラッグ(found)を使わなきゃいけない場合の例

良い例
def find(seq, target):
  for i, value in enumerate(seq):
    if value == target:
      break
  else:
    return -1
  return i

for内でbreakがなかったらelse文が実行される。elseではなくnobreakという名前にすべきだったと後悔しているそう。

Dictionary

Dictionaryのキーをループ

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
  print(k)

↑辞書に変更を加えるとおかしなことが起こる

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d.keys():
  if k.startswith('r'):
    del d[k]

d.keys()が事前にリストのコピーを作るので、辞書の変更が可能

キーと値をループ

だめな例
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
  print(k, '--->', d[k])

良い例
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k, v in d.items():
  print(k, '--->', v)

リストから辞書を作成

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']

d = dict(zip(names, colors))

リスト内の出現回数を数える

非効率な方法
colors = ['red', 'green', 'red', 'blue', 'green', 'red']

d = {}
for color in colors:
  if color in d:
    d[color] = 0
  d[color] += 1

正しいやり方
d = {}
for color in colors:
  d[color] = d.get(color, 0) + 1

最近の方法
d = defaultdict(int)
for color in colors:
  d[color] += 1

リストをグルーピングする

だめな例
names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie']

d = {}
for name in names:
  key = len(name)
  if key not in d:
    d[key] = []
  d[key].append(name)

正しいやり方
d = {}
for name in names:
  key = len(name)
  d.setdefault(key, []).append(name)

最近の方法
d = defaultdict(list)
for name in names:
  key = len(name)
  d[key].append(name)

getは辞書に代入はしない。setdefaultは代入する

複数の辞書をくっつける

だめな例
defaults = {'color': 'red', 'user': 'guest'}
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args([])
command_line_args = {k: v for k, v in vars(namespace).items() if v}

d = defaults.copy()
d.update(os.environ)
d.update(command_line_args)

辞書が大量にコピーされる

良い例
d = collections.ChainMap(command_line_args, os.environ, defaults)

コピーせずにもとの辞書をそのまま持つ

コードの可読性向上

関数のkeyword引数

だめな例
twitter_search('@obama', False, 20, True)

引数の意味がわからない

良い例
twitter_search('@obama', retweets=False, numtweets=20, popular=True)

NamedTuple

だめな例
> doctest.testmod()
(0, 4)

0,4の意味がわからない

良い例
> doctest.testmod()
TestResults(failed=0, attempted=4)

TestResults

TestResults = namedtuple('TestResults', ['failed', 'attempted'])

で作れる

tupleのunpacking

だめな例
p = 'Raymond', 'Hettinger', 0x30, 'python@example.com'

fname = p[0]
lname = p[1]
age = p[2]
email = p[3]

良い例
fname, lname, age, email = p

複数の状態を同時に更新する

だめな例
def fibonacci(n):
  x = 0
  y = 1
  for i in range(n):
    print(x)
    t = y
    y = x + y
    x = t

実行途中に状態が崩れている瞬間がある。行の順序を間違えやすい

良い例
def fibonacci(n):
  x, y = 0, 1
  for i in range(n):
    print(x)
    x, y = y, x+y

こっちのほうが人間の思考に近い。

効率化(高速化、省メモリ)

文字列の結合

だめな例
names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie']

s = names[0]
for name in names[1:]:
  s += ', ' + name

良い例
', '.join(names)

リストの更新

だめな例
names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie']

del names[0]
names.pop(0)
names.insert(0, 'mark')

遅い

良い例
names = deque(['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie'])

del names[0]
names.popleft()
names.appendleft('mark')

速い

decoratorとcontext manager

  • business logicとadministrative logicを分ける
  • コードがきれいになる
  • 正しく名前をつけないとグッチャになる
  • With great power comes with great responsibility

キャッシュ

だめな例
def web_lookup(url, saved={}):
  if url in saved:
    return saved[url]
  page = urlib.urlopen(url).read()
  saved[url] = page
  return page

良い例
@lru_cache()
def web_lookup(url):
  return urllib.urlopen(url).read()

business logicとadministrative logicが分離している

一時的なコンテクスト

だめな例
oldcontext = getcontext().copy()
getcontext().prec = 50
print(Decimal(355) / Decimal(113))
setcontext(oldcontext)

良い例
with localcontext(Context(prec=50)):
  print(Decimal(355) / Decimal(113))

ファイルの開閉

だめな例
f = open('data.txt')
try:
  data = f.read()
finally:
  f.close()

良い例
with open('data.txt') as f:
  data = f.read()

スレッドのロック

だめな例
lock = threading.Lock()
lock.acquire()
try:
  print('Critical section 1')
  print('Critical section 2')
finally:
  lock.release()

良い例
lock = threading.Lock()
with lock:
  print('Critical section 1')
  print('Critical section 2')

エラーの無視

だめな例
try:
  os.remove('somefile.tmp')
except OSError:
  pass

良い例
with ignored(OSError):
  os.remove('somefile.tmp')

標準出力を一時的に置き換え

だめな例
with open('help.txt', 'w') as f:
  oldstdout = sys.stdout
  sys.stdout = f
  try:
    help(pow)
  finally:
    sys.stdout = oldstdout

良い例
with open('help.txt', 'w') as f:
  with redirect_stdout(f):
    help(pow)

リストの内包記法

だめな例
result = []
for i in range(10):
  s = i**2
  result.append(a)
print(sum(result))

マシな例
print(sum([i**2 for i in range(10)])

良い例
print(sum(i**2 for i in range(10)))
9
12
1

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
9
12