Коллекции в Python

Collections — это встроенный модуль Python, предоставляющий такие полезные типы данных, как контейнеры.

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

Сегодня мы разберем различные структуры данных, поддерживаемые модулем collections, и на примерах рассмотрим, когда их стоит использовать.

Содержание

  1. Именованный кортеж namedtuple
    1. Что такое именованный кортеж
    2. Альтернативный способ создания namedtuple
    3. Зачем использовать namedtuple вместо обычного словаря
    4. Как создать namedtuple из словаря в Python
    5. Как заменить атрибут в именованном кортеже
  2. Counter
  3. Defaultdict
  4. OrderedDict
    1. Что происходит, когда вы удаляете и повторно вставляете ключи в OrderedDict
    2. Сортировка с помощью OrderedDict
  5. ChainMap
    1. Что происходит, когда у нас есть избыточные ключи в ChainMap
    2. Как добавить новый словарь в ChainMap
    3. Как изменить порядок словарей в ChainMap
  6. UserList
  7. UserString
  8. UserDict

Итак, давайте приступать! Однако сначала нам обязательно нужно импортировать collections:

# Import the collections module
import collections

Именованный кортеж namedtuple

Что такое именованный кортеж?

namedtuple можно представить как расширенную версию обычного кортежа tuple либо как быстрый способ создать класс с определенными именованными атрибутами.

Ключевое различие между кортежем и именованным кортежем заключается в следующем. Кортеж позволяет вам обращаться к данным через индексы, а с именованным кортежем вы можете получить доступ к элементам непосредственно по их именам.

Фактически вы можете определить, какие атрибуты может содержать именованный кортеж, и создать несколько его экземпляров. Точно так же, как вы поступили бы с классами.

Таким образом, с точки зрения функциональности он больше похож на класс, хотя и называется кортежем.

[python_ad_block]

Давайте создадим namedtuple, который представляет из себя фильм movie с такими атрибутами, как жанр (genre), рейтинг (rating) и ведущий актер (lead_actor).

# Creating a namedtuple. 

# The field values are passed as a string seperated by ' '
from collections import namedtuple
movie = namedtuple('movie','genre rating lead_actor')

# Create instances of movie
ironman = movie(genre='action',rating=8.5,lead_actor='robert downey junior')
titanic = movie(genre='romance',rating=8,lead_actor='leonardo dicaprio')
seven   = movie(genre='crime',rating=9,lead_actor='Brad Pitt')

Теперь мы можем получить доступ к любой информации о нашем фильме, используя идентификатор. Это довольно быстро и удобно.

# Access the fields
print(titanic.genre)
print(seven.lead_actor)
print(ironman.rating)

# Вывод:
# romance
# Brad Pitt
# 8.5

Альтернативный способ создания namedtuple

В качестве альтернативы вы можете передать список, состоящий из имен полей, вместо просто имен полей, разделенных пробелом.

К примеру, это может выглядеть так:

# Creating namedtuple by passing fieldnames as a list of strings
book = namedtuple('book',['price','no_of_pages','author'])

harry_potter = book('500','367','JK ROWLING')
pride_and_prejudice = book('300','200','jane_austen')
tale = book('199','250','christie')

print('Price of pride and prejudice is ',pride_and_prejudice.price)
print('author of harry potter is',harry_potter.author)

# Вывод:
# Price of pride and prejudice is  300
# author of harry potter is JK ROWLING

Доступ к элементам в namedtuple возможен как по индексу, так и по идентификатору.

print(tale[1])

# Вывод:
# 250

Зачем использовать namedtuple вместо обычного словаря

Основным преимуществом namedtuple является то, что он занимает меньше места (памяти), чем аналогичный словарь.

Поэтому, в случае больших данных именованные кортежи эффективны.

Пример:

# Create a dict and namedtuple with same data and compare the size
import random
import sys

# Create Dict
dicts = {'numbers_1': random.randint(0, 10000),'numbers_2':random.randint(5000,10000)} 
print('Size or space occupied by dictionary',sys.getsizeof(dicts))

