2018/11/10

Python Tips: unittest で例外のテストをしたい

今回は Python の標準ライブラリのひとつ unittest で例外関係の部分をテストする方法についてまとめてみました。

対象の Python のバージョンは Python 3.7 です。

import unittest

例外を含むパターンの説明に入る前に、かんたんに unittest の基本の使い方を見ておきましょう。 unittest の基本的な書き方は次のとおりです。

test_sample.py:
import unittest


class TestRange(unittest.TestCase):
    def setUp(self):
        super().setUp()

    def tearDown(self):
        super().tearDown()

    def test_length(self):
        self.assertEqual(len(range(5)), 5)

    def test_min_max(self):
        self.assertIn(0, range(10))
        self.assertIn(9, range(10))
        self.assertNotIn(10, range(10))

unittest.TestCase を継承したクラス TestXXX を作成し、名前が test で始まる名前のメソッドを作成しその中にテストケースを書きます。すると、各メソッドが独立したテストケースとなります。

setUp() は各テストケースの前に、 tearDown() は各テストケースの後に実行されます。

テストケースが書かれたファイルを実行するにはコマンド python -m unittest を使用します。

python -m unittest test_sample.py

上の test_sample.py に対して実行すると次のような出力が表示されます。

python -m unittest test_sample.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

デフォルトではテストケースの成功 1 件につき 1 つの . が出力されます。

-v オプションをつけることで、どのテストが実行されたかを確認することができます。

python -m unittest -v test_sample.py
test_length (test_exception.TestRange) ... ok
test_min_max (test_exception.TestRange) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

クラス単位・メソッド単位で絞り込んで個別のテストだけを実行することも可能です。

python -m unittest test_exception.TestRange.test_length
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

名前に特定の文字列を含むテストだけを絞って実行することなんかもできます。

python -m unittest test_exception -k min_max
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

本当にざっくりとだけですが、テストの書き方と実行方法を押さえました(以下のコードを手元で実行したい方等はご参考にしてください)。続いて本題である例外のテストの仕方を見ていきます。

1. 特定の例外があがることをテストする


特定の例外があがることをテストしたいときは unittest.TestCaseassertRaises() メソッドを使用します。 assertRaises() はコンテキストマネージャとして作られているので、 with キーワードとともに使用します。

import unittest


def periodic_table(sign):
    amap = {
        'H': '水', 'He': '兵', 
        'Li': 'リー', 'Be': 'ベ',
        'B': 'ぼ', 'C': 'く', 
        'N': 'の', 'O': 'ぉ', 
        'F': 'ふ', 'Ne': 'ね',
    }
    try:
        return amap[sign]
    except KeyError:
        raise ValueError('不正な元素記号が指定されました: {} 。'.format(sign))


class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        with self.assertRaises(ValueError):
            periodic_table('Ubn')

テスト test_invalid_key() では、関数 periodic_table() に不正な値を与えると例外が上がることをチェックしています。

このテストを上の python -m unittest で走らせると無事成功することが確認できます。

ちなみに、 test_invalid_key() は次のように書いても( ValueErrorException に変更しても)成功します。

def test_invalid_key(self):
        with self.assertRaises(Exception):
            periodic_table('Ubn')

なぜなら、 assertRaises() は通常の tryexcept の仕組みと同じように、例外クラスの継承ツリーにおける親クラスは子孫の例外クラスも捕捉するようになっているためです。

つまり、 assertRaises()Exception のような大きめの例外を渡すと、想定と異なる例外があがったのにそれをキャッチし、本来失敗すべきケースを成功とみなしてしまうことがあります。そのため、 assertRaises() には適切な粒度の例外を渡さなくてはなりません。

2. 例外のメッセージも含めてテストする


例外のメッセージも含めてテストするには assertRaises()msg 引数を使用します(ちなみに、 msg 引数は Python 3.3 で追加されたので、 Python 3.2 以前では使用することができません)。

class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        with self.assertRaises(ValueError, msg='不正な元素記号が選択され'):
            periodic_table('Ubn')

