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

в 20:48, , рубрики: python

Я полагаю, что 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-й вариант, как в плане читабельности, так и скорости.

Автор: random1st

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js