# converting same dictionary to a namedtuple
data=namedtuple('data',['numbers_1','numbers_2'])
my_namedtuple= data(**dicts)
print('Size or space occupied by namedtuple',sys.getsizeof(my_namedtuple))

# Вывод:
# Size or space occupied by dictionary 240
# Size or space occupied by namedtuple 64

Выполняя приведенный выше код, вы обнаружите, что namedtuple имеет размер 64 байта, тогда как словарь занимает гораздо больше — 240 байт. Это почти в 4 раза больше памяти.

А теперь представьте себе эффект при обработке большого количества таких объектов. В общем, с целью экономии памяти гораздо лучше использовать именованные кортежи.

Как создать namedtuple из словаря в Python

Вы заметили, как мы преобразовали словарь в именованный кортеж с помощью оператора **?

Все, что вам нужно сделать для этого — определить структуру namedtuple и передать словарь (**dict) этому именованному кортежу в качестве аргумента. Единственное требование заключается в том, что ключи словаря должны совпадать с именами полей namedtuple.

# Convert a dictionary into a namedtuple
dictionary=dict({'price':567,'no_of_pages':878,'author': 'cathy thomas'})

# Convert
book = namedtuple('book',['price','no_of_pages','author'])
print(book(**dictionary))

# Вывод:
# book(price=567, no_of_pages=878, author='cathy thomas')

Как заменить атрибут в именованном кортеже

Что делать, если значение одного атрибута необходимо изменить?

Вам нужно обновить его в данных. Для этого просто воспользуемся методом ._replace():

# update the price of the book
my_book=book('250','500','abc')
my_book._replace(price=300)

print("Book Price:", my_book.price)

# Вывод:
# Book Price: 250

Counter

Объект counter предоставляется библиотекой collections. Давайте поподробнее разберем, что он собой представляет.

К примеру, у вас есть список каких-то случайных чисел. Что, если вы хотите узнать, сколько раз встречается каждое число?

Счетчик counter позволяет легко вычислить частоту. Он работает не только с числами, но и с любым итерируемыми объектами, такими как строки и списки.

Counter — это подкласс dict, используемый для подсчета хешируемых объектов.

Он возвращает словарь с элементами в качестве ключей и “счетчиком” (количество вхождений элемента) в качестве значений.

Примеры

Теперь давайте разберем некоторые примеры.

#importing Counter from collections
from collections import Counter

numbers = [4,5,5,2,22,2,2,1,8,9,7,7]
num_counter = Counter(numbers)
print(num_counter)

# Вывод:
# Counter({2: 3, 5: 2, 7: 2, 4: 1, 22: 1, 1: 1, 8: 1, 9: 1})

Давайте используем счетчик counter, чтобы найти частоту вхождений каждого символа в строке.

#counter with strings
string = 'lalalalandismagic'
string_count = Counter(string)
print(string_count)

# Вывод:
# Counter({'a': 5, 'l': 4, 'i': 2, 'n': 1, 'd': 1, 's': 1, 'm': 1, 'g': 1, 'c': 1})

Как мы видим, counter позволяет посмотреть, какие элементы есть в строке и сколько их.

Усложним задачу. Допустим, у вас есть предложение и вы хотите посмотреть количество слов в нем. Как это сделать?

При помощи функции split() можно составить список слов в предложении и передать его в Counter().

# Using counter on sentences
line = 'he told her that her presentation was not that good'

list_of_words = line.split() 
line_count=Counter(list_of_words)
print(line_count)

# Вывод:
# Counter({'her': 2, 'that': 2, 'he': 1, 'told': 1, 'presentation': 1, 'was': 1, 'not': 1, 'good': 1})

Как найти наиболее частотные элементы с помощью счетчика

Счетчик очень полезен в реальных приложениях. Особенно, когда вам нужно обработать большие данные, и вы хотите узнать частотность некоторых элементов. Давайте рассмотрим несколько очень полезных методов, использующих counter.

Counter().most_common([n])

Такое выражение возвращает список «n наиболее распространенных элементов» вместе с их количеством в порядке убывания.

# Passing different values of n to most_common() function
print('The 2 most common elements in `numbers` are', Counter(numbers).most_common(2))
print('The 3 most common elements in `string` are', Counter(string).most_common(3))