msg は完全なメッセージである必要はありません。 msg が例外のメッセージの中に含まれていればテストはパスしたものとみなされます。

例外をメッセージも含めてテストする方法は他にもあります。そのひとつは、コンテキストマネージャ変数を使う方法です。

class TestSillyCase(unittest.TestCase):
    def test_silly_check(self):
        with self.assertRaises(NotImplementedError) as cm:
            raise NotImplementedError('実装してから呼ぶのじゃ')

        self.assertIsInstance(cm.exception, NotImplementedError)
        self.assertEqual(cm.exception.args[0], '実装してから呼ぶのじゃ')

コンテキストマネージャ変数(この場合は cm )にはアトリビュート exception がありキャッチされた例外がその中に格納されています。

さらにもうひとつの方法として、 assertRaises() の代わりに assertRaisesRegex() メソッドを使う方法もあります。

class TestShop(unittest.TestCase):
    def test_lack_of_gold(self):
        with self.assertRaisesRegex(ValueError, '^手持ちは \d+ ゴールドです。 \w+ は買えません。$'):
            raise ValueError('手持ちは 15 ゴールドです。 ひのきの棒 は買えません。')

assertRaisesRegex() はその名のとおり正規表現でメッセージのチェックを行う機能を提供します。

・・・というわけで、 unittest で例外を含むパターンをテストする方法についてかんたんにではありますがまとめてみました。

ちなみに、 assertRaises()assertRaisesRegex() ともに、コンテキストマネージャではなく通常のメソッドとして利用することもできます。

class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        self.assertRaises(ValueError, periodic_table, 'Ubn')

この使い方をする場合は、第 1 引数に想定される例外クラスを、第 2 引数に対象の callable を、第 3 引数以降は第 2 引数の callable に渡したい引数を渡します。結果はコンテキストマネージャパターンと同じになります。

以上です。

参考

2018/10/21

ライブラリ: dataclasses

今回は Python 3.7 で標準ライブラリに追加された dataclasses について見ていきたいと思います。

import dataclasses

目次


  • dataclasses とは
  • dataclasses の使い方
  • dataclasses の使いどころ

dataclasses とは


dataclasses は、独自のクラスを定義するときに定型的な特殊メソッド( special methods )( __init__() 等)の記述を省略できる機能を提供するモジュールです。 Python 3.7 から標準ライブラリとして Python 本体に同梱されるようになりました。

代表的な機能は次の 2 つです。

  • dataclasses.dataclass: クラスのデコレータ。対象のクラスのクラス変数をもとに特殊メソッドを追加する。
  • dataclasses.field: クラス変数に使う。 dataclass とあわせて使うことでアトリビュートごとの挙動を細かく設定できる。

dataclasses の使い方


まずインストールについてですが、 dataclasses は Python 3.7 以降に同梱されているためインストールする必要はありません。

代表的な機能である次の 2 つから見ていきましょう。

  • dataclasses.dataclass
  • dataclasses.field

dataclasses.dataclass

dataclass はクラスに対して使うデコレータです。

dataclass でデコレートされたクラスには、 __init__()__repr__()__eq__() の 3 つの特殊メソッドが自動的に追加されます。

from dataclasses import dataclass

TAX_RATE = 0.1


@dataclass
class Book:
    title: str
    isbn13: str
    price: int

    def price_with_tax(self):
        return int(self.price * (1 + TAX_RATE))


# __init__() が自動的に定義されている:
book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)

# クラス変数と同名のアトリビュートが利用できる:
print(book1.title)
# => '小僧の神様・城の崎にて'
print(book1.isbn13)
# => '978-4101030050'
print(book1.price)
# => 562
print(book1.price_with_tax())
# => 618

# __repr__() も自動的に定義されている:
print(repr(book1))
# => Book(title='小僧の神様・城の崎にて', isbn13='978-4101030050', price=562)

# __repr__() の出力は eval() すると新しいインスタンスが作れる形になっている:
book2 = eval(repr(book1))

# __eq__() も自動的に定義されている:
print(book1 == book2)
# => True

