364
301

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

「関数型プログラミング」と「オブジェクト指向」ってなんやねんPython編

Last updated at Posted at 2020-05-06

こ の 記 事 は 土 木 専 攻 の エ ン ジ ニ ア が 鼻 水 た ら し な が ら 書 き 上 げ た 努 力 の 結 晶 で す 。
ご 指 摘 は マ サ カ リ で な く 真 っ 白 な タ オ ル で も 優 し く 投 げ て く だ さ い 。

ことの発端

ぼく「いやーしかしぼくもそこそこ長いことPyhton書いとるしもうPython完全に理解したんちゃうんかなー!」

ぼく「久々にdocs.python.orgでもみるかー。今見たら全部わかるんちゃうかwww」

ぼく「関数型プログラミング HOWTO…?何やねんそれ…」

ぼく「えーと…?ほとんどのプログラミング言語は手続き型?で、入力に対して行うべきことをコンピューターに教える指示リスト?オブジェクト指向?はオブジェクトの集まりを操作する。オブジェクトには内部状態があり、その状態を調べたり変更したりするためのメソッドがある…?関数型?は問題をいくつかの関数に分けて考え、入力を待ち受けて出力を吐くだけで、同じ入力に対して異なる出力を吐くような内部状態を一切持ちません?」

ぼく「???????」

ぼく「えーと…?」

Pythonはそうしたマルチパラダイム言語です。この中のどれを使っても、基本的に手続き型な、または基本的にオブジェクト指向な、とか、基本的に関数型なプログラムやライブラリを書くことができます。大きなプログラムでは、各部で別々のアプローチを使って書くことがあるかもしれませんGUIはオブジェクト指向で、でも処理ロジックは手続き型や関数型で、といったようにです。

ぼく「結局何が言いたいねん…何もわからん…」

プログラミング手法の違い

ぼく「そもそもプログラミングには色んな手法特徴があるんやな」

手法 特徴 代表的な言語
手続き型 命令を一まとめにした手続き(関数など)を記述し、組み合わせていくスタイル。 C
関数型 関数の合成と適用からプログラムを組み立てるスタイル。 Haskell
オブジェクト指向 扱う対象のデータと操作手続きをひとまとまりとして考えるスタイル。 Java

ぼく「うーん…いまいちよう分からんな…とくに手続き型と関数型は何が違うんや。もう少し調べてみよか。」

手続き型プログラミングの特徴

  • 課題を解決するための方法を手順通りに細かく書いていく
  • 手順は上から1つづつ記述していくこともできるが、一般的には複数の関数を定義し、それらを組み合わせてデータを変化させていく
  • シンプルで処理の流れがわかりやすい(見るのも書くのも簡単)
  • 代表的な言語:Cなど

ぼく「なるほど。要はこんな感じで処理を上から順に書いていくってことやな。ぼくがPythonでようやっとんのはこれや」

def add(x, y):
    return x + y

def squared(x):
    return x ** 2

squared_x = squared(2) # 4
squared_y = squared(4) # 16
add_xy = add(squared_x, squared_y) # 20

ぼく「ほな、次」

関数型プログラミングの特徴

  • 変数および関数に参照透過性があり、副作用が抑制または完全に排除されていること
  • つまり、関数の内部は外部にあるデータを一切頼らず、またそれらを変更しない
  • 副作用が完全に排除された純粋関数型では変数に値を代入するという考え方がなく、変数に値が束縛される
  • 処理を細かく関数に分け、それらを組み合わせて利用するためパイプラインや高階関数、クロージャなどが使われる
  • 代表的な言語:Haskellなど

ぼく「んん???難しそうな単語が仰山出てきたな…」

参照透過性とは

  1. 環境からの影響を受けず、値がいつ参照しても同じ値であること(イミュータブル(不変))である変数
  2. 引数が同じであれば必ず同じ戻り値となる関数も含む
  3. 値が不変であるため、ヒューマンエラーを抑制できる
  4. 副作用を発生させないので並列処理に強い(データの競合を発生させにくい)

