Встроенные типы данных (их назначение, методы и стандартное поведение) - Строки (часть 2)

Теорема о бесконечных обезьянах

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

Я бы уточнил: скорее поздно или очень поздно, но никак не рано.


В самом начале я уже задавался вопросом "откуда появляется текст". И я даже ответил на него - откуда угодно. В этом ответе сконцентрирована основная боль разработчика, связанная с обработкой текста. Ожидая появления текста извне, нужно готовиться к худшему.


Например, вы просите пользователя ввести его имя и фамилию, не уточняя, как это нужно сделать. При этом, вы ожидаете, что в результате у вас появятся два слова, написанные с заглавной буквы, разделенные одним пробелом. Пускай это будет мой любимый Вася Пупкин. Как поступит Вася, если у него нет чётких инструкций? Да как угодно. Сначала по привычке потопчется по пробелу для успокоения нервов, потом напишет "вася", снова подолбится по пробелу, вспоминая свою фамилию, опомнится, начнёт стирать пробелы, влезет в имя, начнёт исправлять... Короче, идеального результата с первого захода ждать не приходится.

А как поступит Вася, если у него будут чёткие инструкции? Да точно так же! Может он и не глупый парень, но... В любом случае, как бы вы ни инструктировали ваших пользователей, единственное средство получить ожидаемый вами результат - расставить жёсткие ограничения на ввод, а после него ловушки.


Например, такая задача и один из вариантов её решения "в лоб".

''' Введите два числа. Они могут быть:
- как на одной строке через пробел(-ы), так и на двух;
- как целыми, так и дробными.
Предполагается, что на первой строке введено хотя бы одно число,
и если введено только одно число, то оно введено без пробелов.'''
# ожидаем первый ввод

a = input() # input всегда результирует в строку и ни во что другое

# если в "а" есть пробел
if " " in a:
a = a.split() # разбей "а" по пробелам и сделай список
b = a[1] # b - это второй элемент списка
a = a[0] # перезаписываем a - это первый элемент списка
# если в a нет пробел - ожидай второго ввода
else:
b = input()

# если точка есть в обоих значениях
if "." in a and "." in b:
# преобразуем обе строки в числа с точкой
a = float(a)
b = float(b)
# если же точка есть только в одном из значений
elif "." in a:
# преобразуем одно значение во float, другое в int
a = float(a)
b = int(b)
elif "." in b:
b = float(b)
a = int(a)
else: # во всех остальных случаях - оба значения инты
a, b = int(a), int(b) # да, так тоже можно заводить значения в переменные

Возможно, кто-то скажет, что это не самый лучший пример, а может быть даже глупый. И я даже отчасти соглашусь. Однако, здесь на примитивном уровне показана проблема "ожидай от юзера чего угодно". Этот пример не является панацеей от слова "совсем", так как он не реализует максимальную защиту данных для последующей обработки. Во-первых, здесь предоставлен достаточно широкий выбор для порядка ввода - это уже плохо. Что, если в самом первом инпуте к нам прилетят два числа, разделённые запятой, а не пробелом, или запятой с пробелом? А что, если первый ввод был верным, а из второго к нам прилетят ещё 2 числа, пускай даже введённые через пробел? Во-вторых, нам могут ввести что угодно помимо чисел... Короче, этот код рассчитан на интеллигентного юзера, который читает инструкции и старательно их выполняет. Но даже с таким юзером этот код рано или поздно упадёт, потому что когда-то юзер обязательно ошибётся.


Такая же история происходит с данными, приходящими из интернета. Интернет большой, люди разные, текста много. Когда-то в 2000-х была мода на "ПаДоНкАфФсКиЙ яЗыК" (до сих пор не пойму, как им не лень было так заморачиваться). Но вот представьте, что к вам прилетела такая строка. Ручками править будем? Или вы работаете со стандартной кодировкой utf-8, а к вам пришла виндовская cp1251 или, упаси бог, koi-8, о которой вы или уже забыли, или знать не знаете? Короче, вариантов уронить код на казалось бы ровном месте - страшное количество.

