-
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
Feature request: unpack #486
Comments
Thanks, @elyase, good suggestion. I'm open to the idea. The solution with lambda isn't bad imho and is what I would usually go with, but let's explore this some more. We already have from toolz import pipe, curry
@curry
def starapply(func, args, kwargs={}):
return func(*args, **kwargs)
data = ((1, 2, 3, 4), ('a', 'b', 'c', 'd'), ('🤔', '🙈', '😌', '💕'))
pipe(
data,
starapply(f),
list
) This uses a curried version of I don't think this looks bad at all. In fact, I kind of like it, and the name |
I think The only downside is that I could see someone unfamiliar with this idea misinterpreting it as:
rather than as the more correct
I think that's probably fine though. In as little as there is any convention for naming stuff that does this at all in Python, I've been thinking about names or better interfaces for this pattern for about a year now, and I think |
|
Has any progress been made on this? I currently work around this by using from pytoolz import curried, curry
def starapply(func, *args, **kwargs):
# turn func(*args, **kwargs) into f(more) -> func(*args, *more, **kwargs)
return curried.reduce(curry(func, *args, **kwargs)) demo: >>> import datetime
>>> this_year = starapply(datetime.date, 2022)
>>> args = (9, 28)
>>> this_year(args) # not *args!
datetime.date(2022, 9, 28) This works for my purposes but it won't work for functions that accept fewer args than passed in (the error depends on the return value at the point enough arguments have been passed in), plus I am not sure my version can be made into a curried function. |
Looks like there is an So you could just do from functools import partial
from apply import apply
def starcall(f):
return partial(apply, f) If that's all that's desired, then if I was the However, I'm thinking maybe there's something better we can do here! Imagine a
In other words, the signature of the callable returned by Since dictionaries can take both (As much as I'd love to test for So now we're building up to something pretty useful that also happens to be just complex and long enough to be worth "getting right" once and then reusing, but also doesn't require pulling in any fancy dependencies to get right - so a pretty good value proposition for class starcall:
__slots__ = ('function',)
def __init__(self, function):
if not callable(function):
raise TypeError('starcall argument must be callable')
self.function = function
def __call__(self, args_or_kwargs):
splat_as_kwargs = True
try:
args_or_kwargs.keys
type(args_or_kwargs).__getitem__
except AttributeError:
splat_as_kwargs = False
if splat_as_kwargs:
return self.function(**args_or_kwargs)
return self.function(*args_or_kwargs) |
Perhaps overkill, but we could also fully generalize the class Arguments:
__slots__ = ('args', 'kwargs')
def __init__(self, /, *args, **kwargs):
self.args = args
self.kwargs = kwargs
@classmethod
def wrap(cls, args_or_kwargs):
if isinstance(args_or_kwargs, Arguments):
return cls(*args_or_kwargs, **args_or_kwargs)
try:
args_or_kwargs.keys
type(args_or_kwargs).__getitem__
except AttributeError:
return cls(*args_or_kwargs)
return cls(**args_or_kwargs)
def __iter__(self):
return iter(self.args)
def keys(self):
return self.kwargs.keys()
def __getitem__(self, key):
return self.kwargs[key] And then class starcall:
__slots__ = ('function',)
def __init__(self, function):
if not callable(function):
raise TypeError('starcall argument must be callable')
self.function = function
def __call__(self, args_or_kwargs):
arguments = Arguments.wrap(args_or_kwargs)
return self.function(*arguments, **arguments) With that in place, in a return (1, 4.2) # or Arguments(1, 4.2) return {'foo': 1, 'bar': 4.2} # or Arguments(foo=1, bar=4.2) return Arguments(1, bar=4.2) to cause |
Actually I'm on the fence about class starcall:
__slots__ = ('function',)
def __init__(self, function):
if not callable(function):
raise TypeError('starcall argument must be callable')
self.function = function
def __call__(self, args_or_kwargs):
arguments = Arguments.wrap(args_or_kwargs)
return self.function(*arguments.args, **arguments.kwargs) and then the arguments class doesn't need those three special methods just to be splattable: class Arguments:
__slots__ = ('args', 'kwargs')
def __init__(self, /, *args, **kwargs):
self.args = args
self.kwargs = kwargs
@classmethod
def wrap(cls, args_or_kwargs):
if isinstance(args_or_kwargs, Arguments):
return args_or_kwargs
try:
args_or_kwargs.keys
type(args_or_kwargs).__getitem__
except AttributeError:
return cls(*args_or_kwargs)
return cls(**args_or_kwargs) (Also have mixed feelings about calling those attributes |
Oh! I overcomplicated it, @curry
def starcall(function, args_or_kwargs):
arguments = Arguments.wrap(args_or_kwargs)
return function(*arguments.args, **arguments.kwargs) The currying enables the Now, that doesn't support the example @mjpieters had:
But I think that's fine, because that example's >>> this_year = starcall(partial(datetime.date, 2022))
>>> args = (9, 28)
>>> this_year(args) # not *args!
datetime.date(2022, 9, 28) >>> date = curry(datetime.date)
>>> this_year = starcall(date(2022))
>>> args = (9, 28)
>>> this_year(args) # not *args!
datetime.date(2022, 9, 28) |
Inspired by #557 , an idea for enhancing the Add a class Arguments:
__slots__ = ('args', 'kwargs')
def __init__(self, /, *args, **kwargs):
self.args = args
self.kwargs = kwargs
@classmethod
def wrap(cls, args_or_kwargs):
if isinstance(args_or_kwargs, Arguments):
return args_or_kwargs
try:
args_or_kwargs.keys
type(args_or_kwargs).__getitem__
except AttributeError:
return cls(*args_or_kwargs)
return cls(**args_or_kwargs)
@classmethod
def from_iterable(cls, iterable):
args = []
kwargs = {}
for argument in iterable:
if isinstance(argument, tuple) and len(argument) == 2:
name, value = argument
if name is not None:
kwargs[name] = value
continue
args.append(argument)
return cls(*args, **kwargs) Of course @composed(Arguments.from_iterable)
def foo():
yield 123
yield 'bar', 456
def example(*args, **kwargs):
print(args, kwargs)
f = compose(starcall(example), foo)
f() # prints: (123,) {'bar': 456} Of course, maybe it would be even better to just provide a decorator version of
wrap should probably be renamed for symmetry and clarity to from_splattable . |
This is similar to
itertools.starmap
but without the map part. It would allow the following:Currently you need to do it via lambda:
Also requested in funcy and StackOverflow
The text was updated successfully, but these errors were encountered: