Rのmagritrrからの%>%のようなPythonの機能パイプ


84

R(のおかげでmagritrr)では、を介してより機能的な配管構文で操作を実行できるようになりました%>%。これは、これをコーディングする代わりに、次のことを意味します。

> as.Date("2014-01-01")
> as.character((sqrt(12)^2)

これを行うこともできます:

> "2014-01-01" %>% as.Date 
> 12 %>% sqrt %>% .^2 %>% as.character

私にとってこれはより読みやすく、これはデータフレームを超えたユースケースにまで及びます。Python言語は同様のものをサポートしていますか?


1
素晴らしい質問です。関数に引数が多い場合に特に興味があります。同様にcrime_by_state %>% filter(State=="New York", Year==2005) ...年末からdplyrは私の最も一般的なRイディオムを置き換える方法
Piotr Migdal 2015

1
もちろん、多くのラムダ、マップ、リデュースを使用してそれを行うことができます(そして、そうするのは簡単です)が、簡潔さと読みやすさが重要なポイントです。
Piotr Migdal 2015

12
問題のパッケージはmagrittrです。
piccolbo 2015

1
はい、同じ理由で、これまでに作成されたすべてのRパッケージはHadleyによって作成されました。彼はもっと知られています。(ここでは、羨望の的を隠したアラート)
piccolbo 2015

1
この問題を解決しているstackoverflow.com/questions/33658355/…への回答を参照してください。
Piotr Migdal 2016年

回答:


34

これを行う1つの可能な方法は、と呼ばれるモジュールを使用することです。 macropy。Macropyを使用すると、作成したコードに変換を適用できます。したがって、a | bに変換できますb(a)。これには多くの長所と短所があります。

Sylvain Lerouxが述べたソリューションと比較すると、主な利点は、使用したい関数のインフィックスオブジェクトを作成する必要がないことです。つまり、変換を使用するコードの領域をマークするだけです。次に、変換は実行時ではなくコンパイル時に適用されるため、変換されたコードは実行時にオーバーヘッドを被りません。すべての作業は、バイトコードが最初にソースコードから生成されたときに行われます。

主な欠点は、macropyが機能するために特定の方法をアクティブにする必要があることです(後述)。より高速なランタイムとは対照的に、ソースコードの解析は計算がより複雑であるため、プログラムの開始に時間がかかります。最後に、マクロピーに精通していないプログラマーがコードを理解しにくいと感じる可能性があることを意味する構文スタイルを追加します。

コード例:

run.py

import macropy.activate 
# Activates macropy, modules using macropy cannot be imported before this statement
# in the program.
import target
# import the module using macropy

target.py

from fpipe import macros, fpipe
from macropy.quick_lambda import macros, f
# The `from module import macros, ...` must be used for macropy to know which 
# macros it should apply to your code.
# Here two macros have been imported `fpipe`, which does what you want
# and `f` which provides a quicker way to write lambdas.

from math import sqrt

# Using the fpipe macro in a single expression.
# The code between the square braces is interpreted as - str(sqrt(12))
print fpipe[12 | sqrt | str] # prints 3.46410161514

# using a decorator
# All code within the function is examined for `x | y` constructs.
x = 1 # global variable
@fpipe
def sum_range_then_square():
    "expected value (1 + 2 + 3)**2 -> 36"
    y = 4 # local variable
    return range(x, y) | sum | f[_**2]
    # `f[_**2]` is macropy syntax for -- `lambda x: x**2`, which would also work here

print sum_range_then_square() # prints 36

# using a with block.
# same as a decorator, but for limited blocks.
with fpipe:
    print range(4) | sum # prints 6
    print 'a b c' | f[_.split()] # prints ['a', 'b', 'c']

そして最後に、ハードワークを行うモジュール。あるプロセスから別のプロセスに出力を渡すためのエミュレートシェル構文として、機能パイプのfpipeと呼んでいます。

fpipe.py

from macropy.core.macros import *
from macropy.core.quotes import macros, q, ast

macros = Macros()

@macros.decorator
@macros.block
@macros.expr
def fpipe(tree, **kw):

    @Walker
    def pipe_search(tree, stop, **kw):
        """Search code for bitwise or operators and transform `a | b` to `b(a)`."""
        if isinstance(tree, BinOp) and isinstance(tree.op, BitOr):
            operand = tree.left
            function = tree.right
            newtree = q[ast[function](ast[operand])]
            return newtree

    return pipe_search.recurse(tree)

2
素晴らしいように聞こえますが、私が見る限り、Python 2.7でのみ機能します(Python 3.4では機能しません)。
Piotr Migdal

3
@fpipeデコレータと同じことを行うが、または(|)の代わりに右シフト(>>)を再定義する、依存関係のない小さなライブラリを作成しました:pypi.org/project/pipeop
Robin Hilliard

複数のデコレータを使用してサードパーティのライブラリを必要とすることは、かなり単純な問題に対する非常に複雑な解決策であると否定されています。さらに、Python2のみのソリューションです。バニラPythonソリューションもより速くなることは間違いありません。
jramm

37

パイプはPandas0.16.2の新機能です

例:

import pandas as pd
from sklearn.datasets import load_iris

x = load_iris()
x = pd.DataFrame(x.data, columns=x.feature_names)

def remove_units(df):
    df.columns = pd.Index(map(lambda x: x.replace(" (cm)", ""), df.columns))
    return df

def length_times_width(df):
    df['sepal length*width'] = df['sepal length'] * df['sepal width']
    df['petal length*width'] = df['petal length'] * df['petal width']

x.pipe(remove_units).pipe(length_times_width)
x

注意:Pandasバージョンは、Pythonの参照セマンティクスを保持しています。そのlength_times_widthため、戻り値は必要ありません。そのx場で変更します。


4
残念ながら、これはデータフレームに対してのみ機能するため、これを正解として割り当てることはできません。しかし、私が念頭に置いていた主なユースケースとしてここで言及するのは、これをデータフレームに適用することでした。
cantdutchthis 2015年

22

PyToolz [doc]は、任意に構成可能なパイプを許可しますが、それらはそのパイプ演算子構文で定義されていません。

クイックスタートについては、上記のリンクをたどってください。そしてここにビデオチュートリアルがあります:http//pyvideo.org/video/2858/functional-programming-in-python-with-pytoolz

In [1]: from toolz import pipe

In [2]: from math import sqrt

In [3]: pipe(12, sqrt, str)
Out[3]: '3.4641016151377544'

1
PyToolzは素晴らしいポインタです。一方のリンクが死んでいて、もう一方のリンクがすぐに死んでいると言った
2016年

2
彼のベースURLは次のようです:http//matthewrocklin.com/blogおよびPyToolztoolz.readthedocs.io/en/latest。ああ、インターネットの短命...
smci 2016

18

Python言語は同様のものをサポートしていますか?

「より機能的な配管構文」これは本当により「機能的な」構文ですか?代わりに、Rに「中置」構文を追加すると思います。

そうは言っても、Pythonの文法は、標準の演算子を超えて中置記法を直接サポートしていません。


そのようなものが本当に必要な場合は、独自の中置記法を実装するための開始点として、TomerFilibaからそのコードを取得する必要があります。

Tomer Filibaによるコードサンプルとコメント(http://tomerfiliba.com/blog/Infix-Operators/):

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)

この固有のクラスのインスタンスを使用して、関数を中置演算子として呼び出すための新しい「構文」を使用できるようになりました。

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

18

個人的なスクリプトでこれが必要な場合は、Coconutの使用を検討することをお勧めします、Pythonの代わりに。

ココナッツはPythonのスーパーセットです。したがって、Coconutのパイプ演算子を使用できます|>、Coconut言語の残りの部分を完全に無視しながら、。

例えば:

def addone(x):
    x + 1

3 |> addone

にコンパイルされます

# lots of auto-generated header junk

# Compiled Coconut: -----------------------------------------------------------

def addone(x):
    return x + 1

(addone)(3)

print(1 |> isinstance(int))... TypeError:isinstanceは2つの引数を期待し、1を取得しました
nyanpasu64 2018年

1
@あなたはまだ、この問題を持ってしようとした場合jimbo1qazprint(1 |> isinstance$(int))好ましく、または、1 |> isinstance$(int) |> print
ソロモンウッコ2018

@SolomonUckoあなたの答えは間違っています。$はPythonパーシャルにマップされるため、1 |> print$(2)呼び出しますprint(2, 1)。しかし、print(1, 2)UFCSとmagrittrに一致するものが欲しいです。動機:1 |> add(2) |> divide(6)0.5である必要があり、括弧は必要ありません。
nyanpasu 6419

@ jimbo1qazええ、私の前のコメントが間違っているようです。あなたは実際に必要になるでしょう1 |> isinstance$(?, int) |> print。他の例の場合:1 |> print$(?, 2)1 |> (+)$(?, 2) |> (/)$(?, 6)。部分適用の括弧は避けられないと思います。
ソロモンウッコ

どのように両方の醜いを見てみる|>(+)$(?, 2)、私はプログラミング言語と数学の設立は、私は、構文のこのタイプを使用したくないという結論に来て、括弧のセットに頼るよりもさらに醜いことになりました。構文が優れている場合(たとえば、DlangにはUFCSがありますが、算術関数についてはIDKがある場合、またはPythonに..パイプ演算子がある場合)に使用します。
nyanpasu 6419

11

有る dfplyモジュールます。あなたはでより多くの情報を見つけることができます

https://github.com/kieferk/dfply

いくつかの例は次のとおりです。

from dfply import *
diamonds >> group_by('cut') >> row_slice(5)
diamonds >> distinct(X.color)
diamonds >> filter_by(X.cut == 'Ideal', X.color == 'E', X.table < 55, X.price < 500)
diamonds >> mutate(x_plus_y=X.x + X.y, y_div_z=(X.y / X.z)) >> select(columns_from('x')) >> head(3)

私の意見では、これは正解としてマークされるべきです。また、dfplydplythonは両方とも同じパッケージのようです。それらの間に違いはありますか?@BigDataScientist
InfiniteFlash

dfplydplythonplydataパッケージはのPythonのポートでdplyr、彼らは構文にかなり似ているように行っているので、パッケージ。
BigDataScientist

9

|>Elixirのパイプ演算子を見逃したので>>、astライブラリとcompile / execを使用して、コンパイル時にPythonの右シフト演算子を非常にElixirのようなパイプとして再解釈する単純な関数デコレータ(約50行のコード)を作成しました。

from pipeop import pipes

def add3(a, b, c):
    return a + b + c

def times(a, b):
    return a * b

@pipes
def calc()
    print 1 >> add3(2, 3) >> times(4)  # prints 24

それがしているのは、次のように書き直すことa >> b(...)だけです。b(a, ...)です。

https://pypi.org/project/pipeop/

https://github.com/robinhilliard/pipes


9

sspipeライブラリを使用できます。2つのオブジェクトpとを公開しますpx。と同様にx %>% f(y,z)、書くことができx | p(f, y, z)、同様にx %>% .^2書くことができますx | px**2

from sspipe import p, px
from math import sqrt

12 | p(sqrt) | px ** 2 | p(str)

8

で構築pipeするInfix

Sylvain Lerouxが示唆しているように、Infix演算子を使用して中置辞を作成できますpipe。これがどのように達成されるか見てみましょう。

まず、これがTomerFilibaのコードです

Tomer Filibaによるコードサンプルとコメント(http://tomerfiliba.com/blog/Infix-Operators/):

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)

この固有のクラスのインスタンスを使用して、関数を中置演算子として呼び出すための新しい「構文」を使用できるようになりました。

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

パイプ演算子は、先行するオブジェクトを引数としてパイプに続くオブジェクトに渡すため、x %>% fに変換できますf(x)。したがって、pipe演算子はInfix次のように定義できます。

In [1]: @Infix
   ...: def pipe(x, f):
   ...:     return f(x)
   ...:
   ...:

In [2]: from math import sqrt

In [3]: 12 |pipe| sqrt |pipe| str
Out[3]: '3.4641016151377544'

部分適用に関する注記

%>%演算子dpylr関数の最初の引数を通じてプッシュ引数なので、

df %>% 
filter(x >= 2) %>%
mutate(y = 2*x)

に対応

df1 <- filter(df, x >= 2)
df2 <- mutate(df1, y = 2*x)

Pythonで同様のことを実現する最も簡単な方法は、カリー化を使用することです。toolzライブラリが提供してcurry簡単にカリー化機能を構築可能デコレータ機能を。

In [2]: from toolz import curry

In [3]: from datetime import datetime

In [4]: @curry
    def asDate(format, date_string):
        return datetime.strptime(date_string, format)
    ...:
    ...:

In [5]: "2014-01-01" |pipe| asDate("%Y-%m-%d")
Out[5]: datetime.datetime(2014, 1, 1, 0, 0)

|pipe|引数を最後の引数位置プッシュすることに注意してください。

x |pipe| f(2)

に対応

f(2, x)

カレー関数を設計するときは、静的引数(つまり、多くの例で使用される可能性のある引数)をパラメーターリストの前に配置する必要があります。

モジュールのtoolzさまざまな関数を含む、多くの事前に実行された関数が含まれていることに注意してくださいoperator

In [11]: from toolz.curried import map

In [12]: from toolz.curried.operator import add

In [13]: range(5) |pipe| map(add(2)) |pipe| list
Out[13]: [2, 3, 4, 5, 6]

これはRで次のように大まかに対応します

> library(dplyr)
> add2 <- function(x) {x + 2}
> 0:4 %>% sapply(add2)
[1] 2 3 4 5 6

他の中置区切り文字の使用

他のPython演算子メソッドをオーバーライドすることで、Infix呼び出しを囲むシンボルを変更できます。例えば、スイッチング__or__および__ror____mod____rmod__変化する|にオペレータをmodオペレータ。

In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: '3.4641016151377544'

6

私の2cを追加します。私は個人的に関数型プログラミングにパッケージfnを使用しています。あなたの例は

from fn import F, _
from math import sqrt

(F(sqrt) >> _**2 >> str)(12)

F部分適用と構成のための関数型構文糖衣構文を持つラッパークラスです。_匿名関数用のScalaスタイルのコンストラクターです(Pythonと同様lambda)。これは変数を表すため_、1つの式で複数のオブジェクトを組み合わせて、より多くの引数を持つ関数を取得できます(たとえば、_ + _と同等lambda a, b: a + b)。F(sqrt) >> _**2 >> str結果Callableとして、必要な回数だけ使用できるオブジェクトが作成されます。


まさに私が探しているもの-例としてscalaについても言及しました。今すぐお試しください
StephenBoesch 2018

@javadbaこれがお役に立ててうれしいです。これ_は100%柔軟ではないことに注意してください。すべてのPython演算子をサポートしているわけではありません。さらに、_インタラクティブセッションでの使用を計画している場合は、別の名前(たとえばfrom fn import _ as var)でインポートする必要があります。ほとんどの(すべてではないにしても)インタラクティブPythonシェル_は、最後の割り当てられていない戻り値を表すために使用し、インポートされたオブジェクトをシャドウイングするためです。
Eli Korvigo 2018年

5

パイプ関数を実装するために、サードパーティのライブラリや紛らわしいオペレーターのトリックは必要ありません。基本を自分で簡単に実行できます。

パイプ関数が実際に何であるかを定義することから始めましょう。本質的には、標準の「裏返し」の順序ではなく、論理的な順序で一連の関数呼び出しを表現する方法にすぎません。

たとえば、次の関数を見てみましょう。

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value

あまり面白くありませんが、面白いことが起こっていると仮定しvalueます。それらを順番に呼び出し、それぞれの出力を次の出力に渡します。バニラパイソンでは、次のようになります。

result = three(two(one(1)))

それは信じられないほど読みやすくはなく、より複雑なパイプラインの場合は悪化します。したがって、これは最初の引数を取る単純なパイプ関数と、それを適用する一連の関数です。

def pipe(first, *args):
  for fn in args:
    first = fn(first)
  return first

それを呼びましょう:

result = pipe(1, one, two, three)

それは私には非常に読みやすい「パイプ」構文のように見えます:)。演算子のオーバーロードなどよりも読みにくいとは思いません。実際、私はそれがより読みやすいPythonコードであると主張します