Главное правило при работе с любыми данными - приводить их к установленному формату или стандарту. К какому именно - решать в каждом случае вам, как разработчику (ну или вашему боссу). Если вы работаете с текстовыми данными, которые приходят откуда угодно, то вам так или иначе придётся сделать так, чтобы до начала их прямого использования по назначению все они были как минимум в одном регистре и в одной кодировке. Даже если вы не хотите - жизнь заставит. И для этого помимо прочего вам понадобятся строковые методы.


Методы строк

Общий принцип вызова методов у объектов в питоне - через точку (я думаю, вы это уже заметили). Если в редакторе кода или в консоли поставить точку после встроенного объекта, у вас гарантированно выпадет список из доступных методов. Так вы всегда можете узнать, что этот объект умеет делать.


Строковые методы - это набор мощных инструментов для работы с текстовыми данными. Часть из них я вам уже продемонстрировал: encode, decode, format... Но это, во-первых, не совсем то, что вам нужно в повседневной жизни, а во-вторых, лишь верхушка айсберга. Нырнём глубже.


Конкатенация (сложение или объединение строк)

"Плюсики"

Первым делом, нужно вспомнить, что строки умеют "складываться". Я это демонстрировал ранее на примере Hello, world.

>>> a = "Hello "
>>> b = 'world'
>>> s = a + b
>>> s
'Hello world'

Также строку можно сложить с самой собой.

>>> s += s
>>> s
'Hello worldHello world'

Метод str.join([iterable])

Кроме этого, у строк есть потрясающий метод str.join(). Про него почему-то часто забывают. Данный метод также занимается конкатенированием строковых элементов из итерируемых объектов через строковый разделитель. Под итерируемыми объектами имеются в виду коллекции: кортежи, списки, множества и даже словари (из них будут извлечены ключи).

>>> my_tuple = ('AAA','BBB','CCC')
>>> my_list = ['ZZZ','YYY','XXX']
>>> my_set = {'a1','b1','c1'}
>>> my_dict = {'a': '1', 'b': '2', 'c': '3'}
>>> s1 = ' ~какой-то разделитель~ '.join(my_tuple)
>>> s2 = ' ~какой-то разделитель~ '.join(my_list)
>>> s3 = ' ~какой-то разделитель~ '.join(my_set)
>>> s4 = ' ~какой-то разделитель~ '.join(my_dict)
>>> print(s1, s2, s3, s4, sep='\n')
AAA ~какой-то разделитель~ BBB ~какой-то разделитель~ CCC
ZZZ ~какой-то разделитель~ YYY ~какой-то разделитель~ XXX
b1 ~какой-то разделитель~ a1 ~какой-то разделитель~ c1
a ~какой-то разделитель~ b ~какой-то разделитель~ c

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

>>> my_list = [1, 2, 3, 4, 5] # список чисел (int)
>>> "+".join(my_list)
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found

Если они таковыми не являются, то их необходимо конвертировать в строки. Для этого два пути:

использовать встроенную функцию map(), которая принимает в себя 2 обязательных аргумента: функцию и итерируемый объект. В результате функция, указываемая первым аргументом, будет применена к каждому элементу из итерируемого объекта, указываемого последним:

>>> "+".join(map(str, my_list))
'1+2+3+4+5'

2. (!pythonic way) использовать генераторное выражение. Их суть я планировал объяснять в отдельной теме, но посмотреть-то всегда можно:

>>> "+".join(str(element) for element in my_list)
'1+2+3+4+5'

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

>>> my_list = [1, 2, 3, 4, 5]
... s = ''
... for element in my_list:
... if not isinstance(element, str):
... element = str(element)
... s += element + "+"
... s = s[:-1]
>>> s
'1+2+3+4+5'

Дублирование строк (умножение строк)

При необходимости строку можно повторить несколько раз. Внимание, песня!

>>> s = "We wish you a Merry Christmas\n"
>>> s1 = "And a happy New Year!\n"
>>> print(s * 3 + s1) # в принте можно также производить вычисления
We wish you a Merry Christmas
We wish you a Merry Christmas
We wish you a Merry Christmas
And a happy New Year!

