Skip to content

gh640/python-idioms-ja

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 

Repository files navigation

Python 3 イディオム集

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 では _ が文字列を翻訳するための関数名としてよく使われます)では、衝突を避けるために __ などを使います。

コレクション系の値の名前

listtuple 等のコレクション系の値の名前には単語の複数形を使います。

# ○:
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 のキーの存在チェック

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 のサブセットの取得には {} を使った 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 を作りたいときは 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 のコピー

list のコピー(複製)には copy() メソッドを使用します。

alist = [...]

# ○:
cloned_list = alist.copy()

Python 3.3 未満( 3.2 以下)のバージョンでは copy() メソッドが提供されていないので次の書き方を使います。

alist = [...]

# ○:
cloned_list = alist[:]

list の長さを変える

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 の要素の組み換え

ネストされた 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)

条件式

コレクション系の値の非空チェック

listtupledict 等のコレクション系の値の非空チェックは名前を if 文に直接渡して行います。

links = magical_func_collecting_links(url)

# ○:
if links:
    ...

# ✕:
if len(links):
    ...

# ✕:
if len(links) > 0:
    ...

not が含まれる条件式

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}。')

以前の説明( Python 3.9 以前)

他の言語にある 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))

for ループ

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 ループ

インデックスが必要な 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: 寅

複数の iterable の for ループ

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

モジュールのバージョンによって場所が異なる関数やクラスの 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()

About

🤹‍♀️ (Japanese) Python 3 のイディオム集です。

Topics

Resources

License

Stars

Watchers

Forks