2

dicts support several initialization/update arguments:

d = dict([('a',1), ('b',2)]) # list of (key,value)
d = dict({'a':1, 'b':2})     # copy of dict
d = dict(a=1, b=2)           #keywords

How do I write the signature and handle the arguments of a function that handles the same set of cases?

I'm writing a class that I want to support the same set of arguments, but just store the list of (key,value) pairs in order to allow for multiple items with the same key. I'd like to be able to add to the list of pairs from dicts, or lists of pairs or keywords in a way that is the same as for dict.

The C implementation of update isn't very helpful to me as I'm implementing this in python.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
Dave
  • 7,555
  • 8
  • 46
  • 88
  • I don't think this can be exactly reproduced in python, because `dict(**kwarg)` and `dict(mapping, **kwarg)/dict(iterable, **kwarg)` are mutually exclusive signatures. The nearest you can get would perhaps be `dict(*arg, **kwarg)` - where you would need to check the length of `arg` and raise an error if it's > 1 (or maybe just allow passing multiple iterables/mappings, unlike `dict`). – ekhumoro Jan 31 '20 at 18:51
  • @ekhumoro `dict(*args, **kwargs)` is how it would be done checking the length and type of `args` – juanpa.arrivillaga Jan 31 '20 at 18:53
  • 1
    @juanpa.arrivillaga Actually, I think I've overlooked another possibility provided by [PEP 570](https://www.python.org/dev/peps/pep-0570): `def (x=None, /, **kwargs)`. That *does* exactly reproduce the signature - but it requires python >= 3.8. – ekhumoro Jan 31 '20 at 19:33
  • 1
    @ekhumoro sure, I think you could assume that key-word only arguments are unecessary, so just `def(x=None, **kwargs)` might suffice – juanpa.arrivillaga Jan 31 '20 at 19:35
  • @juanpa.arrivillaga That is ambiguous, though, and will produce strange errors if you tried `fn(arg, x=1)` - (i.e. "multiple values for argument 'x'"). – ekhumoro Jan 31 '20 at 19:39
  • P.S. the new syntax makes `x` *positional only*. So `fn(x=1)` would add it to `kwarg`, whereas `fn(1)` would assign it to `x`. – ekhumoro Jan 31 '20 at 19:49
  • @ekhumoro sorry, that's what I meant. I hadn't considered the case of a key `x` – juanpa.arrivillaga Jan 31 '20 at 19:55
  • Is it a requirement that all the keys are hashable, or do you want to accept pairs of any type whatsoever? – ekhumoro Jan 31 '20 at 20:17
  • Keep in mind that if Python were released today with 20 years of experience to draw from, `dict` would probably only support one of those signatures and replace the others with class methods. For example, `dict(a=1, b=2)`, `dict.from_association_list([('a', 1), ('b', 2)])`, and `dict.from_mapping({'a': 1, 'b': 2})`. – chepner Jan 31 '20 at 20:17
  • @ekhumoro hashable is expected – Dave Jan 31 '20 at 20:30

3 Answers3

2

Here is a function that should behave the same as dict (except for the requirement to support multiple keys):

def func(*args, **kwargs):
    result = []
    if len(args) > 1:
        raise TypeError('expected at most 1 argument, got 2')
    elif args:
        if all(hasattr(args[0], a) for a in ('keys', '__getitem__')):
            result.extend(dict(args[0]).items())
        else:
            for k, v in args[0]:
                hash(k)
                result.append((k, v))
    result.extend(kwargs.items())
    return result

A few notes:

  • The collections.abc.Mapping class isn't used to test for mapping objects, because dict has lower requirements than that.
  • The signature of the function could be defined more precisely using positional-only syntax, like this: def func(x=None, /, **kwargs). However, that requires Python >= 3.8, so a more backwards-compatible solution has been preferred.
  • The function should raise the same exceptions as dict, but the messages won't always be exactly the same.

Below are some simple tests that compare the behaviour of the function with the dict constructor (although it does not attempt to cover every possibility):