# Вывод:
# The 2 most common elements in `numbers` are [(2, 3), (5, 2)]
# The 3 most common elements in `string` are [('a', 5), ('l', 4), ('i', 2)]

Метод most_common() можно использовать для вывода наиболее часто повторяющегося элемента. Используется в частотном анализе.

print(Counter(list_of_words).most_common(1))

# Вывод:
# [('her', 2)]

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

print(Counter(string).most_common(3))

# Вывод:
# [('a', 5), ('l', 4), ('i', 2)]

Что произойдет, если вы не укажете n при использовании most_common(n)?

Тогда все элементы с их количеством вхождений будут напечатаны в порядке убывания количества.

print(Counter(string).most_common())

# Вывод:
# [('a', 5),('l', 4),('i', 2),('n', 1),('d', 1),('s', 1),('m', 1),('g', 1),('c', 1)]

Еще есть метод Counter().elements(), который возвращает все элементы, количество которых больше 0.

print(sorted(string_count.elements()))

# Вывод:
# ['a', 'a', 'a', 'a', 'a', 'c', 'd', 'g', 'i', 'i', 'l', 'l', 'l', 'l', 'm', 'n', 's']

Defaultdict

Словарь представляет из себя неупорядоченный набор ключей и значений.

В парах ключ:значение ключи должны быть уникальны и неизменяемы. Поэтому список не может быть ключом словаря, так как он изменяемый. А вот кортеж может.

# Dict with tuple as keys: OKAY
{('key1', 'key2'): "value"}


# Dict with list as keys: ERROR
{['key1', 'key2']: "value"}

Чем defaultdict отличается от простого словаря?

Если вы попытаетесь получить доступ к ключу, которого нет в словаре, он выдаст ошибку KeyError. В то время как при использовании defaultdict такой ошибки не будет.

Если вы попробуете обратиться к отсутствующему ключу, defaultdict просто вернет значение по умолчанию.

Синтаксис будет следующим: defaultdict(default_factory).

При обращении к отсутствующему ключу функция default_factory вернет значение по умолчанию.

# Creating a defaultdict and trying to access a key that is not present.
from collections import defaultdict
def_dict = defaultdict(object)
def_dict['fruit'] = 'orange'
def_dict['drink'] = 'pepsi'
print(def_dict['chocolate'])

# Вывод:
# <object object at 0x7fc14cc4e1f0>

При запуске этого кода вы не получите KeyError.

Если вы хотите вывести сообщение о том, что значение запрошенного ключа отсутствует, можно определить собственную функцию и передать ее в defaultdict.

К примеру, это может выглядеть так:

# Passing a function to return default value
def print_default():
    return 'value absent'

def_dict=defaultdict(print_default)
print(def_dict['chocolate'])

# Вывод:
# value absent

Во всем остальном defaultdict ничем не отличается от обычного словаря. Синтаксис команд полностью идентичен.

Вы также можете обойти ошибку KeyError и при использовании обычного словаря — с помощью метода get().

# Make dict return a default value
mydict = {'a': 'Apple', 'b': 'Ball'}
print(mydict.get('c', 'NOT PRESENT'))
# Вывод:
# NOT PRESENT

OrderedDict

Словарь — это НЕупорядоченная коллекция пар ключ-значение. Однако OrderedDict поддерживает упорядочивание ключей.

Это в некотором роде подкласс словаря dict.

Давайте создадим обычный словарь и сделаем его OrderedDict, чтобы показать, в чем заключается разница.

# create a dict and print items
vehicle = {'bicycle': 'hercules', 'car': 'Maruti', 'bike': ' Harley', 'scooter': 'bajaj'}

print('This is normal dict')
for key,value in vehicle.items():
    print(key,value)

print('-------------------------------')

# Create an OrderedDict and print items
from collections import OrderedDict
ordered_vehicle=OrderedDict()
ordered_vehicle['bicycle']='hercules'
ordered_vehicle['car']='Maruti'
ordered_vehicle['bike']='Harley'
print('This is an ordered dict')

for key,value in ordered_vehicle.items():
    print(key,value)

