Reputation: 1033
I want to write a general purpose function:
def foo(positional, a=None, b=None, c=None, *, keyword_only=True):
# ... ?? ... magic_code
return a_b_c_in_tuple_in_order
that returns a tuple retaining the order of keyword arguments a,b,c
:
xx = 'some object'
>>> foo(xx, 1, 2, 3)
(1, 2, 3)
>>> foo(xx, 1, 2)
(1, 2)
>>> foo(xx, a=1, b=2, c=3, keyword_only=False)
(1, 2, 3)
>>> foo(xx, b=2, a=1, c=3) # <---- key behaviour
(2, 1, 3)
>>> foo(xx, b=2, c=3)
(2, 3)
>>> foo(xx, c=3, a=1)
(3, 1)
>>> foo(xx, a='may be anything', c=range(5), b=[1, 2])
('may be anything', range(0, 5), [1, 2])
>>> foo(xx, b=1)
(1,) # may be 1 or (1,)
How can I achieve this? Is such a code unpythonic, and if yes, what should I use instead?
Primary objectives are easy usage and readability.
My aim is to use such a function for conversions between unit systems (e.g. SI <--> imperial, but actual use case is more advanced), where the user would be able to intuitively write e.g.
l, (t1, t2) = convert(params, lengths=L, times=(T1, T2), normalized=True)
# or
(t1, t2), l = convert(params, times=(T1, T2), lengths=L, normalized=True)
regardless of how the function is defined, and if the quantities are floats, arrays, etc.
Responding well to such misusages are not required, but being fool-proof is a bonus:
>>> foo(b=2, a=1, c=3, positional=xx)
(2, 1, 3)
>>> foo(b=2, positional=xx, a=1, keyword_only=False, c=3)
(2, 1, 3)
Upvotes: 1
Views: 243
Reputation: 2699
@rdas almost had it Using their answer as a decorator preserves the original function signature and gives you the data that you want:
kwargs_to_extract = {'a', 'b', 'c'}
def kwarg_tuple_returner(fn):
def tuple_extractor(positional, *args, **kwargs):
_unused_return = fn(positional, *args, **kwargs)
if args and kwargs:
return args + tuple(v for k, v in kwargs.items() if k in kwargs_to_extract)
if args:
return args
if kwargs:
return tuple(v for k, v in kwargs.items() if k in kwargs_to_extract)
return tuple_extractor
@kwarg_tuple_returner
def foo(positional, a=None, b=None, c=None, *, keyword_only=True):
# ... ?? ... magic_code
# nothing below matters because we return our argument value from our decorator
a = "mangled"
b = 5
c = 3.14
return None
xx = 'obj'
print(foo(xx, 1, 2, 3))
print(foo(xx, 1, 2))
print(foo(xx, a=1, b=2, c=3, keyword_only=False))
print(foo(xx, b=2, a=1, c=3))
print(foo(xx, b=2, c=3))
print(foo(xx, c=3, a=1))
print(foo(xx, a='may be anything', c=range(5), b=[1, 2]))
print(foo(xx, b=1))
print(foo(b=2, a=1, c=3, positional=xx))
print(foo(b=2, positional=xx, a=1, keyword_only=False, c=3))
print(foo(xx, 1, c=2))
Results in:
(1, 2, 3)
(1, 2)
(1, 2, 3)
(2, 1, 3)
(2, 3)
(3, 1)
('may be anything', range(0, 5), [1, 2])
(1,)
(2, 1, 3)
(2, 1, 3)
(1, 2)
Upvotes: 2
Reputation: 21275
This seems to pass all your examples, though I'm not sure what the behaviour of positional
& keyword_only
should be:
def foo(positional, *args, **kwargs):
if args and kwargs:
return args + tuple(v for k, v in kwargs.items() if k in {'a', 'b', 'c'})
if args:
return args
if kwargs:
return tuple(v for k, v in kwargs.items() if k in {'a', 'b', 'c'})
xx = 'obj'
print(foo(xx, 1, 2, 3))
print(foo(xx, 1, 2))
print(foo(xx, a=1, b=2, c=3, keyword_only=False))
print(foo(xx, b=2, a=1, c=3))
print(foo(xx, b=2, c=3))
print(foo(xx, c=3, a=1))
print(foo(xx, a='may be anything', c=range(5), b=[1, 2]))
print(foo(xx, b=1))
print(foo(b=2, a=1, c=3, positional=xx))
print(foo(b=2, positional=xx, a=1, keyword_only=False, c=3))
print(foo(xx, 1, c=2))
Result:
(1, 2, 3)
(1, 2)
(1, 2, 3)
(2, 1, 3)
(2, 3)
(3, 1)
('may be anything', range(0, 5), [1, 2])
(1,)
(2, 1, 3)
(2, 1, 3)
(1, 2)
It relies on the fact that dictionaries in python3 are ordered.
Upvotes: 1