dataclass の最もシンプルな使い方は上のサンプルのように @dataclass という形でデコレートするものですが、 @dataclass にはオプションを渡すこともできます。オプションでは、特殊メソッドの追加の有無等を指定することができます。

from dataclasses import dataclass


@dataclass(repr=False, eq=False)
class Book:
    title: str
    isbn13: str
    price: int


# __init__() が自動的に定義されている:
book1 = Book('暗夜行路', '978-4101030074', 961)

# repr=False により __repr__() は定義されていないので object のデフォルトの出力になる:
print(repr(book1))
# => <__main__.Book object at 0x106fa94a8>

book2 = Book('暗夜行路', '978-4101030074', 961)

# eq=False により __eq__() は自動追加されていないので、デフォルトの object.__eq__() で判定される:
print(book1 == book2)
# => False

ちなみに、 dataclass デコレータが @dataclass でも @dataclass() でも同じような使い方ができるのは、 dataclass がそのように(シンプルに使えるように)工夫して作られているからであり、一般のデコレータはこのようには行きません。

dataclass デコレータがアトリビュート化する要素として認識するのは タイプアノテーションのあるクラス変数だけ です。タイプアノテーションの無いクラス変数は無視されるので、そこは注意が必要です。次のコードではタイプアノテーションの無い isbn13 は自動追加されません。

from dataclasses import dataclass


@dataclass
class Book:
    title: str
    isbn13 = ''
    price: int


# isbn13 は dataclass には無視される:
book1 = Book('和解', 464)
print(repr(book1))
# => Book(title='和解', price=464)

また、 dataclass は「タイプアノテーションがあるかどうか」という点だけを見ており、具体的な型が何なのかは見ていないという点もポイントです。つまり、 int と定義されている変数に str 型の値を代入しても何も問題ありません(バリデーション等は特に追加されません)。

# 次のコードも特に問題なし
book1 = Book('和解', '464')

尚、 Python 3.7 における dataclass の宣言部は次のようになっています。

@dataclasses.dataclass(
    *, 
    init=True, 
    repr=True, 
    eq=True, 
    order=False, 
    unsafe_hash=False, 
    frozen=False)

引数がすべて *, の後に並んでいるので、引数はすべてキーワードを明示的に指定した渡し方をする必要があります。

dataclassesdataclass でデコレートされたクラスとそのインスタンスに対して使える関数をいくつか用意しています。次のコードでは fields()asdict()astuple() の 3 つを試しています。

from dataclasses import dataclass, fields, asdict, astuple


@dataclass
class Book:
    title: str
    isbn13: str
    price: int


# fields() 関数はそのクラスあるいはインスタンスのフィールドの一覧をタプルで返す:
# タプルの各要素は dataclasses.Field クラスのインスタンス
print(type(fields(Book)))
print([x.name for x in fields(Book)])
# => ['title', 'isbn13', 'price']

book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)
print([x.name for x in fields(book1)])
# => ['title', 'isbn13', 'price']

# asdict() 関数は辞書を生成する
print(asdict(book1))
# => {'title': '小僧の神様・城の崎にて', 'isbn13': '978-4101030050', 'price': 562}

# astuple() 関数は tuple を生成する
print(astuple(book1))
# => ('小僧の神様・城の崎にて', '978-4101030050', 562)

dataclasses のアイデアの概要は以上でおおよそ押さえられたかと思いますが、実際に利用する場合は詳細を確認してから使うようにしてください。


つづいて field を見てみましょう。

dataclasses.field

fielddataclass でデコレートされたクラスのアトリビュートの挙動を細かく指定するためのものです。

例えば、アトリビュートのデフォルト値に list を指定したいときには field が次のように使えます。

from dataclasses import dataclass, field
from typing import List


@dataclass
class Film:
    title: str = ''
    cast: List[str] = field(default_factory=list)


f1 = Film('ジュラシック・ワールド', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f1))
# => Film(title='ジュラシック・ワールド', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])


