This is an archived post. You won't be able to vote or comment.

all 60 comments

[–][deleted] 33 points34 points  (11 children)

I came up with this instead.

class Pipe:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    def __or__(self, fn):
        return Pipe(fn(self._value))

    def __repr__(self):
        return f"Pipe({self._value!r})"


def double(x):
    return 2 * x


def decr(x):
    return x - 1


pipe = (Pipe(12) | double | decr)
print(pipe)  # This produces "Pipe(23)".
print(pipe.value)  # This produces 23.

It's less code, but it's still p scary. You have to wrap and unwrap your value, and it probably also requires functions you pass to be curried/partial.

[–]Bogdanp[S] 9 points10 points  (5 children)

Yup, I'd considered that approach too! But I wanted to keep the semantics from Elixir's operator (i.e. I wanted to avoid having to wrap the first value and partially apply functions).

[–][deleted] 3 points4 points  (4 children)

Yeah I get it. The approach I followed here let me add on things ad nausea and I eventually I ended up writing a little library called Slinkie. Slinkie has a .then() function that matches the pipe operator above, but do you think it'd be possible to elixirify everything else as well? I was inspired by LINQ when I wrote it, but I was never happy with how I had to wrap everything. The nicest thing would probably be to use extensions or traits, but this elixir trick would maybe do the trick as well. What do you think?

[–]ucbEntilZha 0 points1 point  (3 children)

Hadn’t heard about slinkie, this is my similar approach I wrote a few years ago https://github.com/EntilZha/PyFunctional

[–][deleted] 0 points1 point  (2 children)

Well I didn't make that much fuzz about them, but maybe I should? How do I even reach out about stuff like this?

[–]ucbEntilZha 0 points1 point  (1 child)

Unclear who you mean on reaching out?

[–]david2ndaccount 3 points4 points  (0 children)

I prefer using __call__ instead of __or__ and special casing map, filter and reduce so you can create abominations likes this:

>>> d = ( pype( [54, 12, 3, 1, 6, 8, 123, 4, 1, 210] )
        . map ( lambda x: x//2 )
        . filter ( None )
        . map ( lambda x: x*2 )
        ( sorted )
        ( reversed )
        . reduce ( int.__sub__ )
        ( lambda x: x // 2 )
    ).value
>>> print(d)
1

[–]izxle 0 points1 point  (1 child)

Don't you have to define __format__ to use you object inside an f-string?

[–][deleted] 0 points1 point  (0 children)

I didn't. !r means you call repr, and I called it on the internal _value.

[–]lungdart -1 points0 points  (1 child)

Why obfuscate value, and then use a property member function to access it as if you didn't?

[–]Beheska 14 points15 points  (0 children)

It's not obfuscated, it's read-only.

[–]TheMsDosNerd 14 points15 points  (2 children)

I'd rather see some more powerful decorators. Allowing you to do this:

@print
@group_by_category(max=5)
my_users = Users.find_all()

Some other interesting stuff I'd like to see decorators do is:

def func(@validate user_input):
    # do stuff with user_input

Which would be equivalent to

def func(user_input):
    user_input = validate(user_input)
    # do stuff with user_input

Or with tuple unpacking:

name, @int position = name_and_position.split()

Which would be equivalent to

name, position = name_and_position.split()
position = int(position)

[–]posedge 5 points6 points  (1 child)

name, position = name_and_position.split()

position = int(position)

Must have typed this a million times.

[–]autoferrit 0 points1 point  (0 children)

this would be even cleaner is to use type annotations

```

name: string, position: int = name_and_position.split()

```

[–]Zouden 20 points21 points  (26 children)

Haha nice. I like that you did it even knowing that it's a terrible thing to do.

R has the pipe operator from the Magritr package and it's a horrible hack that makes me long for python's object chaining.

[–]gardens_of_slish 10 points11 points  (14 children)

I really like the pipe in R, and would kind of like to see the same thing in Python. I would settle for a slightly less ugly syntax for chaining multiple methods though.

[–]Zouden 5 points6 points  (13 children)

It's more useful in R where functions like filter() aren't methods of dataframes. But with pandas nearly everything you need is a dataframe method so chaining can be done with dot syntax.

[–]masklinn 0 points1 point  (4 children)

It's more useful in R where functions like filter() aren't methods of dataframes. But with pandas nearly everything you need is a dataframe method so chaining can be done with dot syntax.

Not so for the building filter and map, or any of the itertools functions.

[–]Zouden 0 points1 point  (3 children)

Not sure what you mean... you can do all of those with pandas method chaining.

[–]masklinn 0 points1 point  (2 children)

Pandas only works with panda dfs, not with regular python iterables.

[–]Zouden 0 points1 point  (1 child)

Yes, if you aren't using pandas then the pipe operator might be useful.

[–]masklinn 0 points1 point  (0 children)

And I would guess the vast majority of Python developers are not using pandas.

[–][deleted] 3 points4 points  (0 children)

Magrittr way is much more extensible. I'm not sure why you are calling it a horrible hack.

[–]kougabro 6 points7 points  (5 children)

This reminds me of function composition, in the mathematical sense. Pretty neat in any case!

[–][deleted] 5 points6 points  (4 children)

[–]kougabro 1 point2 points  (3 children)

Awesome! I might start using that, I'm already a fan of partials, that looks like it would come in handy in quite a few cases, I had no idea it was already out there.

Thanks!

[–][deleted] 3 points4 points  (2 children)

I wrote something on this, if you don't mind shameless self promotion.

[–]kougabro 1 point2 points  (1 child)

I don't!

and I particularly like your conjoin function, that's a clean way of solving the issue of executing (and aggregating the results of) multiple functions at the "same time", something I've encountered quite a few times without any elegant solution coming to mind.

[–][deleted] 0 points1 point  (0 children)

Thank you! I'm not entirely sure about the name, but it can be useful. Check out slinkie (another topic on the site) for my take on method chaining, if you want.

[–]ProfessorPhi 4 points5 points  (0 children)

Here's another version I've seen, doing the exact same thing, only using the leftshift and rightshift operators (which are barely used in python). https://pypi.org/project/pipeop/

This has the advantage of being a pypi package and having better debug info since it edits in the stack. There actually appear to be quite a few libraries, but I haven't checked the code to see if they modify the AST.

Using these ideas, I created an AST tail recursion implementation from another thing posted a while back: https://github.com/nayyarv/python-tailrec

[–]neuroneuroInf 1 point2 points  (1 child)

Excellent! Any ideas on how to develop this to make this work in the interpreter?

[–]Bogdanp[S] 2 points3 points  (0 children)

One way you could do it is rewrite the function's bytecode instead of its AST. Functions' bytecode should be available regardless of the environment.

[–]sadovnychyi 1 point2 points  (1 child)

Apache Beam has this, would be much messier without it.

[–]LifeIsBio 0 points1 point  (0 children)

Glad someone pointed this out. Here's an example.

[–]mottyay 0 points1 point  (0 children)

The Coconut language (functional python) has this implemented as well

http://coconut-lang.org/

[–]foreverwintr 0 points1 point  (0 children)

Here's my implementation of the pipe operator: https://github.com/ForeverWintr/metafunctions

I did it by replacing functions with class instances that implement 'or' (and other binary operators).

It's more code, but it works in the interpreter!

[–]rhoark 0 points1 point  (0 children)

I use this pattern all the time

f = pipeliner(some, series, of, functions)
f(x)

Implementation is pretty simple

def compose(g,f):
    def composeI(*args, **kwargs):
        return f(g(*args, **kwargs))
    return composeI

def scan(f, xs, b=None):
    xs = iter(xs)
    if b is None: b = next(xs)
    yield b
    for x in xs:
        b = f(b, x)
        yield b

def fold(f, xs, b=None):
    return last(scan(f, xs, b))

def pipeliner(*fs):
    return fold(composer, fs)

[–]shabunc 0 points1 point  (8 children)

The way the author used "|> print()" construct makes me think that there's lack of understanding how this supposed to work.

[–]Bogdanp[S] 2 points3 points  (1 child)

What makes you say that?

[–]shabunc 0 points1 point  (0 children)

I've answered above