Python 3.x のイディオム集です。
変数宣言
名前
組み込みデータ型
内包表記
条件式
制御フロー
例外
改行
関数
クラス
モジュール
ファイル
関係の強い複数の変数を定義するときはかっこ無しの tuple
表記を使うと、関係性がわかりやすくなります。
import datetime
year, month, day = 2018, 5, 27
...
date = datetime.date(year, month, day)
割り当てる必要があるがその後使用しない変数の名前には _
などを使います。
head, *_ = calculation()
values = [1, 3, 5]
values += [-1 for _ in range(10 - len(values))]
_
が特定の用途に使われる文脈(たとえば Django では _
が文字列を翻訳するための関数名としてよく使われます)では、衝突を避けるために __
などを使います。
list
・ tuple
等のコレクション系の値の名前には単語の複数形を使います。
# ○:
numbers = [1, 3, 5, ...]
urls = (
'https://www.google.co.jp',
'https://www.facebook.com',
'https://twitter.com',
)
menu_items = (
('/about/', '○○とは'),
('/services/', 'サービス'),
('/contact/', '問い合わせ'),
)
# ✕:
number = [...]
url = (
...,
)
menu_item = (
...,
)
for
ループや内包表記でその要素を取り出すときは単数形を使います。
スコープが狭く意味が明白な場合は 1 文字変数を使っても大丈夫です。
# ○:
for n in numbers:
...
for url in urls:
...
for i in menu_items:
...
dict
に存在するかどうかわからないキーで要素にアクセスする場合は例外処理を使用します。
# ○:
try:
value = adict[key]
except KeyError:
...
...
# ✕:
if key in adict:
value = adict[key]
...
このスタイルは「 EAFP 」スタイル( Easier to Ask for Forgiveness than Permission )と呼ばれたりします。
他方の if
文を使ったスタイルは「 LBYL 」スタイル( Look Before You Leap )と呼ばれます。
dict
のサブセットの取得には {}
を使った dict
の内包表記を使用します。
adict = {'tokyo': 'T', 'hokkaido': 'H', 'okinawa': 'O', 'kagoshima': 'K'}
targets = ['tokyo', 'kagoshima']
# ○:
subdict = {k: adict[k] for k in targets}
# {'tokyo': 'T', 'kagoshima': 'K'}
# ○:
subdict = {k: adict[k] for k in adict.keys() & targets}
# {'kagoshima': 'K', 'tokyo': 'T'}
複数の dict
を組み合わせて新たな dict
を作りたいときは dict
のアンパック演算を使用します。
dict1 = {...}
dict2 = {...}
dict3 = {...}
merged_dict = {**dict1, **dict2, **dict3}
キーの衝突が発生した場合は後に方に記述された dict
の値が残ります:
items1 = {'コーラ': 'ペプシ', 'ジンジャーエール': 'ウィルキンソン'}
items2 = {'ジンジャーエール': 'カナダドライ'}
merged = {**items1, **items2}
# => {'コーラ': 'ペプシ', 'ジンジャーエール': 'カナダドライ'}
dict
の数が不定の場合は dict
の内容表記( dict comprehension )を使うとシンプルに書けます。
many_dicts = [dict1, dict2, ..., dictn]
merged_dict = {k: v for d in many_dicts for k, v in d.items()}
list
のコピー(複製)には copy()
メソッドを使用します。
alist = [...]
# ○:
cloned_list = alist.copy()
Python 3.3 未満( 3.2 以下)のバージョンでは copy()
メソッドが提供されていないので次の書き方を使います。
alist = [...]
# ○:
cloned_list = alist[:]
list
の長さを変えるには次のようなやり方があります。
短くする:
original = [x ** 2 for x in range(11)]
# => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# 先頭を残して長さを 3 にする
original[:3]
# => [0, 1, 4]
# 末尾を残して長さを 3 にする
original[-3:]
# => [64, 81, 100]
長くする:
# 末尾に追加する
original = [x ** 2 for x in range(3)] # => [0, 1, 4]
length = 5
original += [None for _ in range(length - len(original))]
original
# => [0, 1, 4, None, None]
# 先頭に追加する
original = [x ** 2 for x in range(3)] # => [0, 1, 4]
length = 5
original = [None for _ in range(length - len(original))] + original
original
# => [None, None, 0, 1, 4]
規定の長さに揃える:
def normalize_length(alist, length, fallback):
"""list の長さを揃える"""
shortened = alist[:length]
return shortened + [fallback for _ in range(length - len(shortened))]
ネストされた list
(または tuple
)の要素を組み替えるには *
演算子(スプラット演算子)と zip()
関数を使うとシンプルに書けます。
original = [['ね', 'ねずみ'], ['うし', 'うし'], ['とら', 'とら'], ['う', 'うさぎ']]
shorts, fulls = [x for x in zip(*original)]
shorts
# => ('ね', 'うし', 'とら', 'う')
fulls
# => ('ねずみ', 'うし', 'とら', 'うさぎ')
ネストされた list
を行列と見るなら、これは「行列の行と列を入れ替える方法」ということができます。
複雑な内包表記を読みやすくするには、処理の一部をローカルの関数にして抽出する、小さなジェネレータ式に分割して記述する、等の方法があります。
class MyModel:
FIELD_LABEL_MAP = {...}
# 整理前: 複雑な内包表記
def get_target_field_labels(self):
return [
self.FIELD_LABEL_MAP.get(field.name, '')
for field in seld.model.get_fields()
if isinstance(field, models.CharField) and field.editable
]
# 整理後: 処理の一部をローカルの関数にして抽出する
def get_target_field_labels(self):
def label(field):
return self.FIELD_LABEL_MAP.get(field.name, '')
def is_target(field):
return isinstance(field, models.CharField) and field.editable
return [
label(field)
for field in seld.model.get_fields()
if is_target(field)
]
# 整理後: 小さなジェネレータ式に分割して記述する
def get_target_field_labels(self):
target_fields = (x for x in seld.model.get_fields()
if isinstance(x, models.CharField) and x.editable)
labels = (self.FIELD_LABEL_MAP.get(x.name) for x in target_fields)
return list(labels)
list
・ tuple
・ dict
等のコレクション系の値の非空チェックは名前を if
文に直接渡して行います。
links = magical_func_collecting_links(url)
# ○:
if links:
...
# ✕:
if len(links):
...
# ✕:
if len(links) > 0:
...
not
が含まれる条件式は人間が読んで読みやすい形(≒英語の語順に近い形)で書きます。
# ○:
if value is not None:
...
# ✕:
if not value is None:
...
# ○:
if key not in adict:
...
# ✕:
if not key in adict:
...
# ✕:
if not (key in adict):
...
Python 3.10 で match
~ case
構文が導入されたので、 Python 3.10 以降では match
~ case
構文を使います。
# ○:
def printer_factory(name):
match name:
case 'html':
return HtmlPrinter()
case 'pdf':
return PdfPrinter()
case 'toml':
return TomlPrinter()
case 'dsv':
return DsvPrinter()
case _:
raise ValueError(f'不正な name が指定されました: {name}。')
他の言語にある switch
/case
のようなことをしたい場合は dict
を使用します。
# ○:
def printer_factory(name):
printer_map = {
'html': HtmlPrinter,
'pdf': PdfPrinter,
'toml': TomlPrinter,
'dsv': DsvPrinter,
}
try:
return printer_map[name]()
except KeyError as e:
raise ValueError('不正な name が指定されました: {}。'.format(name))
iterable のループには原則インデックスを使いません。
urls = [
'https://www.google.co.jp',
'https://www.facebook.com',
'https://twitter.com',
]
# ○:
for url in urls:
print(url)
# ✕:
for i in range(len(urls)):
print(urls[i])
インデックスが必要な for
ループには enumerate()
を使用します。
animals = [
'子',
'丑',
'寅',
]
# ○:
for i, animal in enumerate(animals):
print('順位 {:02d}: {}'.format(i, animal))
# =>
# 順位 00: 子
# 順位 01: 丑
# 順位 02: 寅
# ○:
for i, animal in enumerate(animals, start=1):
print('順位 {:02d}: {}'.format(i, animal))
# =>
# 順位 01: 子
# 順位 02: 丑
# 順位 03: 寅
for
ループで複数の iterable を同時に回したい場合は zip
を使用します。
animals = ['猫', '馬', '河童']
animals_en = ['cat', 'horse', 'kappa']
# ○:
for animal, animal_en in zip(animals, animals_en):
print('{} は英語で {} です。'.format(animal, animal_en))
# ✕:
for i in range(min(len(animals), len(animals_en))):
print('{} は英語で {} です。'.format(animals[i], animals_en[i]))
2 つの iterable の長さが異なる場合、 zip()
は短い方の長さだけループを回します。
長い方の長さにあわせたい場合は itertools.zip_longest()
を使用します。
from itertools import zip_longest
# ○:
for animal, animal_en in zip_longest(animals, animals_en):
...
複数の例外をキャッチする場合は、「小さい例外」(例外クラスの継承ツリーにおいて子孫側の例外)を先に、「大きい例外」を後にキャッチします。
class CustomBaseError(Exception):
pass
class InvalidValueError(CustomBaseError):
pass
# ○:
try:
myfunc()
except InvalidValueError as e:
...
except CustomBaseError as e:
...
# ✕:
try:
myfunc()
except CustomBaseError as e:
...
except InvalidValueError as e:
...
コードをそのまま書くと 80 文字や 100 文字等の行長さ制限にひっかかる場合は、 ()
を使用したり式を分割したりして制限以内に収めます。
関数・メソッド呼び出し:
# ○:
class Student(models.Model):
year_in_school = models.CharField(
max_length=2,
choices=YEAR_IN_SCHOOL_CHOICES,
default=FRESHMAN,
db_index=True,
)
# ✕
class Student(models.Model):
year_in_school = models.CharField(max_length=2, choices=YEAR_IN_SCHOOL_CHOICES, default=FRESHMAN, db_index=True)
メソッドチェーン:
# ○:
class PublishedBookRelatedManager(models.Manager):
def get_queryset(self):
books_published = Book.published_objects.all()
return (
super()
.get_queryset()
.distinct()
.filter(book__in=books_published)
)
# ✕:
class PublishedBookRelatedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().distinct().filter(book__in=Book.published_objects.all())
# ✕:
class PublishedBookRelatedManager(models.Manager):
def get_queryset(self):
books_published = Book.published_objects.all()
return super().get_queryset() \
.distinct() \
.filter(book__in=books_published)
文字列:
class MyFormView(FormView):
def form_valid(self, form):
# この形で書かれた文字列リテラルは自動的に連結されるので `+` 演算子は不要です
message = (
'お問い合わせいただきありがとうございます。'
'ご入力いただいたメールアドレスに 3 営業日以内にご連絡差し上げます。'
'連絡が無い場合は大変お手数ですが再度お問い合わせいただきますようお願いいたします。'
)
messages.success(message)
return super().form_valid(form)
関数のデフォルト引数には原則 mutable な値は使いません。
# ○:
def merge_dicts(d1=None, d2=None):
d1 = d1 or {}
d2 = d2 or {}
d1.update(d2)
return d1
# ✕:
def merge_dicts(d1={}, d2={}):
d1.update(d2)
return d1
merge_dicts(d2={'a': 1})
# => {'a': 1}
merge_dicts(d2={'b': 2})
# => {'a': 1, 'b': 2}
merge_dict()
# => {'a': 1, 'b': 2}
引数の数が多い関数を使用するときは、実行側でキーワード指定で引数を渡します。
def easy_to_misuse_func(file, column, cond, coerce):
...
# ○:
easy_to_misuse_func(file=a, column=b, cond=c, coerce=d)
# ✕:
easy_to_misuse_func(a, b, c, d)
関数の定義側で引数の間に *,
を置くことで、キーワード指定での引数渡しを強制することもできます。
def easy_to_misuse_func(*, file, column, cond, coerce):
...
easy_to_misuse_func(a, b, c, d)
# =>
# TypeError: easy_to_misuse_func() takes 0 positional arguments but 4 were given
easy_to_misuse_func(file=a, column=b, cond=c, coerce=d)
# => OK
Parameters after “*” or “*identifier” are keyword-only parameters and may only be passed used keyword arguments.
関数やメソッドに複数の戻り値を持たせたい場合は tuple
を使用します。
import requests
# ○:
def get_page(url):
res = requests.get(url)
return res.ok, res.content
is_ok, content = get_page('https://www.yahoo.co.jp/')
tuple
ではまかなえなくなったら、 collections.namedtuple
や独自クラスのインスタンスを使います。
Python 3 にはオブジェクト指向言語で一般的な「インタフェース」が言語仕様として備わっていません。
「継承先クラスにメソッドの実装を矯正する」という意味でインタフェースに近い挙動を実現するには、標準モジュール abc
のクラス ABC
とデコレータ abc.abstractmethod
を使用します。
import abc
class ControllerInterface(abc.ABC):
@abc.abstractmethod
def dispatch(self, request):
pass
class HomePageController(ControllerInterface):
def dispatch(self, request):
...
class InvalidController(ControllerInterface):
# `dispatch()` が定義されていないのでインスタンス生成時に `TypeError` があがる
pass
abc.abstractmethod
に似たデコレータに以下のものがあります。
abc.abstractclassmethod
abc.abstractstaticmethod
abc.abstractmethod
のようにインスタンス生成時にチェックは走りませんが、例外 NotImplementedError
を使用する方法もあります:
from abc import ABC
class ControllerInterface(ABC):
def dispatch(self, request):
raise NotImplementedError()
class HomePageController(ControllerInterface):
def dispatch(self, request):
...
class InvalidController(ControllerInterface):
# `dispatch()` が定義されていないので `dispatch()` が呼び出されたときに `NotImplementedError` があがる
pass
ただし、いずれの場合でもクラス宣言時にエラーがあがるわけではないため、インタフェースが正しく実装されていることの確認にはテスト等を使用する必要があります。
自作のクラスでメソッドを連ねて呼び出したい場合は、メソッドが self
(または新しいインスタンス)を返すようにします。
# ○:
class Query:
def select(self, columns):
...
return self
def where(self, column, value):
...
return self
figure1 = (Query()
.select(['figure'])
.where('rhand', 'チョキ')
.where('lhand', 'グー'))
figure2 = (Query()
.select(['figure'])
.where('rhand', 'パー')
.where('lhand', 'パー'))
モジュールのバージョンによって場所が異なる関数やクラスの import
には try
except
を使用します。
# ○:
try:
from urllib.parse import urlparse
except:
from urlparse import urlparse
ファイルを利用する際は原則 with
文(コンテキストマネージャ)を使用します。
path = 'sample.txt'
# ○:
with open(path) as f:
...
# ✕:
f = open(path)
...
f.close()