Одна простенькая задачка. Быстро, красиво или чисто?
Я полагаю, что 99% Python разработчиков решали так или иначе эту задачу, поскольку она входит в стандартный набор задач, предлагаемый им для соискателей должности Python Developer’а в одной широко известной компании.
Ниже любопытства ради я привел список проанализированных мной решений
Для оценки правильности кода я набросал простенький юниттест.
Здесь я привел первое найденное мной решение. Запускаем юниттест:
Какой первый момент я отметил сходу? Использование dict () — это вызов функции, в то время как использование {} — синтаксическая конструкция. Заменим инициализацию словаря:
Мелочь, а приятно. Хотя можно списать и на погрешность. От следующего варианта я плакал кровью, но все же приведу его здесь:
Результат теста:
Без комментариев.
Результат теста:
Как видим, существенного ускорения добиться не удалось. Еще вариация на тему:
Результат:
Вариант 5
Результат:
Вполне ожидаемо использование встроенных функций дает существенное ускорение. Можно ли добиться еще более впечатляемого результата?
Результат:
Еще быстрее:
Результат:
Встает резонный вопрос, можно ли получить что-то быстрее этого решения. Очевидно, что если нельзя сделать вычисления быстрее, их стоит сделать ленивыми. Сомнительность такого варианта очевидна, однако теперь уже все зависит от контекста. В частности, нижеследующий код вполне себе проходит тесты, однако это уже не вполне питоновский словарь:
От себя добавлю, что лично мне наиболее импонирует 6-й вариант, как в плане читабельности, так и скорости.
# Есть два списка разной длины. В первом содержатся ключи, а во втором значения.
# Напишите функцию, которая создаёт из этих ключей и значений словарь.
# Если ключу не хватило значения, в словаре должно быть значение 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-й вариант, как в плане читабельности, так и скорости.