OPの例を解決する謙虚なパイプは次のとおりです。

from math import sqrt
from datetime import datetime

def as_date(s):
  return datetime.strptime(s, '%Y-%m-%d')

def as_character(value):
  # Do whatever as.character does
  return value

pipe("2014-01-01", as_date)
pipe(12, sqrt, lambda x: x**2, as_character)

3

代替ソリューションの1つは、ワークフローツールdaskを使用することです。構文的にはそれほど楽しいものではありませんが...

var
| do this
| then do that

...それでも変数がチェーンを流れることができ、daskを使用すると、可能な場合は並列化の追加の利点が得られます。

daskを使用してパイプチェーンパターンを作成する方法は次のとおりです。

import dask

def a(foo):
    return foo + 1
def b(foo):
    return foo / 2
def c(foo,bar):
    return foo + bar

# pattern = 'name_of_behavior': (method_to_call, variables_to_pass_in, variables_can_be_task_names)
workflow = {'a_task':(a,1),
            'b_task':(b,'a_task',),
            'c_task':(c,99,'b_task'),}

#dask.visualize(workflow) #visualization available. 

dask.get(workflow,'c_task')

# returns 100

elixirを使用した後、Pythonでパイピングパターンを使用したいと思いました。これは完全に同じパターンではありませんが、類似しており、私が言ったように、並列化の追加の利点があります。daskに、最初に実行する他のタスクに依存しないワークフロー内のタスクを取得するように指示すると、それらは並行して実行されます。

