3

Say I have a variable x, which is of an unknown data type. I also have some random function foo. Now I want to do something along the following lines:

If x is a type that can be unpacked using **, such as a dictionary, call foo(**x). Else, if x is a type that can be unpacked using *, such as a tuple, call foo(*x). Else, just call foo(x).

Is there an easy way to check whether a type can be unpacked via either ** or *?

What I am currently doing is checking the type of x and executing something like:

if type(x) == 'dict':
    foo(**x)
elif type(x) in ['tuple','list', ...]:
    foo(*x)
else:
    foo(x)

But the problem is that I don't know the complete list of data types that can actually be unpacked and I'm also not sure if user defined data types can have a method that allows them to be unpacked.

K. Mao
  • 443
  • 4
  • 11

3 Answers3

2

You could use try:

try:
    foo(**x)
except:
    try:
        foo(*x)
    except:
        foo(x)

Its kind of crude, and doesn't distinguish why the exception occurred (which might be mitigated by checking the type of exception), but eliminates the need to try and enumerate which types can be called which way.

Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
  • 1
    Indeed, `except TypeError` is the minimal restriction you should put on that; though that still doesn't tell you whether `**` raised the exception or something within `foo`. – deceze Oct 28 '16 at 14:49
  • Cute answer, but what if foo(x) fails too? might Make another try for it ? Since x is unknown right ? – MooingRawr Oct 28 '16 at 14:49
1

Let's check the errors we receive when doing it badly:

>>> x = 1
>>> f(*x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() argument after * must be a sequence, not int
>>> f(**x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() argument after ** must be a mapping, not int

Great: so we need a sequence type for * and a mappping type for **. The rest is fairly straightforward: the Python 3 docs state:

There are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.

The fail-safe way to check if a var is a sequence type is:

>>> import collections
>>> all(isinstance(x, collections.Sequence) for x in [[], (), 'foo', b'bar', range(3)])
True

(see Python: check if an object is a sequence for more info)

The mapping type, according to the docs, is a dict:

There is currently only one standard mapping type, the dictionary.

You can check this in the same way, using isinstance, which will even take care of derived classes:

>>> from collections import OrderedDict
>>> from collections import Counter
>>> all(isinstance(x, dict) for x in [{}, OrderedDict(), Counter()])
True

So you could do as follows:

import collections
if isinstance(x, dict):
    foo(**x)
elif isinstance(x, collections.Sequence):
    foo(*x)
else:
    foo(x)
Community
  • 1
  • 1
brianpck
  • 8,084
  • 1
  • 22
  • 33
  • This is close to what I'm looking for, but it doesn't seem to work for sets. For example `isinstance(set([1,2,3]),collections.Sequence)` is false. However, `'{},{}'.format(*set([1,2,3]))` unpacks to `'1,2'` just fine. I'm not sure if there are any other exceptions, but there's at least this one. – K. Mao Oct 28 '16 at 15:13
  • set([1,2,3]) unpacks to 1,2 is fine? – Scott Hunter Oct 28 '16 at 15:19
  • @K.Mao Hmm...you're right, but note that this doesn't really make a lot of sense since `set`'s are not ordered. I'm checking up on this now – brianpck Oct 28 '16 at 15:21
  • I did a bit of digging around. Maybe `collections.Iterable` could work? You can unpack sets and dictionaries using * (though dictionaries should probably be unpacked using **). And `all(isinstance(x,collections.Iterable) for x in [set(),[],(),{},'foo',b'bar',range(3)])` evaluates to true, whereas `collections.Sequence` fails on sets and dictionaries. However, I do agree that it doesn't make very much sense to unpack sets or dictionaries using *, considering they are not ordered. But for some reason you can do it in Python. – K. Mao Oct 28 '16 at 15:28
  • You're right: for a `dict` it will iterate through keys. I would advise using `Sequence` because those will give you *predictable, ordered* behavior. – brianpck Oct 28 '16 at 15:30
0

How about

try:
    (lambda **a: None)(**x)
except TypeError:
    try:
        (lambda *a: None)(*x)
    except TypeError:
        # x cannot be unpacked
        foo(x)
    else:
        # x can be unpacked as a list
        foo(*x)
else:
    # x can be unpacked as a dict
    foo(**x)

Or you can define some utility functions

def is_list_unpackable(obj):
    try:
        (lambda *a: None)(*obj)
    except TypeError:
        return False
    return True

def is_dict_unpackable(obj):
    try:
        (lambda **a: None)(**obj)
    except TypeError:
        return False
    return True

if is_dict_unpackable(x): # type(x) == 'dict'
    foo(**x)
elif is_dict_unpackable(x): # type(x) in ['tuple','list', ...]:
    foo(*x)
else:
    foo(x)

(Inspired by Scott Hunter's answer and deceze's comment)

PROgram52bc
  • 98
  • 1
  • 1
  • 8