# Вывод:
# This is normal dict
# bicycle hercules
# car Maruti
# bike  Harley
# scooter bajaj
-------------------------------
# This is an ordered dict
# bicycle hercules
# car Maruti
# bike Harley

В OrderedDict даже после изменения значения некоторых ключей порядок остается прежним.

# I have changed the value of car in this ordered dictionary.
ordered_vehicle['car']='BMW'# I have changed the value of car in this ordered dictionary.
for key,value in ordered_vehicle.items():
    print(key,value)

# Вывод:
# bicycle hercules
# car BMW
# bike harley davison

Что происходит, когда вы удаляете и повторно вставляете ключи в OrderedDict

При удалении ключа информация о его порядке также удаляется. Когда вы повторно вставляете ключ, он обрабатывается как новая запись и соответствующая информация сохраняется.

# deleting a key from an OrderedDict
ordered_vehicle.pop('bicycle')
for key,value in ordered_vehicle.items():
    print(key,value)

# Вывод:
# car BMW
# bike harley davison

При повторной вставке ключа запись считается новой.

# Reinserting the same key and print
ordered_vehicle['bicycle']='hercules'
for key,value in ordered_vehicle.items():
    print(key,value)

# Вывод:
# car BMW
# bike harley davison
# bicycle hercules

Вы видите, что bicycle находится в конце, так как порядок изменился, когда мы удалили ключ.

Есть несколько полезных команд, которые можно использовать с OrderedDict. К примеру, мы можем выполнять функции сортировки.

Сортировка с помощью OrderedDict

Сортировка элементов, например, по возрастанию значений, может помочь в анализе данных. Давайте посмотрим, что мы можем сделать. 

  1. Сортировка элементов по ключу KEY (в порядке возрастания):
# Sorting items in ascending order of their keys
drinks = {'coke':5,'apple juice':2,'pepsi':10}
OrderedDict(sorted(drinks.items(), key=lambda t: t[0]))

# Вывод:
# OrderedDict([('apple juice', 2), ('coke', 5), ('pepsi', 10)])
  1. Сортировка пар по значению VALUE (в порядке возрастания):
# Sorting according to values
OrderedDict(sorted(drinks.items(), key=lambda t: t[1]))

# Вывод:
# OrderedDict([('apple juice', 2), ('coke', 5), ('pepsi', 10)])
  1. Сортировка словаря по длине строки ключа (в порядке возрастания):
# Sorting according to length of key string
OrderedDict(sorted(drinks.items(), key=lambda t: len(t[0])))

# Вывод:
# OrderedDict([('coke', 5), ('pepsi', 10), ('apple juice', 2)])

ChainMap

ChainMap — это контейнерный тип данных, в котором хранится несколько словарей.

Если у вас несколько связанных или похожих словарей, зачастую их можно хранить вместе, в ChainMap.

Распечатать все элементы ChainMap можно при помощи .map:

# Creating a ChainMap from 3 dictionaries.
from collections import ChainMap
dic1={'red':5,'black':1,'white':2}
dic2={'chennai':'tamil','delhi':'hindi'}
dic3={'firstname':'bob','lastname':'mathews'}

my_chain = ChainMap(dic1,dic2,dic3)
my_chain.maps

# Вывод:
# [{'black': 1, 'red': 5, 'white': 2}, {'chennai': 'tamil', 'delhi': 'hindi'},{'firstname': 'bob', 'lastname': 'mathews'}]

Также мы можем вывести ключи всех словарей, используя функцию .keys():

print(list(my_chain.keys()))

# Вывод:
# ['firstname', 'lastname', 'chennai', 'delhi', 'red', 'black', 'white']

Можно получить и значения всех словарей в ChainMap — при помощи функции .values().

print(list(my_chain.values()))

# Вывод:
# ['bob', 'mathews', 'tamil', 'hindi', 5, 1, 2]

Что происходит, когда у нас есть избыточные ключи в ChainMap?

Возможно, что 2 словаря содержат один и тот же ключ. К примеру, как это показано ниже:

# Creating a chainmap whose dictionaries do not have unique keys
dic1 = {'red':1,'white':4}
dic2 = {'red':9,'black':8}
chain = ChainMap(dic1,dic2)
print(list(chain.keys()))