ぼく「ほんだらx = 1を一度定義したらそのプログラム中はxが1であることが必ず保証されるっちゅうことかいな。」

ぼく「関数も同じことが言えんねんな。毎度必ず同じ値が帰ってくるっちゅーんは数学みたいやな。ぼくもプログラミング始めた頃は」

x = x + 1

ぼく「とか見てひっくり返りそうになったわ。右辺と左辺が等しいわけないやんこんなん。」

ぼく「…うーんなるほどなー。Pythonはほとんどの変数が可変だから適用させるのが難しそうな概念やけど、戻り値は一定の方がありがたいわな。テストしやすいし。ほんで副作用って何や?」

副作用とは

  1. 処理の実行により外部から観測可能な変化を与えること
  2. ミュータブル(可変)な変数への再代入などが代表例
  3. 状態を持たず、副作用が完全に排除された関数を純粋関数、そうでない物を非純粋関数と呼ぶ

ぼく「あーはいはいなるほどね。完全に理解したわ。」

ぼく「でも再代入を状態を完全に排除してどうやってプログラムを組むんや…?for文はよう使うけど、Pythonのfor文はローカルスコープを作らないし副作用の代表例や…」

ぼく「そうか!!!!組み込み関数のmapを利用するんや!!!!」

# 手続き型
def squared(x):
    return x ** 2

number_list = [1, 2, 3, 4, 5]
squared_list = [] # [1, 4, 9, 16, 25]
for n in number_list:
    squared_list.append(squared(n))
# 関数型
squared_list = list(map(lambda x: x ** 2, [1, 2, 3, 4, 5])) # [1, 4, 9, 16, 25]

ぼく「1行な上に状態を変化させずに書くことができた。すごない?」

ぼく「ただ、手続き型の方が直感的にわかりやすいコードなのは間違いないな…要は使いどころを考えろって話やな。」

ぼく「次は何や?」

その他のキーワード

  • 高階関数:関数を引数にとる関数のこと(コールバック関数ともいう)
  • クロージャ:ネストされた内部の関数が親の関数で定義された変数を記憶している関数のこと

ぼく「さっき利用したmaplambda式(無名関数)を受け取ってくれるから高階関数の代表例やな。」

ぼく「関数を受け取れるようにすることで、具体的な処理を1つの関数内に書かなくてよくなるから処理を分割しやすい、つまり拡張性が高まるんやな。」

ぼく「また、関数を受け取れば受け取った関数の前後に決まった処理を挟むことが容易やし、メインの処理に影響を与えないで機能を追加できるな!これをPythonではデコレータって呼ぶらしいで!」

# 関数の処理時間を計測
from functools import wraps
import time

# 関数を受け取る関数を定義
# 内部の関数を返却する
def time_measurement(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        exe = func(*args, **kwargs)
        end = time.time()
        print(f"time_measurement: {end - start}")
        return exe
    return wrapper

# @をつけて呼び出す(デコレータ)と、呼び出し先の関数の引数に入る
@time_measurement
def func(num):
    res = 0
    for n in range(num):
        res += n
    return res

func(10000000) # time_measurement: 0.48942112922668457

ぼく「それと下ののようにネストした関数が、外で定義された変数を記憶している関数のことをクロージャっていうらしいで!」

# エンクロージャ
def outer():
    x = 1
    # クロージャ
    def inner(y):
        return x + y
    return inner

f = outer() # 戻り値はinner()
i = f(10) # innerが実行されるので戻り値は11になる

ぼく「ただし、親で定義された変数は参照可能であるが更新についてはnonlocalという特殊な文を宣言しないと、できないので注意な!」

ぼく「関数型は関数を使ってプログラミングをすることには変わりないけど、保守性とか拡張性なんかを考慮し、言語の力フルパワーで使って副作用が(なるべく)ないプログラムを組むことなんやな。」

オブジェクト指向型プログラミング

ぼく「最後はオブジェクト指向か。よくりんごとか車とかに例えられるやつやな!」

  • 頻繁に変更されるであろう箇所をクラスに抽出することで、システムが変更に対して柔軟に対応できるようにするための手法
  • オブジェクト指向では関数型と違い内部に状態もち、それを参照・変化させることでプログラムを組む
  • 副作用の発生などを抑えるために、インターフェイスに定義されたメソッド(振る舞い)からのみ変更を許容することで、内部に隠蔽する
  • 隠蔽/抽象化・継承・多様性などの特徴を持つ
  • 代表的な言語:C++・Javaなど

ぼく「なるほど。要はクラスのことや。データの構造と振る舞いを1つの物として捉えてプログラムを組むんやな。」

ぼく「関数型とは違って状態があるのを許容するけど、内部に隠蔽することでバグの発生を抑えるんやな。」

ぼく「ただ、キーワードが多すぎてよう分からんな…一個ずつ調べてこ。」

オブジェクト指向型プログラミングのキーワード

隠蔽(カプセル化)/抽象化

隠蔽(カプセル化)
  • インスタンスのプロパティをすべてメソッドを通じて変更させることにより、外部から直接不正なプロパティを変更させるのを防ぐ
  • クラスの役割を1つにすることでカプセル化させやすくなるので重要なのは「正しい名付け」。名付けが良いということはつまりシンプルで目的が明確になっているので隠蔽しやすい

ぼく「ほー。クラスから作ったインスタンスの変数は直接いじっちゃだめなんか。」

ぼく「とはいえPythonは変更不可能な変数なんて定義できないから、やるならこんな感じかいな?」

  • pythonでは@propertyや@property.setterを利用する
  • 外部からアクセスさせたくないプロパティには変数の最初にアンダースコアをつける
  • ただし、pythonでは本当にアクセスできないプライベート変数を作成することができないので、慣例として「アクセスしないようにする」ことを表明する

ぼく「まずは重量に応じて値段が変わるお肉クラスでも作ってみよか。」

class Meat:
    # インスタンスの初期化
    # 各インスタンスはここで定義された属性を持つ
    def __init__(self, name, weight):
        if weight < 0:
            raise ValueError("重量は0gを下回ってはいけません")
        self.name = name
        self.weight = weight
        # 外部からは直接利用できないように_をつける
        # 割引は初期値5に設定
        self._unit_price = 5
        self._price = 0
    # インスタンスを呼び出したときに表示される文字列を設定
    def __repr__(self):
        return f"{self.name}: {self.price}"
    # propertyデコレータをつけることで外からはメソッドをインスタンス変数のように呼び出せる
    @property
    def unit_price(self):
        # 参照したい値を戻り値として設定
        return self._unit_price
    @property
    def price(self):
        return self.weight * self._unit_price
    # 変数の変更はsetterを利用
    # 変数を変更させたい時は直接上書きするのではなく、メソッドを通して上書きさせる
    # こうすることで外部からの参照はこのメソッドを通して行われることになり、不正な値の混入を防ぐ
    @unit_price.setter
    def unit_price(self, value):
        if not (1 <= value <= 10):
            raise ValueError("単価は1円〜10円の間で設定してください")
        self._unit_price = int(value)

ぼく「よし、早速使ってみるで」

>>> meat = Meat("松坂牛", 200)
>>> meat
松坂牛: 1000

>>> meat.unit_price = 100
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 30, in unit_price
ValueError: 単価は1円10円の間で設定してください

>>> meat.unit_price = 10
>>> meat
松坂牛: 2000

>>> meat.price = 3000
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: can't set attribute

ぼく「やっぱり松坂牛は高いな。お腹いっぱい食いたいもんや。」

ぼく「でも想定した通りの動きやな。これで以下のように実装できたで。」

  • 初期値としてnameweightが設定可能
  • オブジェクトを呼び出すとname: price円が表示される
  • priceは自動計算される
  • unit_priceは範囲外の値を設定しようとするとエラー
  • priceに直接値を設定しようとするとエラー
抽象化

ぼく「よく考えたら重量から金額を算出するのはお肉特有の処理ちゃうな。」

ぼく「クラス名をItemとかに変更してみよか。重量によって値段が変わるとも限らんし、単価は決まっていて個数だけが変化することもあるかも知らん。よし、一旦単価設定は外しとこか。」

ぼく「こういうのを抽象化っていうんやな。」

  • 頻繁に変更されるであろう箇所を集めて、本質的な特徴のみクラスに抽出すること
  • 「AはBである」と表現できるときに、それらはもっと抽象的にまとめられるはず
  • 交換可能パーツを作るために、共通点を規格としてまとめることが抽象化の概念
  • 変更に強くなるが、やりすぎ注意
class Item:
    def __init__(self, name):
        self.name = name
        self._price = 0
    def __repr__(self):
        return f"{self.name}: {self.price}"
    @property
    def price(self):
        return self._price

継承

ぼく「再度お肉クラスを作りたいときには上の抽象クラス(Itemクラス)を継承して具象クラスを作るんやな」

  • 親となる抽象化されたオブジェクトを受け継いで、もう少し具体化されたクラスを作成すること
  • pythonであればクラス定義時に親となるクラスを引数に取ることで継承できる
  • イニシャライズ時にsuper()メソッドを利用して明示的に継承しなければ親クラスのプロパティやメソッドは利用することができない
class Meat(Item):
    def __init__(self, name, weight):
        # super()を利用して明示的に親クラスの__init__()を呼び出す
        super().__init__(name)
        self.weight = weight
        self._unit_price = 5
    @property
    def unit_price(self):
        return self._unit_price
    # メソッドを上書き(オーバーライド)
    @property
    def price(self):
        return self.weight * self._unit_price
    @unit_price.setter
    def unit_price(self, value):
        if not (1 <= value <= 10):
            raise ValueError("単価は1円〜10円の間で設定してください")
        self._unit_price = int(value)
>>> meat = Meat("松坂牛", 500)
>>> meat
松坂牛: 2500

>>> meat.unit_price = 100
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 21, in unit_price
ValueError: 単価は1円10円の間で設定してください

>>> meat.unit_price = 10
>>> meat
松坂牛: 5000

>>> meat.price = 3000
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: can't set attribute

ぼく「これでりんごクラスもみかんクラスも作りたい放題や!!!!」

多様性(ポリモーフィズム)

ぼく「ちなみに今回は抽象クラスをクラスとして作成したんやけど、普通はインターフェイスっていうものに振る舞い(メソッド名)だけ定義するらしいで。」

ぼく「インターフェイスではメソッドの中身は書かず、具体的な実装は継承先のクラスに任せるんや(メソッドの強制実装)。」

ぼく「そうすることで、継承先のクラスは全て同名のメソッドを持っていることが確定するから見通しがよくなるんやな!これをポリモーフィズムっていうらしいで!!」

  • 様々な使い方ができるように、抽象化されたクラスに対してプログラミングを行うこと
  • つまり同名の関数やメソッドでありながら、型に合わせて異なる振る舞いをすること

## 総括

ぼく「ふぅ。やっとなんとなく書くスタイルの特徴がわかったで。どれも素晴らしいな。」

ぼく「手続き型は単純なプログラムを組むなら簡単に素早く見通しの良いコードが書ける。ただ、中規模以上だとスパゲッティ直行コースやな…」

ぼく「関数型は書くのは大変そうやけど、大規模な開発では再利用可能なパーツごとに切り分けているおかげで拡張性が良さそうだし、バグが少なくなりそうやな。」

ぼく「オブジェクト指向も大規模に強そうや。難しそうだけど、うまいこと抽象化できれば結果的にコード量も減りそうやし!」

ぼく「ちなみにPythonは副作用は発生してしまうけど、関数型でもプログラムを組めるし、オブジェクト指向もいける万能な言語なんやな!マルチパラダイム言語っていうらしいで!」

ぼく「だからこそ手法はしっかり学んで正しく使わなあかんねんな!ほなPython完全理解目指して頑張りまっかー!」

お し ま い

364
301
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
364
301

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?