関数型プログラミング


こんにちは!このポストでは、私のお気に入りの関数型プログラミングテクニック(FP)をPythonに紹介したいと思います.私はFPの大ファンです.私はFP原理に従うことによって、読みやすく、デバッグが容易なコードを書くことができます.Pythonは関数型プログラミング言語ではありません(そして、それは決してありません)、しかし、Pythonでも有益なHaskellのような言語から学ぶことができる多くのことがあると思います.
要するに、以下のようになります.
  • 使用リストと辞書の理解
  • ジェネレータ式とitertools
  • 凍結データプールを使用する
  • あなたのリストと辞書を不変にしておきなさい
  • 継承の代わりにuse unionを使用します.

  • 使用リストと辞書の理解
    最初のテクニックはlist comprehensions リストを作成したり、map and filter リストで
    Haskellでは、リストの理解を次のように使います.
    ghci> [x*2 | x <- [1..10], x*2 >= 12] 
    [12,14,16,18,20]
    
    ここでリストを横断しています[1..10] 値をフィルタリングするx*2 >= 12 とマッピングx*2 .
    Pythonでは、我々は同じことを達成する
    >>> [2*x for x in range(1, 11) if 2*x >= 12]
    [12, 14, 16, 18, 20]
    
    上記の「命令的」バージョンは以下の通りです.
    a = []
    for i in range(1, 11):
      val = 2*x
      if val >= 12:
        a.append(val)
    print(a)
    
    また、実行することができますflatmap -入れ子になったリストの理解のような操作
    >>> xss = [[1, 2], [3, 4], [5, 6]]
    >>> [2*x for xs in xss for x in xs]
    [2, 4, 6, 8, 10, 12]
    
    私がよくPythonでのリストの理解に直面する1つの問題はlet 構築する.この構成は、コードの読みやすさを改善し、変数にバインドすることによって中間値を再計算するのを避けるのに便利です.例えば、Haskellでは以下のようにします.
    ghci> [y | x <- [1..50], let y=x*x, y `mod` 5 == 0]
    [25,100,225,400,625,900,1225,1600,2025,2500]
    
    ここではコンピューティングを避けるx*x 値を変数にバインドすることによってy .
    Pythonでは、書くことができました
    >>> [x*x for x in range(1, 51) if x*x % 5 == 0]
    [25, 100, 225, 400, 625, 900, 1225, 1600, 2025, 2500]
    
    しかし、これは計算されますx*x すべての要素に対して2回.我々は、これを使用して、これを回避することができますauxiliary one-element tuple 次のようになります.
    >>> [y for x in range(1, 51) for y in (x*x,) if y % 5 == 0]
    [25, 100, 225, 400, 625, 900, 1225, 1600, 2025, 2500]
    
    それはHaskellと同じくらい読みやすいです、しかし、時々、それは最高の選択のように感じます.複数の行に分割することによって、構造をより読みやすくすることができます.
    ys = [x_squared for x in range(1, 51)
                    for x_squared in (x*x,)  # let x_squared = x*x
                    if x_squared % 5 == 0]
    

    ジェネレータ式とitertoolsHaskellでは、式はlazilyに評価されます.したがって、すべての整数を含んでいる例で働くのは全く自然です.そのような無限のリストは決して完全に評価されることになっていません、しかし、値は彼らが実際に必要であるときだけ、つくられます.
    Pythonはこのような「イテレータ」をサポートしていますgenerator expressions and functions . The itertools パッケージにはイテレータの操作に便利な関数がたくさんあります.
    例として、そのfirst problem プロジェクトEulerで.プロジェクトEuler問題の解決を公に共有することは強く落胆します、しかし、インターネットがすでに最初の問題のために答えでいっぱいであるので、私はここで例外を作ります.これが問題です.

    If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000.


    次のようにイテレータで解決できました.
    >>> from itertools import count, takewhile
    >>> ints = count(1)  # Iterator of all integers
    >>> multiples = (val for val in ints if val % 3 == 0 or val % 5 == 0)  # All integers that are multiples of 3 or five
    >>> multiples_below_1000 = takewhile(lambda val: val < 1000, multiples)  # All such integers below 1000
    >>> sum(multiples_below_1000)  # Sum of all such values, this is the evaluation step
    233168
    
    これはPythonでの問題に対して最も効率的な解決策ではありません.

    凍結データプールを使用する
    Haskellのような純粋な機能言語では、ある場所でオブジェクトを変異することはできません.代わりに、入力を突然変異しない純粋な関数からプログラムを作成しなければなりません.コードは本質的にパイプラインになります.そして、不変のデータ構造は1つの変換から次へ流れます.この種の思考も素晴らしいPragmatic Programmer ブック

    "Thinking of code as a series of (nested) transformations can be a liberating approach to programming. It takes a while to get used to, but once you've developed the habit you'll find your code becomes cleaner, your functions shorter, and your designs flatter. Give it a try."


    今、Pythonで純粋に機能的なプログラムを書こうとするのはあまり意味がありませんが、不変のオブジェクトを持つ“変形型”コードを好んだという考えは良い考えです.Immutableオブジェクトを使用すると、潜在的なバグの1つのクラスを排除します.
    プレーンデータ構造体では、Pythonの組み込みdataclasses 変更不能コンテナの良い選択です.With frozen=True , 我々のDataClassのプロパティは、作成後に変異できません.
    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Doggie:
      name: str
      age: int
    
    我々が変わるならばDoggie 'sの名前、我々は使用できますreplace :
    from dataclasses import replace
    
    def change_doggie_name(doggie: Doggie, new_name: str) -> Doggie:
      return replace(doggie, name=new_name)
    
    新しいオブジェクトを作成するたびにパフォーマンスのペナルティが発生しますので、オーバーヘッドがあなたのプログラムには大きすぎるかどうかを判断するのはあなた次第です.
    我々のクラス内のすべての変更可能なデータ構造を突然変異させることができるので、凍結されたデータクラスが本当に変更可能ではないことを覚えておくことが重要です.
    @dataclass(frozen=True)
    class Doggie:
      name: str
      age: int
      attributes: dict
    
    doggie = Doggie(name="Ben", age=10, attributes={})
    doggie.attributes["colour"] = "brown"  # In-place mutation
    
    凍結データクラスをプロパティとして使用することで、これを回避できます.
    @dataclass(frozen=True)
    class DoggieAttributes:
      colour: str
    
    @dataclass(frozen=True)
    class Doggie:
      name: str
      age: int
      attributes: DoggieAttributes
    
    doggie.attributes.colour = "black"  # NOT ALLOWED
    
    それから、犬の名前を変えることは、連鎖によって達成されることができますreplace 呼び出し
    black_doggie = replace(doggie, attributes=replace(doggie.attributes, colour="black"))
    
    深くネストした構造でreplace することができます退屈な.関数型プログラミング言語では、深くネストされた構造を「つぶす」というこの問題はoptics libraries 例えば lens ハスケルで.Pythonには独自のものがあります lenses パッケージ、しかし、私はそれが現実世界でそこに有用であるとわかりませんでした:静的タイプチェックの力なしで、レンズの過度使用は非常に不可解なコードに帰属することができます.
    それは、私が遭遇したほとんどのデータ構造はreplace 特に問題はありません.特にヘルパー関数の使用時に
    def change_doggie_colour(doggie: Doggie, new_colour: str):
      return replace(doggie, 
                     attributes=replace(doggie.attributes, 
                                        colour=new_colour))
    

    あなたのリストと辞書を不変にしておきなさい
    不変性のトピックについて続けていますが、我々はまだ、変更可能なリストと辞書でPythonコードで動作する必要があります.彼らがmutableであるので、それは我々が彼らを変異させる必要があるというわけではありません.私が場所でリストを突然変異しようとしているとわかるときはいつでもl.append(value) または辞書d["key"] = value , それが本当に必要であるならば、私は止まって、考えます.そのような操作はHaskellのような純粋な機能言語で利用できないでしょう、したがって、適所にオブジェクトを突然変異することなくコードを書くことは確実に可能です.ここでは免れますか.
    私は新しいリストや辞書を作成している場合、多分私は代わりにリストや辞書の理解を使用することができます.既存のリストに値を追加する必要がある場合は、単に位置展開の助けを借りて、まったく新しい新しいオブジェクトを作成する方が良いかもしれません.
    new_list = [*old_list, value]
    
    同様に、既存の辞書にキーを追加する代わりに、キーワード拡張機能を持つ新しい辞書を作成できます.
    new_dict = { **old_dict, "key": value }
    
    リストのみを読み取り専用として扱う利点は、Pythonの typing システム(我々は)スタティック型チェックのために、読み込み専用 typing.Sequence mutableの代わりにリストのためのタイプ typing.List 種類だってtyping.Sequence[T] 型の値の読み込み専用コレクションですT , それはcovariant インT :

    typing.Sequence[A] <: typing.Sequence[B] if A <: B


    これは、我々が使うことができることを意味しますtyping.Sequence[A] 今までtyping.Sequence[B] は、A のサブタイプB . 同様の理由で、読み取り専用を使用する方が良いです typing.Mapping の代わりに typing.Dict .
    既存のものを変異するのではなく、新しいオブジェクトを作成するかどうかを決めるのはあなたのプログラムの正しい解決策です.パフォーマンスクリティカルケースでは、新しいオブジェクトを作成する際のオーバーヘッドを被る代わりに、リストを単純に変異させる方が良いでしょう.同様に、リストを作成する場合、データベースへの書き込みなどの副作用が含まれている場合、読みやすさのためにリストの理解を避ける方が良いでしょう.

    継承の代わりにuse unionを使用します.
    The Pragmatic Programmer 本は遺伝的に興味深いパラグラフを持っています.

    "Do you program in an object-oriented language? Do you use inheritance? If so, stop! It probably isn't what you want to do."


    Pythonでの継承を避ける方法タイプのオブジェクトで動作したいと仮定しましょうDoggie and Cat . 両方Doggie and Cat を持っているsay() メソッドを使用して、そのような動物を(不変の)リストのようなコンテナに置きたい.古典的なプログラミング101のソリューションは、スーパークラスを作成することですAnimal そして継承するDoggie and Cat からAnimal :
    import abc
    
    class Animal:
      def say(self) -> str:
        raise NotImplementedError()
    
    class Doggie(Animal):
      def say(self) -> str:
        return "Woof!"
    
    class Cat(Animal):
      def say(self) -> str:
        return "Meow!"
    
    古典的な多型を得る
    import typing
    
    def all_animals_say(animals: typing.Sequence[Animal]):
      for animal in animals:
        animal.say()
    
    しかしながら、我々はAを使用することによって継承の問題のどれもなしで同じことを成し遂げることができますtype union :
    BetterAnimal = typing.Union[Doggie, Cat]
    
    def all_better_animals_say(animals: typing.Sequence[BetterAnimal]):
        for animal in animals:
            print(animal.say())
    
    クラス内のクラスBetterAnimal を持っていないsay() 適切なタイプの方法、タイプチェッカーは文句を言うでしょう.
    タイプ規約は継承のためのユースケースの1つのクラスだけをカバーするので、タイプユニオンを通して常に継承を取り除くことができるとは思いません.上の例は、「悪い継承」の良い例ではありませんでしたAnimal 抽象的なメソッドを抽象クラスに、任意のハードコード化された行動なしにそれを回すことによって“インターフェイス”.私がしようとしているポイントは:常に継承を使用する前に考えるのを停止します.

    結論
    これはPython用の関数型プログラミングヒントのリストです.他のヒント、コメントや質問がある場合は、コメントを残してください!読書ありがとう!