# Вывод:
# ['black', 'red', 'white']

Обратите внимание, что red не повторяется, он печатается только один раз.

Как добавить новый словарь в ChainMap?

Вы можете добавить новый словарь в начало ChainMap, используя метод .new_child().

# Add a new dictionary to the chainmap through .new_child()
print('original chainmap', chain)

new_dic={'blue':10,'yellow':12} 
chain=chain.new_child(new_dic)

print('chainmap after adding new dictioanry',chain)

# Вывод:
# original chainmap ChainMap({'red': 1, 'white': 4}, {'red': 9, 'black': 8})
# chainmap after adding new dictioanry ChainMap({'blue': 10, 'yellow': 12}, {'red': 1, 'white': 4}, {'red': 9, 'black': 8})

Как изменить порядок словарей в ChainMap?

Порядок, в котором словари хранятся в ChainMap, можно изменить с помощью функции reversed().

# We are reversing the order of dictionaries using reversed() function
print('orginal chainmap',  chain)

chain.maps = reversed(chain.maps)
print('reversed Chainmap', str(chain))

# Вывод:
# orginal chainmap ChainMap({'blue': 10, 'yellow': 12}, {'red': 1, 'white': 4}, {'red': 9, 'black': 8})
# reversed Chainmap ChainMap({'red': 9, 'black': 8}, {'red': 1, 'white': 4}, {'blue': 10, 'yellow': 12})

UserList

Надеюсь, вы знакомы со списками в Python. Если нет, то почитать о них подробнее можно в статье «Списки в Python: полное руководство для начинающих».

UserList — это похожий на список контейнерный тип данных, который является классом-оболочкой для списков.

Синтаксис будет следующим: collections.UserList([list]).

Вы передаете обычный список в качестве аргумента userlist. Этот список хранится в атрибуте ‘data’ и доступен через метод UserList.data.

# Creating a user list with argument my_list
from collections import UserList
my_list=[11,22,33,44]

# Accessing it through `data` attribute
user_list=UserList(my_list)
print(user_list.data)

# Вывод:
# [11, 22, 33, 44]

В чем польза UserLists?

Предположим, вы хотите удвоить все элементы в определенных списках. Или, может быть, вы хотите убедиться, что ни один элемент не может быть удален из заданного списка.

В таких случаях нам нужно добавить в наши списки определенное «поведение», что можно сделать с помощью UserLists.

Давайте рассмотрим, как можно использовать UserList для переопределения функциональности встроенного метода. Приведенный ниже код предотвращает добавление нового значения в список:

# Creating a userlist where adding new elements is not allowed.

class user_list(UserList):
    # function to raise error while insertion
    def append(self,s=None):
        raise RuntimeError("Authority denied for new insertion")

my_list=user_list([11,22,33,44])

# trying to insert new element
my_list.append(55)
#> ---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

<ipython-input-2-e8f22159f6e0> in <module>
      4 
      5 my_list=user_list([11,22,33,44])
----> 6 my_list.append(55)
      7 print(my_list)


&lt;ipython-input-2-e8f22159f6e0&gt; in append(self, s)
      1 class user_list(UserList):
      2     def append(self,s=None):
----> 3         raise RuntimeError("Authority denied for new insertion")
      4 
      5 my_list=user_list([11,22,33,44])


RuntimeError: Authority denied for new insertion

Код выводит сообщение RunTimeError и не позволяет добавить элемент в список. Это может быть полезно, если вы хотите не допустить внесения информации после определенного срока.

UserString

Подобно тому, как UserLists является классом-оболочкой для списков, UserString является классом-оболочкой для строк.

UserString позволяет добавлять к строке определенное поведение. Вы можете передать этому классу любой конвертируемый в строку аргумент и затем получить доступ к этой строке, используя атрибут data.

# import Userstring
from collections import UserString
num=765

# passing an string convertible argument to userdict
user_string = UserString(num)

# accessing the string stored 
user_string.data

# Вывод:
# '765'

Как видите, число 765 было преобразовано в строку «765», и доступ к ней можно получить с помощью метода UserString.data.

