-
Notifications
You must be signed in to change notification settings - Fork 265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Many improvements to curry
based on #147 #150 #155 #159
#170
Conversation
…#155 pytoolz#159 1. Idempotent such that `curry(curry(f))` is equivalent to `curry(f)`; see pytoolz#147 2. Comparable via equality and hashable based on `func`, `args`, and `keywords`; see pytoolz#159 3. `func`, `args`, and `keywords` attributes are readonly 4. `__name__` and `__doc__` attributes are writable 5. More efficient implementation using `partial`; see pytoolz#150 6. Curried objects never transmogrigy into `partial` objects; see pytoolz#155
assert not isinstance(g.func, curry) | ||
assert not hasattr(g.func, 'func') | ||
# curry makes a new curry object, so everything is distinct but equal | ||
assert f is not g |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually want to assert this? Aren't there valid solutions that allow this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I'll remove the "distinct" checks.
In general I like these changes. The one thing I'm not super excited about is including |
Simple: it is a faster way to implement
I had proposed
Even now that
Fair enough. You've said this enough times now to convince me too :) |
So it's not a performance issue, just more convenient for implementation? |
It is a performance issue. Using |
But we still use standard calling syntax. Can you back this up with a tiny benchmark? - return self.func(*args, **kwargs)
+ return self._partial(*args, **kwargs) |
Certainly. I'll be a few minutes though.
Yup. That's not where most of the speed improvement comes from. By using args = self.args + args
if _kwargs:
kwargs = {}
if self.keywords:
kwargs.update(self.keywords)
kwargs.update(_kwargs)
elif self.keywords:
kwargs = self.keywords
else:
kwargs = {} |
Ah, now I understand. |
Benchmarks: In [2]: def f(*args, **kwargs):
...: pass
...:
In [3]: new_f1 = new_curry(f)
In [4]: old_f1 = old_curry(f)
In [5]:
In [5]: %timeit new_f1()
1000000 loops, best of 3: 1 µs per loop
In [6]: %timeit old_f1()
1000000 loops, best of 3: 1.39 µs per loop
In [7]: %timeit new_f1(1)
1000000 loops, best of 3: 1.19 µs per loop
In [8]: %timeit old_f1(1)
1000000 loops, best of 3: 1.56 µs per loop
In [9]: %timeit new_f1(1, 2)
1000000 loops, best of 3: 1.15 µs per loop
In [10]: %timeit old_f1(1, 2)
100000 loops, best of 3: 1.62 µs per loop
In [11]: %timeit new_f1(x=1)
1000000 loops, best of 3: 1.54 µs per loop
In [12]: %timeit old_f1(x=1)
100000 loops, best of 3: 2.63 µs per loop
In [13]: %timeit new_f1(x=1, y=2)
100000 loops, best of 3: 1.83 µs per loop
In [14]: %timeit old_f1(x=1, y=2)
100000 loops, best of 3: 2.91 µs per loop
In [15]:
In [15]: new_f2 = new_curry(f, 1, 2)
In [16]: old_f2 = old_curry(f, 1, 2)
In [17]:
In [17]: %timeit new_f2()
1000000 loops, best of 3: 1.03 µs per loop
In [18]: %timeit old_f2()
1000000 loops, best of 3: 1.44 µs per loop
In [19]: %timeit new_f2(3)
1000000 loops, best of 3: 1.22 µs per loop
In [20]: %timeit old_f2(3)
1000000 loops, best of 3: 1.59 µs per loop
In [21]: %timeit new_f2(3, 4)
1000000 loops, best of 3: 1.29 µs per loop
In [22]: %timeit old_f2(3, 4)
1000000 loops, best of 3: 1.64 µs per loop
In [23]: %timeit new_f2(x=3)
100000 loops, best of 3: 1.8 µs per loop
In [24]: %timeit old_f2(x=3)
100000 loops, best of 3: 2.69 µs per loop
In [25]: %timeit new_f2(x=3, y=4)
100000 loops, best of 3: 1.87 µs per loop
In [26]: %timeit old_f2(x=3, y=4)
100000 loops, best of 3: 2.97 µs per loop
In [27]:
In [27]: new_f3 = new_curry(f, a=1, b=2)
In [28]: old_f3 = old_curry(f, a=1, b=2)
In [29]:
In [29]: %timeit new_f3()
1000000 loops, best of 3: 1.38 µs per loop
In [30]: %timeit old_f3()
1000000 loops, best of 3: 1.63 µs per loop
In [31]: %timeit new_f3(3)
1000000 loops, best of 3: 1.55 µs per loop
In [32]: %timeit old_f3(3)
100000 loops, best of 3: 1.85 µs per loop
In [33]: %timeit new_f3(3, 4)
100000 loops, best of 3: 1.74 µs per loop
In [34]: %timeit old_f3(3, 4)
100000 loops, best of 3: 1.92 µs per loop
In [35]: %timeit new_f3(x=3)
100000 loops, best of 3: 2.16 µs per loop
In [36]: %timeit old_f3(x=3)
100000 loops, best of 3: 3.45 µs per loop
In [37]: %timeit new_f3(x=3, y=4)
100000 loops, best of 3: 2.34 µs per loop
In [38]: %timeit old_f3(x=3, y=4)
100000 loops, best of 3: 3.73 µs per loop |
Seems convincing to me! |
Great!
Do you want to rename it to |
I don't have an opinion on this either way |
I'd stick with I consider this PR ready to merge, but I'll let you do the honors when you're ready. |
args = func.args + args | ||
func = func.func | ||
|
||
if kwargs: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this check? Why not just always throw in the kwargs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a performance tweak. partial
is faster when keywords
is None
instead of {}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that I don't care much about performance tweaks during function creation as I assume that these will happen only a few times. In these cases I think that code cleanliness is more important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but this performance tweak isn't for initialization, it is for usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand. This is only going to happen whenever I add new args onto the curry
, right? I guess that in my use of curried functions this happens only a few times. Can you supply a motivating example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The performance increase is when the curried function gets called, not upon creating the curried object. Maybe this will clarify:
In [1]: from functools import partial
In [2]: def f():
...: pass
...:
In [3]: kwargs = {}
In [4]: f1 = partial(f, **kwargs)
In [5]: f2 = partial(f)
In [6]: f1.keywords
Out[6]: {}
In [7]: f2.keywords
In [8]: f2.keywords is None
Out[8]: True
In [9]: %timeit f1()
1000000 loops, best of 3: 379 ns per loop
In [10]: %timeit f2()
1000000 loops, best of 3: 291 ns per loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, now I understand. Alright, I guess that this makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's subtle. One would expect partial
to behave the same for f1
and f2
from above, but it doesn't.
Also, don't return `self` from `__call__`; return a new `curry` object instead.
Alright, looks good to me. +1. Merging in a bit if no comments. |
Ah, the docstring wasn't updated to show that curried objects are comparable and hashable. @mrocklin, can I leave this for you to do? Otherwise, +1 from me too. |
I'm not particularly concerned about telling users that curries are comparable and hashable. I think that common case users won't care. This is something to think about adding to the curry or serialization docs though. Merging. |
Thanks for following through on all of these issues @eriknw . This stuff takes times and care. |
Absolutely. I know how much you care about |
Also, add a test that ensures `curry` doesn't return a `partial` object if a single argument remains and keyword arguments are present. This behavior was added in pytoolz#170.
Matches curry changes from pytoolz/toolz#170 Matches reduceby changes from pytoolz/toolz#184
curry(curry(f))
is equivalent tocurry(f)
; see Makecurry
idempotent such thatcurry(curry(f)) -> curry(f)
#147func
,args
, andkeywords
;func
,args
, andkeywords
attributes are readonly__name__
and__doc__
attributes are writablepartial
; see Changecurry
to usefunctools.partial
#150partial
objects; see Cython implementation of toolz #155