【Python】functools ~ 高階関数 ~

■ はじめに

Scalaで高階関数(こうかいかんすう)を知る機会があったが
Pythonでもfunctoolsモジュールで扱えるとのことなので
メモってみた。

目次

【1】高階関数 (Higher-Order Functions)
【2】functools
【3】cache
 1)@cache
 2)@cached_property
【4】partial
 1)partial
 2)partialmethod
【5】reduce
【6】wrap
【7】single dispatch
 1)@singledispatch
 2)@singledispatchmethod

【1】高階関数 (Higher-Order Functions)

* 引数として関数を受け取ったり
 戻り値として関数を返す関数
 => 戻り値をオブジェクトとして扱う次元の高い関数のため
  「高階関数 (Higher-Order Functions)」と呼ぶ

特徴

* 以下の特徴を持つ
[1] 関数を引数として受け取る
[2] 関数を返す

【2】functools

* 高階関数を扱いやすくする機能を提供するPython標準ライブラリ

https://docs.python.org/ja/3/library/functools.html

【3】cache

* 同じ引数を入れた時、最初の計算結果を即座に返す

1)@cache

from functools import cache

# 再帰関数
@cache
def factorial(n):
    print('call', n)
    return n * factorial(n-1) if n else 1

result = factorial(10)
print(f'result = {result}')
result = factorial(7)
print(f'result = {result}')

出力結果

call 10
call 9
…
call 1
call 0
result = 3628800
# キャッシュされているから「call X」が表示されることなく値が返されている
result = 5040

2)@cached_property

from functools import cached_property

class Number:
  def __init__(self, number_list):
    self.number_list = number_list

  @cached_property
  def sum(self):
    print('Call')
    return sum(self.number_list)

number = Number([1, 2, 3])
print(number.sum)
print(number.sum)

出力結果

Call
6
6

【4】partial

* 部分的に引数を固定する

1)partial

from functools import partial

def say_something(greeting, name):
  print(f'{greeting}, {name}!!!')

# 注目
say_hello = partial(say_something, 'Hello')
 
say_hello('Mike')

2)partialmethod

* クラスのメソッドのpartial版

サンプル

from functools import partialmethod

class Animal:
  def __init__(self):
    self._alive = False

  @property
  def alive(self):
    return self._alive

  def set_state(self, state):
    self._alive = bool(state)

  set_alive = partialmethod(set_state, True)
  set_dead = partialmethod(set_state, False)

animal = Animal()
print(animal.alive) # False
animal.set_alive()
print(animal.alive) # True
animal.set_dead()
print(animal.alive) # False

【5】reduce

* 2 つの引数をもつ function を左から右に累積的に適用し

サンプル

from functools import reduce

numbers = [1, 2, 3, 4, 5]
# ((((1+2)+3)+4)+5) 
total = reduce(lambda x, y: x + y, numbers)

print(total) # 15

【6】wrap

* 関数が他の関数をラップしていることを示すために使用されるデコレータ

通常は、、、

* 自作デコレータを使うためにラッパー関数を定義するとき、
 何もケアしなければラップされた関数の情報が覆い隠されてしまう
 => この関数は、この内部情報を幾分か見れるように定義可能

from functools import wraps

def my_decorator(f):
    @wraps(f)
    def my_wrapper(*args, **kwds):
        print("Calling decorated function - start")
        print(args)
        print(kwds)
        print("Calling decorated function - end")
        return f(*args, **kwds)
    return my_wrapper

@my_decorator
def demo(*args, **kwds):
    """Docstring"""
    print("Called example function")

demo(123, "abc", a=1, b=1)
print('***************')
print(demo.__name__)
print(demo.__doc__)

出力結果

Calling decorated function - start
(123, 'abc')
{'a': 1, 'b': 1}
Calling decorated function - end
Called example function   
***************
demo
Docstring

出力結果 (「#@wraps(f)」した場合)

Calling decorated function - start
(123, 'abc')
{'a': 1, 'b': 1}
Calling decorated function - end
Called example function   
***************
my_wrapper
None

【7】single dispatch

* ジェネリック関数を定義できる

1)@singledispatch

from functools import singledispatch

@singledispatch
def say_hello(x):
  print("Hello,", x)

def say_int(x):
  print("Hello, integer", x)

def say_str(x):
  print("Hello, string", x)

say_hello.register(int, say_int)
say_hello.register(str, say_str)

say_hello(123) # Hello, integer 123
say_hello('Mike') # Hello, string Mike
say_hello(12.3) # Hello, 12.3

2)@singledispatchmethod

from functools import singledispatchmethod
from decimal import Decimal

class Demo:
    @singledispatchmethod
    def say_hello(self, value):
      print(f'Hello, {value}!!')

    @say_hello.register
    def _(self, obj: int):
      print("int ", obj)

    @say_hello.register(float)
    @say_hello.register(Decimal)
    def _(self, obj):
      print("float or Decimal ", obj)

demo = Demo()
demo.say_hello(1) # => int  1
demo.say_hello(1.23) # => float or Decimal  1.23
demo.say_hello(Decimal("4.56")) # => float or Decimal  4.56
demo.say_hello("Mike") # => Hello, Mike!!

参考文献

https://smooth-pudding.hatenablog.com/entry/2021/03/27/145218
https://docs.kanaries.net/ja/topics/Python/functools-python3
https://marusankakusikaku.jp/python/standard-library/functools/

関連記事

Python ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2014/08/07/231242
Python ~ 基本編 / 文字列 ~
https://dk521123.hatenablog.com/entry/2019/10/12/075251