Чтобы не переписывать первую строку трижды, мы её просто умножили на 3, прибавили последнюю строчку и получили желаемый результат в виде куплета.

Я уже прокомментировал в коде, но ещё раз повторю - вычисления можно производить внутри принта. Порядок действий всегда выполняется согласно правилам математики: сначала деление и умножение, потом вычитание и сложение.


Чистка строк

Вспомним пример из первой части и немного видоизменим его:

>>> s = "  Hello, world!!!!  "

В начале и в конце строки стоят пробелы в разном количестве. Для очистки этих участков в питоне предусмотрено 3 метода: str.strip(), str.lstrip() и str.rstrip().

Метод str.strip() может принимать или не принимать аргументов. По умолчанию он удаляет из начала и из конца строки пробелы (сколько бы их там ни было):

>>> s[0]
' '
>>> s[-1]
' '
>>> s = s.strip()
>>> s
'Hello, world!!!!' # пробелы удалены
>>> s[0]
'H'
>>> s[-1]
'!'

Если же передать в метод какой-либо строковой символ, то он попытается удалить именно их, сколько бы их там ни было:

>>> s = s.strip('!')
>>> s
'Hello, world'

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


Как не трудно догадаться str.lstrip() и str.rstrip() делают ровно то же самое, но только либо в начале строки ('l' - left - слева - str.lstrip()) или в конце строки ("r" - right - справа - str.rstrip()). Иногда бывает полезно очистить только одну сторону, не затрагивая другую.


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

>>> 'xxxyyyzzz'.strip('x')
'yyyzzz'
>>>
>>> 'xxxyyyzzz'.rstrip('yz')
'xxx'
>>>
>>> 'xxxyyyzzz'.lstrip('xyz')
''

Последние два примера отлично показывают, что указывая группу символов нужно чётко понимать, чего вы хотите на самом деле. В метод вы передаёте не "шаблон", а "коллекцию" символов, по каждому из которых будет вестись поиск и в случае обнаружения на заданных позициях (в начале и/или в конце строки) - он непременно будет удалён. Из-за такого не совсем однозначного поведения эти методы являются относительно безопасными. Ещё разок:

>>> 'papa mama'.strip('a m')
'pap'

Разбиение строк


Методы str.split() и str.rsplit()

Один из самых востребованных и популярных методов при работе со строками - str.split(). Аналогично методу str.strip(), у него есть "правый" брат - str.rsplit(). "Левого" брата нет, так как основной метод изначально идёт по строке слева направо, а "из середины к краям" разбиение не производится, что вполне логично.


Данная группа методов разбивает строку по разделителю и создаёт из полученных элементов новый объект - список строк. Ещё раз: данные методы всегда результируют в список и ни во что другое.


Сигнатура метода: str.split(sep=None, maxsplit=-1). Аналогично str.strip(), str.split() по умолчанию сепаратором является пробел.

>>> 'мама мыла раму'.split()
['мама', 'мыла', 'раму']

Для пустой строки метод вернёт пустой список

>>> ''.split()
[]

В методе также можно указать любой другой разделитель. В этом случае это уже будет "шаблон":

>>> 'мама мыла раму'.split('ма')
['', '', ' мыла раму']

Если разделитель в строке не обнаружен, метод вернёт список, в котором один элемент - исходная строка.

>>> 'мама мыла раму'.split(',')
['мама мыла раму']

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

>>> '1-2-3-4-5-6'.split('-', 3)
['1', '2', '3', '4-5-6']
>>>
>>> '1-2-3-4-5-6'.rsplit('-', 2)
['1-2-3-4', '5', '6']
>>>
>>> '1-2-3-4-5-6'.rsplit('-', 0)
['1-2-3-4-5-6']

С практической точки зрения последний пример - бесполезный. Если мы не собираемся ничего разбивать, то нет смысла городить всю эту конструкцию. Но, тем не менее, технически такое возможно, и оно будет работать.


Методы str.partition(sep) и str.rpartition(sep)

Метод str.partition(sep) разбивает строку по первому встреченному заданному разделителю и возвращает кортеж из трех элементов: строка слева от разделителя, сам разделитель и строка справа от разделителя.

>>> 'мама мыла раму'.partition('а')
('м', 'а', 'ма мыла раму')

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

>>> 'мама мыла раму'.partition('мила')
('мама мыла раму', '', '')

Его брат str.rpartition(sep) делает то же самое, только в отношении разделителя, встреченного ближе к концу строки. Если разделитель не найден, то исходная строка будет лежать в последнем элементе кортежа.

>>> 'мама мыла раму'.rpartition('а')
('мама мыла р', 'а', 'му')
>>>
>>> 'мама мыла раму'.rpartition('мила')
('', '', 'мама мыла раму')

-------------

Кроме этого существуют ещё методы str.splitlines() и str.expandtabs() (который не совсем про разбиение, а, скорее, про растягивание). Но они используются несколько реже, поэтому я не буду их описывать. Интересующиеся уже знают, где всё искать.


Поиск вхождений и их замена


Методы str.find(sub[, start[, end]]) и str.rfind(sub[, start[, end]])

По аналогии с предыдущими примерами str.find() и str.rfind() осуществляют поиск подстроки sub в начале или в конце исходной строки. Также, поиск можно производить по срезу исходной строки. В ответ возвращается положительное значение индекса найденного вхождения.

>>> s = 'text'
>>> s.find('t')
0
>>> s.rfind('t')
3
>>>
>>> s = 'abracadabra'
>>> s.find('a', 4)
5
>>> s.rfind('a', 4, -2)
7

В случае отсутствия искомого элемента в ответ возвращается -1

>>> s = 'text'
>>> s.rfind('a')
-1

Методы str.index(sub[, start[, end]]) и str.rindex(sub[, start[, end]])

Аналогичны методам str.find() и str.rfind(), за исключением того, что вызывают исключение ValueError если строка sub не найдена.

>>> s = 'text'
>>> s.rindex('a')
Traceback (most recent call last):
File "<input>", line 1, in <module>
ValueError: substring not found

Метод str.replace(old, new[, count])

Ещё один из самых популярных методов при работе со строками.

Возвращает копию исходной строки в которой все подстроки old заменены на подстроки new. Параметр count позволяет указать количество замен.

>>> s = 'abracadabra'
>>> s.replace('a', 'A')
'AbrAcAdAbrA'
>>> s.replace('a', 'A', 3)
'AbrAcAdabra'
>>> s.replace('a', '') # удалим букву а, заменив её на пустую строку
'brcdbr'

Изменение регистра

В качестве подопытной строки возьмём s = "ПаДоНкАфФсКиЙ яЗыК". Вот вообще не жалко. Причём, перезаписывать эту строку я не буду, а буду применять в консоли методы налету, чтобы результат их работы отображался, но сама строка при этом не потерялась.


Метод str.swapcase()

Преобразует регистр всех элементов строки на строго противоположный:

>>> s.swapcase()
'пАдОнКаФфСкИй ЯзЫк'

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

>>> s.swapcase().swapcase() # метод применён дважды
'ПаДоНкАфФсКиЙ яЗыК' # cтрока вернулась в исходное состояние

Методы str.capitalize() и str.title()

str.capitalize() - Делает заглавной первую букву в первом слове в строке

str.title() - Делает Заглавными Первые Буквы Во Всех Словах В Строке

>>> s.capitalize()
'Падонкаффский язык'
>>> s.title()
'Падонкаффский Язык'

В последнем случае под словами понимается любой набор буквенных символов, разделенных НЕБУКВЕННЫМИ символами, что иногда может создавать проблемы:

>>> "let's go".title()
"Let'S Go"

Методы являются безопасными.


Методы str.lower() и str.upper()

Приводят все символы в строке к нижнему или, соответственно, верхнему регистру:

>>> s.lower()
'падонкаффский язык'
>>> s.upper()
'ПАДОНКАФФСКИЙ ЯЗЫК'

Методы являются безопасными.

Но lower() работает только для стандартных символов. Для отдельных символов, например, расширенной латиницы, он не сработает.


Метод str.casefold()

В этом случае будет полезен метод str.casefold(), который приводит все символы строки к нижнему регистру в соответствии с разделом 3.13 The Unicode Standard. Вот, что я имею в виду:

>>> "Straße".lower() # символ 'ß' заменяет двойную 'S' в немецком языке
'straße'
>>> "Straße".casefold()
'strasse'

Метод безопасный. К сожалению, метода для аналогичной обработки в обратную сторону кроме str.upper() нет.


Проверка содержимого

Все методы этой группы проверяют определенные утверждения, и результатом их работы во всех случаях является ответ в булевом значении - True (истина) или False (ложь). Ответов "не знаю", "может быть" и т.п. не существует.


Методы str.startswith(prefix[, start[, end]]) и str.endswith(suffix[, start[, end]])

Эти методы проверяют утверждения "правда ли что строка начинается (или заканчивается) таким-то символом или группой символов.

>>> 'Есть ли тут префикс???'.startswith('Ес')
True

Параметры prefix и suffix могут принимать не только одну строку в качестве шаблона, но и целый кортеж шаблонов. Если хоть один из них будет найден в начале или конце строки, то мы так же получим True:

>>> 'Есть ли тут префикс???'.startswith(('ту', 'пре', 'Ес'))
True
>>> 'Есть ли тут суффикс???'.endswith(('ут', 'ть', '??'))
True

При необходимости, дополнительные параметры start и end могут ограничить "сектор" поиска, сформировав таким образом срез исходной строки:

>>> 'Есть ли тут префикс???'.startswith('ту')
False
>>> 'Есть ли тут префикс???'.startswith('ту', 8)
True
>>> 'Есть ли тут суффикс???'.endswith('ут', 2, -11)
True
>>> 'Есть ли тут суффикс???'.endswith('?', 11)
True

Обратите внимание, что для обоих методов start всегда отсчитывается с начала строки.


Методы str.isdecimal(), str.isdigit(), str.isnumeric(), str.isalpha(), str.isalnum(), str.isascii(), str.isspace()

Все эти методы проверяют то, какие именно символы лежат в строке.


str.isdecimal() - возвращает True, если строка не пустая и состоит только из десятичных цифр (не путайте с числами).

>>> ''.isdecimal()
False
>>> '123'.isdecimal()
True
>>> '12.3'.isdecimal()
False

str.isdigit() - возвращает True, если строка не пустая и состоит только из десятичных цифр, а также символов юникода, которые относятся к цифрам. Отличается от isdecimal() тем, что ищет символы, которые не могут участвовать в записи десятичных цифр, но при этом сами состоят из цифр. Например, обозначения степеней не могут участвовать в записи чисел, но считаются цифровыми, т.к. сами состоят из цифр.

>>> '2\u00b2 = 4'
'2² = 4'
>>> '\u00b2'.isdigit()
True
>>> '\u00b2'.isdecimal()
False

str.isnumeric() - очень похож на str.isdigit(), во всяком случае определения у них совпадают. Разница в том, что он возвращает True также для цифровых символов юникода, чьё свойство Numeric_Type равно Numeric, то есть в природе оно должно обозначать число. Например, натуральные дроби:

>>> '\u00bd + \u00bd = 1'
'½ + ½ = 1'
>>> '\u00bd'.isnumeric()
True
>>>
>>> '\u00bd'.isdigit()
False
>>>
>>> '\u00bd'.isdecimal()
False

str.isalpha() - возвращает True, если строка не пустая и состоит только из букв.

>>> ''.isalpha()
False
>>>
>>> 'абв'.isalpha()
True
>>>
>>> 'абв.'.isalpha()
False
>>> 'абв1'.isalpha()
False

str.isalnum() - возвращает True, если строка не пустая и состоит только из букв и цифр.