f2 = Film('ジュラシック・ワールド 炎の王国', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f2))
# => Film(title='ジュラシック・ワールド 炎の王国', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])

また、次のように書くと、一部の要素( subtitle )を __eq__() での比較の対象外にすることができます。

from dataclasses import dataclass, field


@dataclass
class Film:
    title: str
    subtitle: str = field(compare=False)


f1 = Film('007', 'ワールド・イズ・ノット・イナフ')
f2 = Film('007', '慰めの報酬')
f3 = Film('007', 'スカイフォール')

# __eq__() には subtitle が使われないので、 == 演算で title のみが比較された結果、 True が返る
print(f1 == f2 == f3)
# => True

field を使うとその他さまざまなカスタマイズを行うことができます。 Python 3.7 における field の宣言部は次のようになっているので、実際に利用するときは事前に詳細を確認するようにしてください。

dataclasses.field(
    *, 
    default=MISSING, 
    default_factory=MISSING, 
    repr=True, 
    hash=None, 
    init=True, 
    compare=True, 
    metadata=None)


かんたんにですが、以上 dataclasses の基本的な使い方についてでした。

最後に使いどころについて。

dataclasses の使いどころ


私の認識では dataclassesnamedtuple の進化版的な位置づけのものです。

使い捨てのスクリプト作成等の場合は、単純に特殊メソッドの作成を省略したいときに気軽に使うとよさそうです。規模が大きめの開発では、アーキテクチャの境界をまたぐ情報の受け渡しに使用するシンプルなデータ構造やプレーンなドメインモデルクラス等で使うと便利かと思います。

dataclass デコレータが追加してくれる特殊メソッドのうち、 __init__()__eq__() は(自分で定義したいので)不要な場合も多そうですが、 __repr__() は多くの場合そのまま使えて便利なのではないでしょうか。

dataclasses の機能を濫用すると「 Explicit is better than implicit. 」に反するコードになりそうな気もしますが、今後 dataclasses がクラス定義における定番と言われるくらいよく使われるようになれば、( Python に不慣れな人にはわかりづらいけれど)ある程度経験のある Pythonista には implicit というほどのものではなくなってくる、のかも、しれません。

というわけで、 Python 3.7 で導入された dataclasses の説明でした。

同じく「特殊メソッドの自動定義」で人気のライブラリ attrs との違いや dataclasses が標準ライブラリに追加された経緯等について興味のある方には以下のページ等が参考になるかと思います。


関連記事


参考

2018/09/25

Python Tips: functools.reduce() を活用したい

Python が標準で提供する関数のひとつに functoolsreduce() があります。

from functools import reduce

reduce() は一見使いどころがわかりづらいのですが、活用できるようになるととても便利な関数です。今回はそんな reduce() について、 使いどころと実践的なサンプル をご紹介してみたいと思います。

使いどころ


reduce() は「 シーケンス → ひとつの値 」という処理をしたいときに利用できる関数です。関数でいえば「引数にシーケンスを受け取り、何らかの処理をして、戻り値をひとつだけ返す」というふるまいの関数を書きたいときに使えます。

このパターンは標準的なビジネスロジックの中にたくさん登場します。いくつか例をあげてみます。

注文の合計金額を計算する: 「注文のラインアイテム」というシーケンスから「合計金額」を計算する。

複数の権限がすべて満たされているかをチェックする: 「複数の権限」というシーケンスから「操作の可否」を計算する。

複数のロールのうちひとつでもユーザが所属するものがあるかをチェックする: 「複数のロール」というシーケンスから「ユーザの所属の有無」を計算する。

リストを連結する: 「複数のリスト」というシーケンスから「連結したリスト」を計算する。

集合の共通部分を取る: 「複数の集合」というシーケンスから「共通部分」を計算する。

集合の和集合を取る: 「複数の集合」というシーケンスから「和集合」を計算する。

複数のフラグをまとめる: 「複数のフラグ」というシーケンスから「合算したフラグ」を計算する。

reduce() については「 reduce() を使うと [3, 5, 10]150 といった数列の積の計算がかんたんにできます」といったシンプルな例を使った說明がよくなされますが、それだけ聞いても「え、それ、業務のプログラミングで使います?」となりがちです。対して、上のような実際にありそうな例を見ると「 reduce() が活用できる場面は意外に多いなぁ」と思えるのではないかと思うのですがいかがでしょうか。

続いて、上にあげた例のいくつかに対してサンプルコードを見てみましょう。

実践的なサンプル


注文の合計金額を計算する

注文のラインアイテムから合計金額を計算します。

from collections import namedtuple
from functools import reduce

LineItem = namedtuple('LineItem', ['商品ID', '単価', '数量'])

def calc_total(items):
    """ラインアイテムの合計金額を計算する"""
    return reduce(lambda accum, line_item: accum + line_item.単価 * line_item.数量, items, 0)

line_items = [
    LineItem('shirt a', 1000, 2),
    LineItem('shirt b', 1200, 1),
    LineItem('shirt c', 4000, 2),
]

calc_total(line_items)
# => 11200

関数 calc_total() は各ラインアイテムの単価と数量を見てその合計金額を計算する関数です。 reduce() の中の accum には「単価 ✕ 数量」の値が蓄積され、最終的に注文全体の合計金額が出ます。

余談ですが、 accum の値がどのように変化していくのかを見たい場合は lambda をローカル関数に差し替えて中身を出力するとよいでしょう。

def calc_total(items):
    """ラインアイテムの合計金額を計算する"""
    def _sumproduct(accum, line_item):
        print(accum)
        return accum + line_item.単価 * line_item.数量

    return reduce(_sumproduct, items, 0)
    # return reduce(lambda accum, line_item: accum + line_item.単価 * line_item.数量, items, 0)

calc_total(line_items)
# 0
# 2000
# 3200
# => 11200

ただ、この処理は、例えば内包表記を使って次のように書くこともできます。

def calc_total(line_items):
    return sum(x.単価 * x.数量 for x in line_items)

reduce() を使った書き方がしっくり来ない場合はムリに reduce() を使わず、そのときどきでコードの意図がわかりやすい・メンテナンスしやすい書き方を選ぶとよいかと思います。

複数の権限がすべて満たされているかをチェックする

あらかじめ定義されている権限を対象ユーザがすべて持っているかどうかをチェックします。

from functools import reduce


class User:
    def __init__(self, permissions):
        self._permissions = permissions

    def has_perm(self, permission):
        return permission in self._permissions

    def has_all_perms(self, permissions):
        """指定された権限をすべて持っているかチェックする"""
        return reduce(lambda accum, p: accum and self.has_perm(p), permissions, True)


required_permissions = ('perm_a', 'perm_b', 'perm_c')


user1 = User(('perm_c',))
print(user1.has_all_perms(required_permissions))
# => False

user2 = User(('perm_a', 'perm_b', 'perm_c'))
print(user2.has_all_perms(required_permissions))
# => True

クラス User のメソッド has_all_perms() は、 list あるいは tuple で渡された一連の権限をユーザがすべて持っているかどうかをチェックします。 reduce() を使うことでわかりやすくシンプルに書くことができています。

上の「注文の合計金額」が sum() を使って書けたのと同様に、こちらは all() と内容表記を使って書くこともできます。

def has_all_perms(self, permissions):
        """指定された権限をすべて持っているかチェックする"""
        return all(self.has_perm(p) for p in permissions)

こちらも all() の方が読みやすくメンテナンスしやすいのであればムリに reduce() を使う必要はありません。

ちなみに、上の「 複数のロールのうちひとつでもユーザが所属するものがあるかをチェックする 」はこのチェック対象のデータが権限からロールに変わって all()any() に変わるようなイメージです。

リストを連結する

各要素がリストのリストに対して、その要素をすべて連結した大きなリストを生成します。

from functools import reduce


def concat_lists(lists):
    """リストを連結する"""
    return reduce(lambda accum, x: accum + x, lists, [])


