Share This
Связаться со мной
Крути в низ
Categories
//Коллекции в Python

Коллекции в Python

25.04.2022Category : 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 либо как быстрый способ создать класс с определенными именованными атрибутами.

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

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

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

kollekcii v python 6a0007a - Коллекции в Python

Английский для программистов

Наш телеграм канал с тестами по английскому языку для программистов. Английский это часть карьеры программиста. Поэтому полезно заняться им уже сейчас

Подробнее ×

Давайте создадим 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».

kollekcii v python d868c8f - Коллекции в Python

Английский для программистов

Наш телеграм канал с тестами по английскому языку для программистов. Английский это часть карьеры программиста. Поэтому полезно заняться им уже сейчас

Подробнее ×

  • 4 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Свежие комментарии

    Рубрики

    About Author 01.

    blank
    Roman Spiridonov

    Моя специальность - Back-end Developer, Software Engineer Python. Мне 39 лет, я работаю в области информационных технологий более 5 лет. Опыт программирования на Python более 3 лет. На Django более 2 лет.

    Categories 05.

    © Speccy 2022 / All rights reserved

    Связаться со мной
    Close