5

I have a list, say:

NUM = 100
my_list = list(range(NUM))

I would like to generate a dict where the key is equal to the value, something like:

my_dict = {item: item for item in my_list}

or:

my_dict = dict(zip(my_list, my_list))

I have run some micro-benchmarks, and it looks like they have similar speed, but I was hoping that the second would be much faster, since the looping should be happening in C.

For example, the following construct:

my_dict = {key: SOMETHING for key in keys}

translates into the much faster:

my_dict = dict.fromkeys(k, SOMETHING)

So, my question is: is there any similar such construct for {x: x for x in my_list}?


EDIT

I have checked dir(dict) and there seems to be nothing in this direction (I would expect it to be called something like dict.fromitems()).


EDIT 2

A method like dict.fromitems() would have a broader application than this specific use-case, because:

dict.fromitems(keys, values)

could, in principle substitute both:

{k, v for k, v in zip(keys, values)}

and:

dict(zip(keys, values))
norok2
  • 25,683
  • 4
  • 73
  • 99
  • 11
    No, there is not. – Martijn Pieters Oct 04 '18 at 15:39
  • 3
    I don't really see why someone would need something like that in the first place... Can you give us an use case? Maybe we can figure out a better way of doing what you want to do – Fred Oct 04 '18 at 15:41
  • 1
    The `dict(zip` version makes two function calls (albeit they're C calls, so faster than Python calls). But `zip` has to build a bunch of tuples, and although that's a cheap operation the dict comp avoids that. – PM 2Ring Oct 04 '18 at 15:42
  • 3
    Is the time issue a bottleneck for your code? – Chris_Rands Oct 04 '18 at 15:42
  • 1
    Also, what Fred said. Do you intend to change the values at any stage? If not, why not just use a set? – PM 2Ring Oct 04 '18 at 15:43
  • @Fred This is used to model a many-to-1 relationship, where I want different items to map to the same value, and one of the relationship is the item with itself. In the end, I want to use something like `my_dict['a']` and `my_dict['alice']` to get to the same information. – norok2 Oct 04 '18 at 15:47
  • @PM2Ring I had thought of something similar for the speed. I would not know how to model a many-to-1 relationship using a `set()`. – norok2 Oct 04 '18 at 15:48
  • To build a "many-to-1" relationship, I'd use a graph (see [networkx](https://networkx.github.io/)). – IMCoins Oct 04 '18 at 15:50
  • 1
    @IMCoins Thanks for the suggestion, but that would be killing a fly with a cannonball... – norok2 Oct 04 '18 at 15:52
  • Now that I know your use case, the key==value thing makes sense, and a set would not be any use for that. :) – PM 2Ring Oct 04 '18 at 16:02
  • @PM2Ring Actually, a fast C implementation of `dict.fromitems()` would replace the `{k: v for k, v in zip(keys, values)}` or `dict(zip(keys, values))` construct, and this is way more general and useful than requiring `k == v`, for which I thought there could be a faster construct anyway. – norok2 Oct 04 '18 at 16:06
  • `dict.fromitems()` doesn't exist because `dict()` itself *already performs that function*. `dict(zip(my_list, my_list))` already passes in items to the constructor, why have a separate `dict.fromitems()` that does the same? If you meant for that classmethod to have a different input, please don't call it `dict.fromitems()`. – Martijn Pieters Oct 04 '18 at 18:19
  • 1
    Usecases where the key and value are the same are *not common* so I doubt that there ever will be support for a faster path than `{k: k for k in iterable}`. It's the iteration and dictionary building that takes time here, not executing bytecode for the loop, which is why `zip()` is no faster. `dict.fromkeys(mylist)` is in the same ballpark of speed* as `dict(zip(mylist, mylist))` and `{k: k for k in mylist}`. – Martijn Pieters Oct 04 '18 at 18:20

3 Answers3

9

No, there is no faster method available for dictionaries.

That's because the performance cost is all in processing each item from the iterator, computing its hash and slotting the key into the dictionary data hash table structures (including growing those structures dynamically). Executing the dictionary comprehension bytecode is really insignificant in comparison.

dict(zip(it, it)), {k: k for k in it} and dict.fromkeys(it) are all close in speed:

>>> from timeit import Timer
>>> tests = {
...     'dictcomp': '{k: k for k in it}',
...     'dictzip': 'dict(zip(it, it))',
...     'fromkeys': 'dict.fromkeys(it)',
... }
>>> timings = {n: [] for n in tests}
>>> for magnitude in range(2, 8):
...     it = range(10 ** magnitude)
...     for name, test in tests.items():
...         peritemtimes = []
...         for repetition in range(3):
...             count, total = Timer(test, 'from __main__ import it').autorange()
...             peritemtimes.append(total / count / (10 ** magnitude))
...         timings[name].append(min(peritemtimes))  # best of 3
...
>>> for name, times in timings.items():
...     print(f'{name:>8}', *(f'{t * 10 ** 9:5.1f} ns' for t in times), sep=' | ')
...
dictcomp |  46.5 ns |  47.5 ns |  50.0 ns |  79.0 ns | 101.1 ns | 111.7 ns
 dictzip |  49.3 ns |  56.3 ns |  71.6 ns | 109.7 ns | 132.9 ns | 145.8 ns
