Neinstein
Neinstein

Reputation: 1033

How to determine the order of passed keyword arguments?

Problem:

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.

Why?

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.

Corner cases:

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

Answers (2)

spacether
spacether

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

rdas
rdas

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

Related Questions