Как и когда можно использовать UserString

UserString можно использовать для изменения строк или выполнения определенных функций.

Предположим, вы хотите удалить определенное слово из текстового файла (где бы оно ни было). Возможно, некоторые слова в тексте неуместны.

Давайте посмотрим на пример того, как UserString можно использовать для удаления определенных слов из строки.

# Using UserString to remove odd words from the textfile
class user_string(UserString):

    def append(self, new):
        self.data = self.data + new

    def remove(self, s):
        self.data = self.data.replace(s, "")

text='apple orange grapes bananas pencil strawberry watermelon eraser'
fruits = user_string(text)

for word in ['pencil','eraser']:
    fruits.remove(word)

print(fruits)

# Вывод:
# apple orange grapes bananas  strawberry watermelon 

В этом примере pencil и eraser были удалены с помощью функционального класса user_string.

Рассмотрим другой случай. Что делать, если вам нужно заменить слово другим во всем файле?

При помощи UserString сделать это просто. Мы определили функцию внутри класса, чтобы заменить определенное слово на The Chairman во всем тексте.

# using UserString to replace the name or a word throughout.
class user_string(UserString):

    def append(self, new):
        self.data = self.data + new

    def replace(self,replace_text):
        self.data = self.data.replace(replace_text,'The Chairman')

text = 'Rajesh concluded the meeting very late. Employees were disappointed with Rajesh'
document = user_string(text)

document.replace('Rajesh')

print(document.data)

# Вывод:
# The Chairman concluded the meeting very late. Employees were disappointed with The Chairman

Как видите, слово Rajesh везде заменено на The Chairman.

UserDict

Это класс-оболочка для словарей. Его синтаксис аналогичен UserList и UserString.

Мы передаем словарь в качестве аргумента, который хранится в атрибуте ‘data’.

# importing UserDict
from collections import UserDict 
my_dict={'red':'5','white':2,'black':1} 

# Creating an UserDict 
user_dict = UserDict(my_dict) 
print(user_dict.data) 

# Вывод:
# {'red': '5', 'white': 2, 'black': 1}

Как можно использовать UserDict

UserDict позволяет нам создать словарь и модифицировать его под наши нужды. Давайте посмотрим на пример того, как UserDict можно использовать для переопределения функциональности встроенного метода. Приведенный ниже код предотвращает удаление пары ключ-значение.

# Creating a Dictionary where deletion of an  is not allowed 
class user_dict(UserDict):       
    # Function to stop delete/pop
    def pop(self, s = None):
        raise RuntimeError("Not Authorised to delete") 

data = user_dict({'red':'5','white':2,'black':1}) 

# try to delete a item
data.pop(1)
#> ---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

<ipython-input-16-2e576a68d2ad> in <module>
     12 
     13 #try to delete a item
---> 14 data.pop(1)


<ipython-input-16-2e576a68d2ad> in pop(self, s)
      5         def pop(self, s = None):
      6 
----> 7             raise RuntimeError("Not Authorised to delete")
      8 
      9 


RuntimeError: Not Authorised to delete

Вы получите сообщение RunTimeError. Это может помочь, если вы не хотите потерять данные.

Что делать, если некоторые ключи имеют ненужные значения, и вам нужно заменить их на null или 0? Эту проблему можно решить следующим образом:

class user_dict(UserDict): 
        def replace(self,key):
            self[key]='0'

file= user_dict({'red':'5','white':2,'black':1,'blue':4567890}) 

# Delete 'blue' and 'yellow'
for i in ['blue','yellow']:
    file.replace(i)

print(file)

# Вывод:
# {'red': '5', 'white': 2, 'black': 1, 'blue': '0', 'yellow': '0'}

Поле с ненужными значениями заменено на 0. И это лишь простые примеры того, как UserDict позволяет создать словарь с необходимой функциональностью.

Заключение

Итак, мы разобрали основные типы контейнерных данных с примерами. Они значительно повышают эффективность работы при использовании на больших наборах данных.

Надеемся, данная статья была вам полезна! Успехов в написании кода!

Перевод статьи «Python Collections – An Introductory Guide».