lists1 = [[1, 2, 3], [5, 8], [13, 21, 34, 55]]
print(concat_lists(lists1))
# => [1, 2, 3, 5, 8, 13, 21, 34, 55]

関数 concat_lists() は、リストからなるリストの各要素を連結します。こちらも reduce() を使うことで、わかりやすく簡潔に書くことができています。

もし処理の途中経過が見たければ、上で述べたように lambda の部分を print() 等で出力を行うローカル関数に差し替えるとよいでしょう。

def concat_lists(lists):
    """リストを連結する"""
    def _extend(accum, x):
        print(accum)
        return accum + x
    return reduce(_extend, lists, [])
    # return reduce(lambda accum, x: accum + x, lists, [])

lists1 = [[1, 2, 3], [5, 8], [13, 21, 34, 55]]
print(concat_lists(lists1))
# []
# [1, 2, 3]
# [1, 2, 3, 5, 8]
# [1, 2, 3, 5, 8, 13, 21, 34, 55]
# => [1, 2, 3, 5, 8, 13, 21, 34, 55]

この concat_lists() の処理は sum() を使って書くことも可能です。

def concat_lists(lists):
    """リストを連結する"""
    return sum(lists, [])

この単純な例だと sum() を使った方がむしろわかりやすいかもしれませんね。

集合の和集合を取る

複数ある集合の和集合を計算します。

from functools import reduce


def union_multiple(*sets):
    """和集合を作る"""
    return reduce(lambda accum, x: accum | x, sets, set({}))


set1 = {'鹿児島', '宮崎', '熊本'}
set2 = {'大分', '佐賀'}
set3 = {'長崎', '福岡'}

print(union_multiple(set1, set2, set3))
# => {'熊本', '長崎', '福岡', '大分', '宮崎', '佐賀', '鹿児島'}

関数 union_multiple() は任意の数の引数( set )を受け取って、それらの和集合を返す関数です。 Python では 2 つの集合の和集合は set_a | set_b (あるいは set_a.union(set_b) )で計算できるので、ここで必要なのは reduce() を使ってそれを蓄積していくことだけです。

標準ライブラリ operator になじみがあって、コードのわかりやすさが損なわれないと考えられるのであれば、同じ処理は operator.or_ を使って次のように書いてもよいでしょう。

from functools import reduce
from operator import or_


def union_multiple(*sets):
    """和集合を作る"""
    # return reduce(lambda accum, x: accum | x, sets, set({}))
    return reduce(or_, sets, set({}))

Python では +-*/&| 等の 2 項演算子をオブジェクトとして扱うことができませんが、その代わりに operator の中にそれらに相当する関数が用意されています。

一例:

  • +: operator.add() または operator.concat()
  • -: operator.sub()
  • *: operator.mul()
  • /: operator.truediv()
  • &: operator.and_()
  • |: operator.or_()

ちなみに、 reduce()operator が提供する関数を知っておくとよりシンプルにわかりやすく書けることがあるので、 reduce() を活用したい方は operator もあわせてチェックしておくとよいかもしれません。

尚、上の「 集合の和集合を取る 」と「 複数のフラグをまとめる 」については、ここではサンプルコードは示しませんがこの union_multiple() と同じような考え方で書くことができます。

応用的なサンプル


ちょっと応用的(トリッキー?)な例もいくつかあげてみます。 reduce() をむやみやたらと使ってしまうとかえってコードがわかりづらくなりますが、こういう使い方もできるということを知っておくと実装の選択肢の幅が広がってよいのではないかと思います。

入れ子の辞書の要素にアクセスする

これは Stack Overflow で紹介されていた使い方です。私は目からウロコでした。

入れ子になった dict がありそれを掘り下げる一連のキーが変数として与えられたときに、要素にアクセスするロジックを reduce() を使ってスムーズに書くことができます。

from functools import reduce


def dict_deep_access(adict, key_tree):
    """ネストされた dict の要素にアクセスする"""
    return reduce(lambda elem, key: elem[key], key_tree, adict)
    # 次のように書くこともできる
    # return reduce(dict.__getitem__, key_tree, adict)


