Object, unpackable to both named and unnamed args at the same time

Practical Motivation

Quite often I need to write functions that pass arguments to several subfunctions. Illustrative example:

def cristmas_with_family(gift_to_mom, gift_to_dad):
    ...
    give_presents('mom', *gift_to_mom)
    give_presents('dad', *gift_to_dad)
    ...

Ok, maybe more practical example:

def make_figure(X, Y, args_for_x_axis=(), args_for_y_axis=()):
    ...
    draw_x_axis(*args_for_x_axis)
    draw_y_axis(*args_for_y_axis)
    ...

This all works quite elegant, until I need to pass both named and unnamed args. Currently, I have to do something like this

def make_figure(X, Y, args_for_x_axis=(), args_for_y_axis=(), kwargs_for_x_axis={}, kwargs_for_y_axis={}):
    ...
    draw_x_axis(*args_for_x_axis, **kwargs_for_x_axis, )
    draw_y_axis(*args_for_y_axis, **kwargs_for_y_axis, )
    ...

or

def make_figure(X, Y, args_for_x_axis=(), args_for_y_axis=()):
    ...
    match args_for_x_axis:
        case ([*args], {**kwargs}):
            draw_x_axis(*args, **kwargs)
        case [*args]:
            draw_x_axis(*args)
        case {**kwargs}:
            draw_x_axis(**kwargs)

    match args_for_y_axis:
        case ([*args], {**kwargs}):
            draw_y_axis(*args, **kwargs)
        case [*args]:
            draw_y_axis(*args)
        case {**kwargs}:
            draw_y_axis(**kwargs)
    ...

Way less elegant!

(Note: using {} as a default value is maybe not very safe since it is mutable, but this is just an illustration)

Suggested solution

Would it be nice to have a single object that would be unpackable to both named and unnamed args? Lets say unpacking can be made using *** syntax (* for unpacking iterables + ** for unpacking mappings). Something like arg this:

def make_figure(X, Y, args_for_x_axis=(), args_for_y_axis=()):
    ...
    draw_x_axis(***args_for_x_axis)
    draw_y_axis(***args_for_y_axis)
    ...

# calling the function
make_figure(X, Y, args_for_x_axis=ListDict(None), args_for_y_axis=ListDict(name='n of pirates by year', col='red'))
1 Like

I had the same idea a while ago.

However, my angle was more from performance perspective.

I thought it might be possible to make pass-through object of this kind more efficient:

def foo(***argskwds):
    print(type(argskwds))    # ListDict or simply:
    # argskwds == (args, kwds)
    return bar(***argskwds)

Haven’t had a chance to think about this much yet, but this part of my very initial thoughts on performance improvements of call protocol.

After all, function call can often be a bottleneck in lower level utilities.

Also, posted this at a time: vectorcall kwnames C array · Issue #699 · faster-cpython/ideas · GitHub

But I have only scratched a surface and don’t have anything more to say as of now.

1 Like

You don’t need syntax for this, just give your ListDict class a call method to call a function with the given args:

class ListDict:
    def __init__(self, *args, **kw):
        self.args = args
        self.kw = kw
    del call(self, fn):
        return fn(*self.args, **self.kw)

def make_figure(X, Y, args_for_x_axis=ListDict(), args_for_y_axis=ListDict()):
    ...
    args_for_x_axis.call(draw_x_axis)
    args_for_y_axis.call(draw_y_axis)
1 Like