より簡単な構文が必要な場合は、タスクの命名を処理するものでラップすることができます。もちろん、この状況では、パイプを最初の引数として使用するためにすべての関数が必要になり、並列化の利点が失われます。しかし、それでよければ、次のようなことができます。

def dask_pipe(initial_var, functions_args):
    '''
    call the dask_pipe with an init_var, and a list of functions
    workflow, last_task = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})
    workflow, last_task = dask_pipe(initial_var, [function_1, function_2])
    dask.get(workflow, last_task)
    '''
    workflow = {}
    if isinstance(functions_args, list):
        for ix, function in enumerate(functions_args):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1))
        return workflow, 'task_' + str(ix)
    elif isinstance(functions_args, dict):
        for ix, (function, args) in enumerate(functions_args.items()):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1), *args )
        return workflow, 'task_' + str(ix)

# piped functions
def foo(df):
    return df[['a','b']]
def bar(df, s1, s2):
    return df.columns.tolist() + [s1, s2]
def baz(df):
    return df.columns.tolist()

# setup 
import dask
import pandas as pd
df = pd.DataFrame({'a':[1,2,3],'b':[1,2,3],'c':[1,2,3]})

これで、このラッパーを使用して、次の構文パターンのいずれかに従ってパイプを作成できます。

# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})

このような:

# test 1 - lists for functions only:
workflow, last_task =  dask_pipe(df, [foo, baz])
print(dask.get(workflow, last_task)) # returns ['a','b']

# test 2 - dictionary for args:
workflow, last_task = dask_pipe(df, {foo:[], bar:['string1', 'string2']})
print(dask.get(workflow, last_task)) # returns ['a','b','string1','string2']

これに関する1つの問題は、関数を引数として渡すことができないことです:(
合法スタック

2

pipeここに非常に素晴らしいモジュールがありますhttps://pypi.org/project/pipe/ それはオーバーロードします| オペレーターとのような多くのパイプ機能を提供しますadd, first, where, tail

>>> [1, 2, 3, 4] | where(lambda x: x % 2 == 0) | add
6

>>> sum([1, [2, 3], 4] | traverse)
10

さらに、独自のパイプ関数を作成するのは非常に簡単です

@Pipe
def p_sqrt(x):
    return sqrt(x)

@Pipe
def p_pr(x):
    print(x)

9 | p_sqrt | p_pr

0

パイプ機能は、パンダメソッドをドットで構成することで実現できます。以下に例を示します。

サンプルデータフレームをロードします。

import seaborn    
iris = seaborn.load_dataset("iris")
type(iris)
# <class 'pandas.core.frame.DataFrame'>

パンダメソッドの構成をドットで示します。

(iris.query("species == 'setosa'")
     .sort_values("petal_width")
     .head())

必要に応じて、パンダのデータフレームに新しいメソッドを追加できます(たとえば、ここで行うように)。

pandas.DataFrame.new_method  = new_method
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.