У одной задачи может быть несколько способов решения. Возьмем классическую задачу программирования — задачу подсчета, в которой мы считаем, сколько раз каждый элемент списка встречается в нем. Способ решения этой задачи на 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!?
Комментарии (12)
Jury_78
29.05.2024 07:21+3Согласно философии Python "должен существовать один и желательно только один очевидный способ сделать что-то".
По моему, из-за обилия библиотек, все это неактуально.
CrazyElf
29.05.2024 07:21Не только из-за библиотек. Как видно из статьи и на чистом питоне без библиотек одно и то же можно написать обычно десятком разных способов.
Groosha
29.05.2024 07:21Теперь приведенный выше код следует идеологии EAFP-подхода (Easier to Ask Forgiveness than Permission) — "проще просить прощения, чем разрешения". Она, кстати, считается более Pythonic. Напомним, Pythonic — стиль кода, который соответствует идиомам Python, он читабельный и понятный.
Гвидо с вами не согласен:
I disagree with the position that EAFP is better than LBYL, or “generally recommended” by Python.
4p4
29.05.2024 07:21>>>выводит:
Counter({'a': 4, 'c': 3, 'b': 2, 't': 2, 'p': 1, 'y': 1})
>>>В последних версиях языка... стали упорядоченными.
Не вижу упорядоченности.
VeselkovaIrina
Терпеть не могла словари, когда только начала их изучать, но от ненависти до любви, как говорится... Очень мощный инструмент, спасибо за познавательную статью!!!