def test(fn):
    d1 = {'a': 1, 'b': 2, 'c': 3}
    d2 = {'d': 4, 'e': 5, 'f': 6}
    class Maplike():
        def __getitem__(self, k):
            return d1[k]
        def keys(self):
            return d1.keys()

    print('test %s\n=========\n' % fn.__name__)
    print('dict:', fn(d1))
    print('maplike:', fn(Maplike()))
    print('seq:', fn(tuple(d1.items())))
    print('it:', fn(iter(d1.items())))
    print('gen:', fn(i for i in d1.items()))
    print('set:', fn(set(d2.items())))
    print('str:', fn(['fu', 'ba', 'r!']))
    print('kwargs:', fn(**d1))
    print('arg+kwargs:', fn(d1, **d2))
    print('dup-keys:', fn(d1, **d1))
    print('empty:', fn())
    print()
    try:
        fn(d1, d2)
        print('ERROR')
    except Exception as e:
        print('len-args: %s' % e)
    try:
        fn([(1, 2, 3)])
        print('ERROR')
    except Exception as e:
        print('pairs: %s' % e)
    try:
        fn([([], 3)])
        print('ERROR')
    except Exception as e:
        print('hashable: %s' % e)
    print()

test(func)
test(dict)

Ouput:

test func
=========

dict: [('a', 1), ('b', 2), ('c', 3)]
maplike: [('a', 1), ('b', 2), ('c', 3)]
seq: [('a', 1), ('b', 2), ('c', 3)]
it: [('a', 1), ('b', 2), ('c', 3)]
gen: [('a', 1), ('b', 2), ('c', 3)]
set: [('d', 4), ('e', 5), ('f', 6)]
str: [('f', 'u'), ('b', 'a'), ('r', '!')]
kwargs: [('a', 1), ('b', 2), ('c', 3)]
arg+kwargs: [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 6)]
dup-keys: [('a', 1), ('b', 2), ('c', 3), ('a', 1), ('b', 2), ('c', 3)]
empty: []

len-args: expected at most 1 argument, got 2
pairs: too many values to unpack (expected 2)
hashable: unhashable type: 'list'

test dict
=========

dict: {'a': 1, 'b': 2, 'c': 3}
maplike: {'a': 1, 'b': 2, 'c': 3}
seq: {'a': 1, 'b': 2, 'c': 3}
it: {'a': 1, 'b': 2, 'c': 3}
gen: {'a': 1, 'b': 2, 'c': 3}
set: {'d': 4, 'e': 5, 'f': 6}
str: {'f': 'u', 'b': 'a', 'r': '!'}
kwargs: {'a': 1, 'b': 2, 'c': 3}
arg+kwargs: {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}
dup-keys: {'a': 1, 'b': 2, 'c': 3}
empty: {}

len-args: dict expected at most 1 argument, got 2
pairs: dictionary update sequence element #0 has length 3; 2 is required
hashable: unhashable type: 'list'
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
1

This should do the trick:

class NewDict:
    def __init__(self, *args, **kwargs):
        self.items = []
        if kwargs:
            self.items = list(kwargs.items())
        elif args:
            if type(args[0]) == list:
                self.items = list(args[0])
            elif type(args[0]) == dict:
                self.items = list(args[0].items())

    def __repr__(self):
        s = "NewDict({"
        for i, (k,v) in enumerate(self.items):
            s += repr(k) + ": " + repr(v)
            if i < len(self.items) - 1:
                s += ", "
        s += "})"
        return s

d1 = NewDict()
d2 = NewDict([('a',1), ('b',2), ('a',2)])
d3 = NewDict({'a':1, 'b':2})
d4 = NewDict(a=1, b=2)

print(d1, d2, d3, d4, sep="\n")
print(d2.items)

Output:

NewDict({})
NewDict({'a': 1, 'b': 2, 'a': 2})
NewDict({'a': 1, 'b': 2})
NewDict({'a': 1, 'b': 2})
[('a', 1), ('b', 2), ('a', 2)]
Ed Ward
  • 2,333
  • 2
  • 10
  • 16
0

I'm not saying this is a safe or great way to do this, but how about ...

def getPairs(*args, **kwargs):
    pairs = []
    for a in args:
        if type(a) == dict:
            pairs.extend([[key, val] for key, val in a.items()])
        elif type(a) == list:
            for a2 in a:
                if len(a2) > 1:
                    pairs.append([a2[0], a2[1:]])

    pairs.extend([[key, val] for key, val in kwargs.items()])

    return pairs

jcfollower
  • 3,103
  • 19
  • 25
  • 1
    The OP wants to "allow for multiple items with the same key", so you cannot use `dict` to parse the `args`. – ekhumoro Jan 31 '20 at 20:12