У одной задачи может быть несколько способов решения. Возьмем классическую задачу программирования — задачу подсчета, в которой мы считаем, сколько раз каждый элемент списка встречается в нем. Способ решения этой задачи на 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)


  1. VeselkovaIrina
    29.05.2024 07:21
    +4

    Терпеть не могла словари, когда только начала их изучать, но от ненависти до любви, как говорится... Очень мощный инструмент, спасибо за познавательную статью!!!


  1. Jury_78
    29.05.2024 07:21
    +3

    Согласно философии Python "должен существовать один и желательно только один очевидный способ сделать что-то".

    По моему, из-за обилия библиотек, все это неактуально.


    1. svvaliv
      29.05.2024 07:21
      +1

      Дзен вообще штука интересная, его реализация на Python сама ему противоречит во многом. Да и придуман он был больше как шутка.


      1. 9982th
        29.05.2024 07:21
        +1

        Этого одного достаточно, чтобы перестать слишком сильно о нем заморачиваться.


    1. CrazyElf
      29.05.2024 07:21

      Не только из-за библиотек. Как видно из статьи и на чистом питоне без библиотек одно и то же можно написать обычно десятком разных способов.


  1. RONIS-RONIS
    29.05.2024 07:21

    Когда будет готов курс по алгоритмам ?)


    1. tguev Автор
      29.05.2024 07:21
      +4

      Как только, так сразу.


  1. AHTOH_79
    29.05.2024 07:21
    +1

    Зря я сюда полез...

    даже не пройдя до конца курс для начинающих


  1. Eugene_Rymarev
    29.05.2024 07:21

    Было бы неплохо дополнить всё это дело тестами быстродействия.


  1. 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.

    (источник цитаты)


  1. sci_nov
    29.05.2024 07:21

    То, что counter оптимизировали, это очень хорошо.


  1. 4p4
    29.05.2024 07:21

    >>>выводит:

    Counter({'a': 4, 'c': 3, 'b': 2, 't': 2, 'p': 1, 'y': 1})

    >>>В последних версиях языка... стали упорядоченными.

    Не вижу упорядоченности.