View on GitHub

Today I Learned

Software Engineering Blog

Chapter 1: Pythonic Thinking

Pythonic思考

The Python community has come to use the adjective Pythonic to describe code that follows a particular style. The idioms of Python have emerged over time through experience using the language and working with others. This chapter covers the best way to do the most common things in Python.

  1. Know Which Version of Python You’re Using
  2. Follow the PEP 8 Style Guide
  3. Know the Differences Between bytes and str
  4. Prefer Interpolated F-Strings Over C-style Format Strings and str.format
  5. Write Helper Functions Instead of Complex Expressions
  6. Prefer Multiple Assignment Unpacking Over Indexing
  7. Prefer enumerate Over range
  8. Use zip to Process Iterators in Parallel
  9. Avoid else Blocks After for and while Loops
  10. Prevent Repetition with Assignment Expressions

1. Know Which Version of Python You’re Using

Pythonのバージョンを知っておく。

$ python --version
Python 3.11.4

$ python
>>> import sys
>>> print(sys.version_info)
sys.version_info(major=3, minor=11, micro=4, releaselevel='final', serial=0)
>>> print(sys.version)
3.11.4 (main, Jul 25 2023, 17:36:13) [Clang 14.0.3 (clang-1403.0.22.14.1)]

2. Follow the PEP 8 Style Guide

PEP8スタイルガイドに従う。

PyLintを使えば自動で確認ができる。

一部の例

3. Know the Differences Between bytes and str

bytesstr の違いを知っておく。

bytesは、符号なし8ビット値のASCIIエンコーディング。テキストエンコーディングを持たない。

strは、Unicodeコードポイントの文字列。バイナリエンコーディングを持たない。

# bytes
>>> a = b'h\x65llo'
>>> print(list(a))
[104, 101, 108, 108, 111]
>>> print(a)
b'hello'

# str
>>> a = 'a\u0300 propos'
>>> print(list(a))
['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']
>>> print(a)
à propos

bytesとstrはいくつかの演算子で一緒に使うことはできない。

# bytes + str
>>> b'one' + 'two'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't concat str to bytes

# bytes > str
>>> assert b'red' > 'blue'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'bytes' and 'str'

# bytes == str
>>> print(b'foo' == 'foo')
False

ファイルハンドルはデフォルトではUnicode文字列が必要

# bytesをread/writeするときは'b' modeが必要
>>> with open('data.bin', 'w') as f:
...   f.write(b'\xf1\xf2\xf3\xf4\xf5')
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: write() argument must be str, not bytes
>>> with open('data.bin', 'wb') as f:
...   f.write(b'\xf1\xf2\xf3\xf4\xf5')
...
5

>>> with open('data.bin', 'r') as f:
...   f.read()
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<frozen codecs>", line 322, in decode
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 0: invalid continuation byte
>>> with open('data.bin', 'rb') as f:
...   f.read()
...
b'\xf1\xf2\xf3\xf4\xf5'

Unicodeデータを読み書きするときは、システムのデフォルトのテキスト符号化に注意。 openencoding パラメータを明示的に指定する。

# システムのテキストエンコーディングを確認
>>> import locale
>>> print(locale.getpreferredencoding())

# 'cp1252'を指定
with open('data.bin', 'r', encoding='cp1252') as f:

4. Prefer Interpolated F-Strings Over C-style Format Strings and str.format

Cスタイルフォーマットや str.formatを使わず、f文字列(format string: フォーマット済み文字列)で埋め込む。

>>> key = 'my_var'
>>> value = 1.234

# Cスタイルフォーマット
>>> formatted = '%-10s = %.2f' % (key, value)
>>> print(formatted)
my_var     = 1.23

# str.format
>>> formatted = '{} = {}'.format(key, value)
>>> print(formatted)
my_var = 1.234

# f文字列
>>> formatted = f'{key} = {value}'
>>> print(formatted)
my_var = 1.234

5. Write Helper Functions Instead of Complex Expressions

複雑な式の代わりにHelper関数を使う。

>>> my_values
{'red': ['5'], 'blue': ['0'], 'green': ['']}

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

上記をHelper関数にする。

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

>>> get_first_int(my_values, 'green')
0

無理に多数のロジックを1行にまとめない。

6. Prefer Multiple Assignment Unpacking Over Indexing

インデックスではなくアンパックを使う。

Pythonでは1つの代入文で複数の値を代入できるアンパック構文がある。コレクションの要素を一括で変数に代入できる。

アンパックを使えば、インデックスを使わずに済むので見た目がスッキリする。

# アンパック構文
>>> item = ('Peanut butter', 'Jelly')
>>> first, second = item
>>> print(first, 'and', second)
Peanut butter and Jelly

snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)]
# インデックスの例
for i in range(len(snacks)):
	item = snacks[i]
	name = item[0]
	calories = item[1]
	print(f'#{i+1}: {name} has {calories} calories')

# アンパックの例
for rank, (name, calories) in enumerate(snacks, 1):
	print(f'#{rank}: {name} has {calories} calories')

7. Prefer enumerate Over range

rangeではなく enumerate を使う。

enumerate を使えば、イテレータでループをしながら、要素のインデックスを取り出すことができる。

flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']

range:

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
    print(f'{i + 1}: {flavor}')

enumerate:

for i, flavor in enumerate(flavor_list):
    print(f'{i + 1}: {flavor}')

8. Use zip to Process Iterators in Parallel

イテレータを並列に処理するにはzipを使う。

longest_name = None
max_count = 0
for name, count in zip(names, counts):
    if count > max_count:
        longest_name = name
        max_count = count

print(longest_name)
# Cecilia

9. Avoid else Blocks After for and while Loops

loop後の elseブロックは使わない。

elseブロックはループが終了された直後に実行されるが、ループ内で breakが実行されるとelseブロックを通らない。

直感的ではなく誤解を生みやすいので使用しない。代わりにヘルパー関数を書く。

for i in range(3):
    print('Loop', i)
else:
    print('Else block!')

Loop 0
Loop 1
Loop 2
Else block!

for i in range(3):
    print('Loop', i)
    if i == 1:
        break
else:
    print('Else block!')

Loop 0
Loop 1

10. Prevent Repetition with Assignment Expressions

代入式で繰り返しを防ぐ。

walrus演算子( := )は変数への値の代入と評価を行う。これで繰り返しをなくす。

代入と評価が分かれているコード:

count = fresh_fruit.get('lemon', 0)
if count:
    make_lemonade(count)
else:
    out_of_stock()

walrus演算子を使ったコード:

if count := fresh_fruit.get('lemon', 0):
    make_lemonade(count)
else:
    out_of_stock()