Одна простенькая задачка. Быстро, красиво или чисто?

от автора

Я полагаю, что 99% Python разработчиков решали так или иначе эту задачу, поскольку она входит в стандартный набор задач, предлагаемый им для соискателей должности Python Developer’а в одной широко известной компании.

# Есть два списка разной длины. В первом содержатся ключи, а во втором значения.
# Напишите функцию, которая создаёт из этих ключей и значений словарь.
# Если ключу не хватило значения, в словаре должно быть значение None.
# Значения, которым не хватило ключей, нужно игнорировать.

Ниже любопытства ради я привел список проанализированных мной решений


Для оценки правильности кода я набросал простенький юниттест.

Вариант 1

import unittest   def dict_from_keys(keys, values):     res = dict()     for num, key in enumerate(keys):         try:             res[key] = values[num]         except IndexError:             res[key] = None      return res   class DictFromKeysTestCase(unittest.TestCase):     def test_dict_from_keys_more_keys(self):         keys = range(1000)         values = range(900)         for _ in range(10 ** 5):             result = dict_from_keys(keys, values)         self.assertEqual(keys,result.keys())      def test_dict_from_keys_more_values(self):         keys =range(900)         values = range(1000)         for _ in range(10 ** 5):             result = dict_from_keys(keys, values)         self.assertEqual(keys, result.keys()) 

Здесь я привел первое найденное мной решение. Запускаем юниттест:

random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 26.118s

OK

Какой первый момент я отметил сходу? Использование dict() — это вызов функции, в то время как использование {} — синтаксическая конструкция. Заменим инициализацию словаря:
random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 25.828s

OK

Мелочь, а приятно. Хотя можно списать и на погрешность.

От следующего варианта я плакал кровью, но все же приведу его здесь:

Вариант 2

def dict_from_keys(keys, values):     res = {}     it = iter(values)     nullValue = False     for key in keys:         try:             res[key] = it.next() if not nullValue else None         except StopIteration:             nullValue = True             res[key] = None     return res 

Результат теста:

random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 33.312s

OK

Без комментариев.

Вариант 3

Следующее решение:

def dict_from_keys(keys, values):    return {key: None if idx>=len(values) else values[idx] for idx, key in enumerate(keys)} 

Результат теста:
random1st@MacBook-Pro ~/P/untitled [1]> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 26.797s

OK

Как видим, существенного ускорения добиться не удалось. Еще вариация на тему:

Вариант 4

def dict_from_keys(keys, values):     return dict((len(keys) > len(values)) and map(None, keys, values) or zip(keys, values)) 

Результат:
random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 20.600s

OK

Вариант 5

def dict_from_keys(keys, values):     result = dict.fromkeys(keys, None)     result.update(zip(keys, values))     return result 

Результат:
random1st@MacBook-Pro ~/P/untitled [1]> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 17.584s

OK

Вполне ожидаемо использование встроенных функций дает существенное ускорение. Можно ли добиться еще более впечатляемого результата?

Вариант 6

def dict_from_keys(keys, values):     return dict(zip(keys, values + [None] * (len(keys) - len(values)))) 

Результат:
random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 14.212s

OK

Еще быстрее:

Вариант 7

def dict_from_keys(keys, values):     return dict(itertools.izip_longest(keys, values[:len(keys)])) 

Результат:
random1st@MacBook-Pro ~/P/untitled> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 10.190s

OK

Встает резонный вопрос, можно ли получить что-то быстрее этого решения. Очевидно, что если нельзя сделать вычисления быстрее, их стоит сделать ленивыми. Сомнительность такого варианта очевидна, однако теперь уже все зависит от контекста. В частности, нижеследующий код вполне себе проходит тесты, однако это уже не вполне питоновский словарь:

Вариант 8

class DataStructure(dict):     def __init__(self, *args, **kwargs):         super(DataStructure, self).__init__(*args, **kwargs)         self._values = None         self._keys = None      @classmethod     def dict_from_keys_values(cls, keys, values):         obj = cls()         obj._values = values[:len(keys)]         obj._keys = keys          return obj      def __getitem__(self, key):         try:             return super(DataStructure, self).__getitem__(key)         except KeyError:             try:                 idx = self._keys.index(key)                 self._keys.pop(idx)                 super(DataStructure, self).__setitem__(                     key, self._values.pop(idx)                 )             except ValueError:                 raise KeyError             except IndexError:                 super(DataStructure, self).__setitem__(key, None)             return super(DataStructure, self).__getitem__(key)      def keys(self):         for k in self._keys:             yield k         for k in super(DataStructure, self).keys():             yield k 

random1st@MacBook-Pro ~/P/untitled [1]> python -m unittest dict_from_keys
..
----------------------------------------------------------------------
Ran 2 tests in 1.219s

OK

От себя добавлю, что лично мне наиболее импонирует 6-й вариант, как в плане читабельности, так и скорости.
ссылка на оригинал статьи https://habrahabr.ru/post/315170/