>>> ''.isalnum()
False
>>>
>>> '111'.isalnum()
True
>>>
>>> 'aaa'.isalnum()
True
>>>
>>> '111aaa'.isalnum()
True
>>>
>>> '\u00b2'.isalnum() # степень квадрата
True
>>> '\u00bd'.isalnum() # натуральная дробь 1/2
True

str.isascii() - возвращает True, если строка состоит только из символов ASCII.

>>> ''.isascii()
True
>>> 'mama'.isascii()
True
>>> 'мама'.isascii()
False
>>> '123'.isascii()
True

str.isspace() - возвращает True, если строка состоит только из пробельных символов.

>>> ''.isspace()
False
>>>
>>> ' '.isspace()
True
>>>
>>> ' '.isspace()
True
>>>
>>> '\u001c'.isspace()
True
>>>
>>> '\xa0'.isspace()
True

-----

Конечно же, это не всё. Есть и другие методы. Я лишь перечислил самые популярные и необходимые. В случае необходимости вы знаете где искать.

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

На этом со строками всё. Спасибо за внимание.


Бонус

Хочу с вами поделиться собственным внезапным (а всё внезапное - чаще всего малоприятное) открытием. Символов вообще придумано огромное количество. Например, одних апострофов лично я знаю как минимум 3 вида (а может их на самом деле и больше, просто не попадались): ', `, ´. В качестве литерала строк используется и распознаётся только первый, но внутри самих строк я встречал все эти варианты - и это всё, как вы понимаете, разные символы. Тем не менее, тот факт, что в строках могут вдруг прилететь неразрывные пробелы, был для меня в своё время потрясением. Я искал в строке обычный пробел и не находил его, отчего мне было, мягко говоря, грустно. Глазами-то я видел "пробел", а программа - нет. Когда я наконец понял в чём дело, то даже завёл для него отдельный комментарий, чтобы не потерять. Вот так это выглядело.

Встроенные типы данных (их назначение, методы и стандартное поведение) - Строки (часть 2) IT, Python, Программирование, Длиннопост, Изучение, Языки программирования

Со временем PyCharm поумнел ещё больше (сам или от того, что это я какой-то плагин к нему прикрутил - сейчас уже сложно сказать) и научился отображать его в редакторе:

Встроенные типы данных (их назначение, методы и стандартное поведение) - Строки (часть 2) IT, Python, Программирование, Длиннопост, Изучение, Языки программирования

И это не 4 символа, а один. Однако в консоли этот символ отображается как обычный пробел. Поскольку такое встречается редко (а когда встречается, то бьёт достаточно больно), я предпочитаю такие вещи коллекционировать, чего и вам желаю. Если хотите заполучить его себе, то вот он в байтовом представлении - b'\xc2\xa0', а вот в utf-8 - '\xa0'


-----

Всё это, конечно же, дублируется в отдельном канале в телеге. По всем вопросам обращайтесь через Telegram.


Да, и тег моё - потому что всё написано моими руками, а не тупо понакопировано с других сайтов.


P.S. Большое спасибо всем моим подписчикам за поддержку и активность! Без вас я, возможно, не решился бы продолжать.


Ссылки на предыдущие посты:

1. Предлагаю помощь в освоении Питона

2. ПЕРВОЕ ЗНАКОМСТВО С PYTHON

3. Встроенные типы данных (их назначение, методы и стандартное поведение) - Введение

4. Встроенные типы данных (их назначение, методы и стандартное поведение) - Числа

5. Встроенные типы данных (их назначение, методы и стандартное поведение) - Строки (1/2)

Программирование на python

626 постов11.8K подписчиков

Добавить пост

Правила сообщества

Публиковать могут пользователи с любым рейтингом. Однако!


Приветствуется:

• уважение к читателям и авторам

• конструктивность комментариев

• простота и информативность повествования

• тег python2 или python3, если актуально

• код публиковать в виде цитаты, либо ссылкой на специализированный сайт


Не рекомендуется:

• допускать оскорбления и провокации

• распространять вредоносное ПО

• просить решить вашу полноценную задачу за вас

• нарушать правила Пикабу