d1 = {
    '沖縄': {
        '恩納村': {
            '万座毛': '隆起サンゴの断崖',
            'なかゆくい市場': '道の駅',
            '真栄田岬': '夕日スポット',
        }
    }
}

key_trees = [('沖縄', '恩納村', '万座毛'), ('沖縄', '恩納村', '真栄田岬')]

for tree in key_trees:
    print(dict_deep_access(d1, tree))
# =>
# 隆起サンゴの断崖
# 夕日スポット

特定のディレクトリからの相対パスでアクセス

特定のディレクトリから対象ファイルへの相対パスが list として与えられたときに、対象ファイルのパスを生成するというものです。

from functools import reduce
from pathlib import Path


def file_relative_from(root, relative_path):
    """指定された場所からの相対パスでファイルを取得する"""
    file = reduce(lambda parent, dir: parent / dir, relative_path, root)
    return file.resolve()


path1 = Path('/private/tmp/abc/abc1.txt')
relative_path1 = ['..', '..', 'def', 'def1.txt']

file_relative_from(path1, relative_path1)
# => /private/tmp/def/def1.txt

関数の組み合わせ

複数の関数を組み合わせて順次適用していくというものです。

from functools import reduce


def apply_filters(seq, filters):
    """シーケンスに複数のフィルタを連続で適用する"""
    return reduce(lambda result, fn: fn(result), filters, seq)


filters = (
    lambda seq: [x.upper() for x in seq],
    lambda seq: [x.center(12) for x in seq],
    lambda seq: ['⚡ {} ⚡'.format(x) for x in seq],
    lambda seq: '\n'.join(seq),
)

関数 apply_filters() は、第 2 引数に callable のシーケンスを受け取り、その要素を第 1 引数に順次適用した結果を返してくれます。

apply_filters()filters を組み合わせると次のような処理になります。

apply_filters(['teenage', 'mutant', 'ninja', 'turtles'], filters)
# =>
# ⚡   TEENAGE    ⚡
# ⚡    MUTANT    ⚡
# ⚡    NINJA     ⚡
# ⚡   TURTLES    ⚡

apply_filters(['genetically', 'modified', 'punk', 'rock', 'pandas'], filters)
# =>
# ⚡ GENETICALLY  ⚡
# ⚡   MODIFIED   ⚡
# ⚡     PUNK     ⚡
# ⚡     ROCK     ⚡
# ⚡    PANDAS    ⚡

この処理は例えば map() や内包表記を使うと次のように書くこともできます。

# 内包表記で書いた場合
def apply_filters_hard_1(seq):
    return '\n'.join(
        '⚡ {} ⚡'.format(x) for x in (
            x.center(12) for x in (
                x.upper() for x in seq
            )
        )
    )

# map() で書いた場合
def apply_filters_hard_2(seq):
    return '\n'.join(
        map(
            lambda x: '⚡ {} ⚡'.format(x),
            map(lambda x: x.center(12),
                map(lambda x: x.upper(),
                    seq
                )
            )
        )
    )

・・・が、これだと関数の適用順序とは逆に lambda を書かなくてはなりません。このように書くぐらいであれば各処理の結果を変数に格納し行を分けて書いた方がよいでしょう。

サンプルの紹介は以上です。

まとめ


というわけで、 Python の reduce() の使いどころと実践的なサンプルについてでした。

これは受け売りですが、 reduce() については、要は「 sum()all()any() 等の「シーケンスからひとつの値を生成する」タイプの処理を抽象化したものが reduce() 」という捉え方ができます。もし OOP のクラスと同じような親子関係(継承関係)が関数にもあるなら、 reduce()sum()all() の親関数、と捉えてもよさそうです。

余談ですが、 Python ・ Ruby ・ JavaScript あたりではこの操作を行う関数に reduce という名前がつけられていますが、関数型ベースの言語においては reduce よりも fold という名前が選ばれることが多いような気がします。

以上です。

関連記事


参考

reduce() については以下のページ等もおもしろいです。