fromkeys |  33.9 ns |  37.2 ns |  37.4 ns |  62.7 ns |  87.6 ns |  95.7 ns

That's a table of the per-item cost for each technique, from 100 to 10 million items. The timings go up as the additional cost of growing the hash table structures accumulate.

Sure, dict.fromkeys() can process items a little bit faster, but it's not an order of magnitude faster than the other processes. It's (small) speed advantage does not come from being able to iterate in C here; the difference lies purely in not having to update the value pointer each iteration; all keys point to the single value reference.

zip() is slower because it builds additional objects (creating a 2-item tuple for each key-value pair is not a cost-free operation), and it increased the number of iterators involved in the process, you go from a single iterator for the dictionary comprehension and dict.fromkeys(), to 3 iterators (the dict() iteration delegated, via zip(), to two separate iterators for the keys and values).

There is no point in adding a separate method to the dict class to handle this in C, because

  1. is not a common enough use case anyway (creating a mapping with keys and values equal is not a common need)
  2. not going to be significantly faster in C than it would be with a dictionary comprehension anyway.
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    Thanks for the insights. However, I believe that construct for creating a `dict` from two iterables will probably be a large enough use-case to justify a `dict.fromitems(keys, values)` method implemented in C. This will be significantly faster than the comprehension or the use of `zip()`, similarly to what `list(items)` being faster than `[i for i in items]`. I was hoping that in this specific case there could have been some trick. – norok2 Oct 04 '18 at 20:19
  • 2
    @norok2: `list()` can be faster because building the list object is as straightforward operation, you can even pre-size the new list object when the size of `items` is already known. The list comp is mostly slower because it has to do dynamic resizing as items are added as you can't pick up the size hint anymore. A dictionary *can't* be pre-sized, so a `dict.fromitems(keys, values)` is **not** going to be faster. – Martijn Pieters Oct 04 '18 at 20:24
  • 1
    @norok2: Even if there was a small speed improvement (`zip()` does have to create a tuples for each pair there, so you can perhaps avoid that with a dedicated `dict.fromitems()` method), you'd have to convince the Python core devs it is worth the extra cost of adding such a method. The costs are: future maintenance, documentation updates, confusion of it's use (where `dict(zip(...))` already provides the same functionality). I don't think they'll bite, as there are no real benefits, there are not enough usecases that require that level of optimisation. – Martijn Pieters Oct 04 '18 at 20:27
0

This is the Zen answer. The dictionary comprehension loop itself is fast, this is not the bottleneck. As Martijn Pieters says the time is spent:

  1. Accessing the key and;
  2. Computing the __hash__ of the key.
  3. Possibly your __hash__ is bad and you have a lot of collisions, causing the dictionary inserts to be expensive.

Don't worry about the loop. If it is taking a long time to build your dictionary it is because these operations are slow.

satnhak
  • 9,407
  • 5
  • 63
  • 81
-3

Using the results of the answer here, we create a new class that subclasses defaultdict, and override its missing attribute to allow for passing the key to the default_factory:

from collections import defaultdict
class keydefaultdict(defaultdict):
    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        else:
            ret = self[key] = self.default_factory(key)
            return ret

Now you can create the sort of dictionary you're looking for by doing the following:

my_dict = keydefaultdict(lambda x: x)

Then, whenever you need do a mapping for the keys that don't map to themselves, you simply update those values.

Timings.

Subclassing defaultdict:

%%timeit
my_dict = keydefaultdict(lambda x: x)
for num in some_numbers: my_dict[num] == num

Results:

4.46 s ± 71.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Dict comprehension

%%timeit
my_dict = {x: x for x in some_numbers}
for num in some_numbers: my_dict[num] == num

Results:

1.19 s ± 20.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

The two become comparable when you wind up needing to access approximately 17% of the original values. And better if you need fewer:

Accessing only a portion of the original values

Subclassing defaultdict:

%%timeit
frac = 0.17
my_dict = keydefaultdict(lambda x: x)
for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num

Results:

770 ms ± 4.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Dict comprehension

%%timeit
frac = 0.175
my_dict = {x: x for x in some_numbers}
for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num

Results:

781 ms ± 4.03 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
PMende
  • 5,171
  • 2
  • 19
  • 26
  • Wow, this digs into the internals! When you say `subclass the default dict` what is the type of `my_dict` in your answer? Is it an ordinary dict? Can you `timeit` and give a benchmark on the example provided in question? Can you add how to pass it the list? – devssh Oct 04 '18 at 16:16
  • @devssh `my_dict` will be an instance of all of: `dict`, `defaultdict`, and `keydefaultdict`. – PMende Oct 04 '18 at 16:19
  • 1
    Could you please be more specific in how to use it for a given `list`? Also, could please you produce some timings? – norok2 Oct 04 '18 at 16:19
  • Even the post you referenced says this type of approach isn't clean and this method of subclassing is really old from 2010, I have the benchmarking code ready, how to pass the list to this keydefaultdict? Someone timed the code in one of the answers below in that other SO post and they said it wasn't faster than cleaner code. – devssh Oct 04 '18 at 16:25
  • @devssh You don't pass the list to it. When you do `my_dict[k]` if `k` isn't an existing key then the factory function is called to create a value from the key (`lambda x:x` returns the key itself), the dict is updated with the new key-value pair, and the value is returned. – PM 2Ring Oct 04 '18 at 16:37
  • but you never called the `my_list` anywhere in your code. Then how do you coerce type of `my_list` to `my_dict`. Isn't this only part of the solution? Do you propose to do `for x in my_list: my_dict[x]` afterwards? Its not clear what I do after declaring this type so I am unable to benchmark. – devssh Oct 04 '18 at 16:45
  • @norok2 I've updated with some timings. The savings come in initializing the dictionary, where it is 6 orders of magnitude faster. The way you're going to use the dictionary after initialization will ultimately determine performance. – PMende Oct 04 '18 at 17:13
  • @devssh *Some* people say it's not clean. It's a matter of perspective. Additionally, how *else* would you do inheritance in in python? That hasn't changed in the last 8 years, far as I know. – PMende Oct 04 '18 at 17:15
  • @devssh Are you familiar with `defaultdict`? It works in the same way. The only difference is that instead of just having a "fixed" default value it has a value that depends on the key (and in particular it is the key). Now profiling it depends in how you think it will be used. IMHO the initialization should include also actually creating all the key-value pairs, so I'd put the `for x in my_list: my_dict[x]` in the initialization step to profile it. Whether the profiling as shown here gives useful information depends on how you plan to use it. – Bakuriu Oct 04 '18 at 17:16
  • This method is quite fast it turns out, although the savings are small. It really seems like overkill and its restricted to this small niche. Nice to know that it is comparable in performance though, had no idea about this before. – devssh Oct 04 '18 at 17:23
  • @PMende Thanks for the suggestion. On the premises that the idea is interesting, this is not going to restrict the `keys` in the dict, for anything that it is not there, it will behave as the identity. This is a very different behavior from a `dict` with `key == value`. Of course, the approach could be extended to make it behave like a normal `dict`. I am not sure this will get any faster. – norok2 Oct 04 '18 at 17:41
  • @norok2 I'm not sure what you mean by "a `dict` with `key == value`". My suggestion builds a `dict` that has keys whose values are equal to the key, just as you're requesting in your original post. – PMende Oct 04 '18 at 18:17
  • 2
    This is certainly not going to be faster, because you added a function call for each value, rather than have the value be available up-front. A Python function call is relatively expensive here. – Martijn Pieters Oct 04 '18 at 18:49
  • @MartijnPieters Well, despite your assertions, the interpreter disagrees with you. I'm going to go with the interpreter. – PMende Oct 04 '18 at 18:59
  • @PMende: Your loop to 'add' the key-value pairs takes 921ms. How is that faster than 620ms to create it with a dict comprehension? – Martijn Pieters Oct 04 '18 at 19:00
  • And note that the 971 ms is the average time of multiple runs, where the **first** run is going to be way, *way* slower. The additional runs done by the timer won't trigger the `__missing__` method anymore, so are much faster. – Martijn Pieters Oct 04 '18 at 19:00
  • @MartijnPieters Are you planning on usinf your dict without accessing the values? Probably not. My method will be faster. – PMende Oct 04 '18 at 19:01
  • @PMende: no, because the 971ms is the average for 7 runs, where the first run is doing the actual key materialising. It's that first run that has to beat the 620ms for the dict comprehension. You can't time that very effectively, as you need to *reset* your dict subclass for each test. – Martijn Pieters Oct 04 '18 at 19:03
  • @PMende: you need to create a sequence of them: `testdicts = iter([keydefaultdict(lambda x: x) for _ in range(1000)])`, then time `%timeit d = next(testdicts); for key in some_numbers: d[key] = key`. Hopefully 1000 gives you enough empty `keydefaultdict()` instances for IPython to run it's tests. Or run all your tests with the `timeit` module so you can control the number of repetitions exactly. – Martijn Pieters Oct 04 '18 at 19:05
  • At any rate, you could just test calling the `__missing__` method with random integers instead, and take that number times `len(some_numbers)` as a good approximation of the cost of materialising that many numbers. **This is going to be slower** than having the dict comprehension do the same work, because the dict comp has no `if` test to perform, no frame call stack to handle, no `lambda` to call. – Martijn Pieters Oct 04 '18 at 19:07
  • @MartijnPieters Yes, you're correct that I wasn't addressing the creation of the full dictionary correctly. I've edited my answer to show that. But, again, performance depends on use case. I've shown that my method will be faster (obviously) if you don't need to access all the keys. – PMende Oct 04 '18 at 19:21
  • 1
    You shouldn't even use the lambda. Just **subclass `dict`** and have its `__missing__` just set the value to the key. – Antti Haapala -- Слава Україні Oct 05 '18 at 08:06