Одна простенька задачка. Швидко, красиво або чисто?

Я вважаю, що 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.118 s

OK

Який перший момент я зазначив сходу? Використання dict() — це виклик функції, в той час як використання {} — синтаксична конструкція. Замінимо ініціалізацію словника:

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

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.312 s

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.797 s

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.600 s

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.584 s

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.212 s

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.190 s

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.219 s

OK

Від себе додам, що особисто мені найбільш імпонує 6-й варіант, як в плані читабельності, так і швидкості.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.