У одной задачи может быть несколько способов решения. Возьмем классическую задачу программирования — задачу подсчета, в которой мы считаем, сколько раз каждый элемент списка встречается в нем. Способ решения этой задачи на Python менялся по мере развития языка. Именно об этом мы будем говорить в этой статье.
Большинство из нас присоединилось к программированию на Python с его третьей версии. Однако мы начнем с Python 1.4. Пристегните ремни, отправляемся в далекое прошлое — в 1997 год!
Python 1.4
Мало кто помнит эти времена. Это даже не Python 2. Тем не менее уже в этой версии языка были словари — тип данных dict
.
Приведенный ниже код:
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = {}
for char in letters:
if letter_counts.has_key(char):
letter_counts[char] = letter_counts[char] + 1
else:
letter_counts[char] = 1
print letter_counts
выводит (порядок может отличаться):
{'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1}
В приведенном выше коде мы итерируем список letters
и проверяем каждый элемент на наличие его в словаре letter_counts
. В случае отсутствия мы его туда добавляем, а в случае присутствия просто увеличиваем на единицу его счетчик.
Обратите внимание, что в Python 1.4 вместо использования привычного нам оператора принадлежности in
(он был добавлен только в Python 2.2) мы используем словарный метод has_key()
(которого сейчас у словарей, конечно, уже нет). Вместо оператора составного присваивания +=
(он был добавлен только в Python 2.0) мы используем обычный оператор сложения +
. Ну и наконец, в данной версии языка print
— это оператор, а не функция, поэтому мы не используем круглые скобки вокруг letter_counts
.
Приведенный выше код следует идеологии LBYL-подхода (Look Before You Leap) — "посмотри перед прыжком": сначала мы проверяем наличие элемента в словаре, а уже после совершаем действия.
Можно переписать приведенную выше программу с использованием конструкции try-except
, которая уже имелась в Python 1.4.
Приведенный ниже код:
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = {}
for char in letters:
try:
letter_counts[char] = letter_counts[char] + 1
except KeyError:
letter_counts[char] = 1
print letter_counts
выводит (порядок может отличаться):
{'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1}
Теперь наш код пытается увеличить счетчик для каждого элемента списка, не проверяя явно его наличие в словаре. Если элемент отсутствует в словаре, то будет возбуждено исключение KeyError
, которое затем будет перехвачено.
Теперь приведенный выше код следует идеологии EAFP-подхода (Easier to Ask Forgiveness than Permission) — "проще просить прощения, чем разрешения". Она, кстати, считается более Pythonic. Напомним, Pythonic — стиль кода, который соответствует идиомам Python, он читабельный и понятный.
Python 1.5
В Python 1.5 словарям был добавлен метод get(key, default=None)
, который возвращает значение словаря, соответствующее ключу key
, если ключ находится в словаре. Если ключ отсутствует, то метод возвращает значение по умолчаниюdefault
.
Приведенный ниже код:
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = {}
for char in letters:
letter_counts[char] = letter_counts.get(char, 0) + 1
print letter_counts
выводит (порядок может отличаться):
{'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1}
В приведенном выше коде мы итерируем список letters
и с помощью метода get()
получаем из словаря letter_counts
текущее количество рассматриваемого элемента списка, увеличиваем его на единицу и записываем обратно в словарь увеличенное значение. Если в словаре еще нет ключа для некоторого элемента, то используется значение по умолчанию, равное 0
.
Python 2.0
В Python 2.0 словарям был добавлен метод setdefault(key, default=None)
. Данный метод возвращает значение словаря, соответствующее ключу key
, при этом:
-
если указанный ключ
key
отсутствует в словаре, метод вставит его в словарь со значениемdefault
и вернет значениеdefault
-
если значение по умолчанию
default
не установлено и ключ отсутствует, метод вставит ключ в словарь со значениемNone
и вернет значениеNone
В Python 2.0 также стал доступен оператор составного присваивания +=
. Перепишем наш код, используя новые возможности.
Приведенный ниже код:
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = {}
for char in letters:
letter_counts.setdefault(char, 0)
letter_counts[char] += 1
print letter_counts
выводит (порядок может отличаться):
{'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1}
К минусам такого кода можно отнести тот факт, что метод setdefault()
вызывается на каждой итерации вне зависимости от того, нужен он или нет.
Python 2.3
В Python 2.3 словарям был добавлен метод fromkeys(iterable, value=None)
, который формирует словарь с ключами из указанной последовательности iterable
и значениями, установленными в value
.
Приведенный ниже код:
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = dict.fromkeys(letters, 0)
for char in letters:
letter_counts[char] += 1
print letter_counts
выводит (порядок может отличаться):
{'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1}
Здесь создается словарь, который уже содержит все необходимые ключи, поэтому нам не нужно беспокоиться об их добавлении, избегая лишних проверок, что является огромным плюсом.
Python 2.5
С выходом Python 2.5 в стандартную библиотеку языка был добавлен класс defaultdict
, который доступен из модуля collections
. Этот класс похож на обычный словарь за тем исключением, что он может использовать произвольный вызываемый объект, который возвращает значение по умолчанию для его ключей в случае их отсутствия.
Приведенный ниже код:
from collections import defaultdict
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = defaultdict(int)
for char in letters:
letter_counts[char] += 1
print letter_counts
выводит (порядок может отличаться):
defaultdict(<class 'int'>, {'a': 4, 'b': 2, 'c': 3, 't': 2, 'p': 1, 'y': 1})
В нашем коде вызываемым объектом является класс int
, который возвращает значение 0
в качестве значения по умолчанию для отсутствующих ключей словаря.
Python 2.7
С выходом Python 2.7 в стандартную библиотеку языка был добавлен класс Counter
, который так же, как и defaultdict
, доступен из модуля collections
. Класс Counter
— это специальный вид словаря, который заточен под подсчет.
Приведенный ниже код:
from collections import Counter
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = Counter(letters)
print letter_counts
выводит (порядок может отличаться):
Counter({'a': 4, 'c': 3, 'b': 2, 't': 2, 'p': 1, 'y': 1})
Использование класса Counter
является, пожалуй, самым Pythonic способом: проще придумать уже невозможно, да и не нужно!
Отметим, что классы defaultdict
и Counter
являются подклассами обычных словарей, то есть типа dict
.
Приведенный ниже код:
from collections import defaultdict, Counter
print(issubclass(defaultdict, dict))
print(issubclass(Counter, dict))
выводит:
True
True
Python 3.x
В современном Python (версия 3.x) самым Pythonic способом решить задачу подсчета является рассмотренный выше класс Counter
.
Приведенный ниже код:
from collections import Counter
letters = ['a', 'b', 'c', 'a', 'c', 'a', 't', 'p', 'b', 'a', 'y', 't', 'c']
letter_counts = Counter(letters)
print(letter_counts)
выводит:
Counter({'a': 4, 'c': 3, 'b': 2, 't': 2, 'p': 1, 'y': 1})
В последних версиях языка класс Counter
стал намного быстрее, его оптимизировали специально под задачу подсчета. Исходный код класса Counter
доступен по ссылке.
Заключение
Согласно философии Python "должен существовать один и желательно только один очевидный способ сделать что-то". Но не всегда очевидное бывает единственным, а единственное — очевидным. Очевидный способ может меняться в зависимости от времени, необходимости, возможностей языка и наших знаний.
P.S. Словари в Python занимают очень важное место. Во-первых, они очень активно используются самим языком "под капотом". Во-вторых, их активно применяют программисты, пишущие на Python. В последних версиях языка словари сильно оптимизировали: они стали быстрее, потребляют меньше памяти, а еще стали упорядоченными.
Присоединяйтесь к нашему телеграм-каналу, будет интересно и познавательно!
❤️Happy Pythoning!🐍
Автор: Тимур Гуев