Строки

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

>>> for x in range(0, 200, 20):
...     F_x = x**2/(x + 1)
...     G_x = (-1)**x*F_x**0.5 - 1
...     s = '| x = {0:<3} | F(x) = {1:<6.2f} | G(F(x)) = {2:<+8.4f} |'
...     print(s.format(x, F_x, G_x))
... 
| x = 0   | F(x) = 0.00   | G(F(x)) = -1.0000  |
| x = 20  | F(x) = 19.05  | G(F(x)) = +3.3644  |
| x = 40  | F(x) = 39.02  | G(F(x)) = +5.2470  |
| x = 60  | F(x) = 59.02  | G(F(x)) = +6.6822  |
| x = 80  | F(x) = 79.01  | G(F(x)) = +7.8889  |
| x = 100 | F(x) = 99.01  | G(F(x)) = +8.9504  |
| x = 120 | F(x) = 119.01 | G(F(x)) = +9.9091  |
| x = 140 | F(x) = 139.01 | G(F(x)) = +10.7901 |
| x = 160 | F(x) = 159.01 | G(F(x)) = +11.6098 |
| x = 180 | F(x) = 179.01 | G(F(x)) = +12.3793 |

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

Наверняка, вам придется строить различные графики, и, вероятнее всего, вам захочется что бы графики сопровождали красивые математические формулы. Тогда вам придется познакомиться с LaTeX и "сырыми" строками Python, что бы добавлять их:

import numpy as np, matplotlib.pyplot as plt
plt.hist(np.random.beta(70, 8, 100000), bins = 'fd')
title = r'$f_{X}(x)={\frac{1}{\mathrm{B}(\alpha,\beta)}}\,x^{\alpha-1}(1-x)^{\beta-1}$'
plt.title(title, fontsize = 20, pad = 20)
plt.show()

Представление latex формул в виде неформатированных строк для графиков matplotlib Python

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

На самом деле, работа с текстом, точнее компьютерная обработка текста на натуральном человеческом языке – это очень удивительная штука, хотя бы потому что, лишь одной лингвистики в этом деле недостаточно. Вам потребуется знание линейной алгебры, теории графов, теории вероятности и математической статистики, теории множеств и других математических разделов. Я считаю, что это как минимум любопытно (хотя на самом деле я считаю, что это очень интересно).

Учитывая перспективы, которые нам открывает работа с текстом и язык Python, который будет нашим инструментом в этом деле, давайте для начала разберемся с типом данных str. Этот тип позволяет представить любой текст (и не только текст) в виде строк. Так что же такое строки?

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

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

Строки — это встроенные объекты. Это означает, что строки являются неотъемлемой частью языка и в этом нет ничего удивительного. Однако, они являются объектами и вот это уже любопытно. И вот почему – в языке Python, в контексте объектно ориентированного программирования, объектами является абсолтно все, т.е. мы не просто можем использовать типы данных, а еще и переопределять их и даже перегружать операторы для работы с ними. Далее вы узнаете, что строки поддерживают операторы "+" (конкатенация) и "*" (повторение). Однако, мы можем придумать для них и другие операторы, например "%" остаток от деления строки на равное количество подстрок. Звучит как бред конечно, но в каждом бреде есть доля реальности, а в каждой реальности есть доля бреда... парадокс... мда. Что там дальше?

Строки создаются с помощью строковых литералов. Благодаря этим литералам Python, собственно и понимает, что перед ним находится объект-строка. Вот несколько примеров создания строк:

>>> s = "Привет мир!!!"
>>> 
>>> s = """Привет мир!!!"""    #  а можно создать и так
>>> 
>>> s = 'Привет мир!!!'        #  но чаще всего вот так

Немного лукавя (строковым литералам посвящена целая страница, где нет никакого лукавства, см. Литералы строк), можно сказать, что апострофы и кавычки – это и есть литералы строк. В самом деле, возьмем число 321321, поместим его в апострофы и уже получим строку '321321'. А убедиться в этом мы можем с помощью встроенной функции type():

>>> type(321321)
<class 'int'>
>>> 
>>> type('321321')
<class 'str'>

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

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

>>> s[5]    #  индекс символа относительно начала строки
'т'
>>> s[-8]    #  индекс того же символа относительно конца строки
'т'

Раз уж мы можем получить доступ к отдельным символам, то почему бы не предположить, что мы можем эти символы менять прямо внутри строки. Давайте заменим строчную букву 'м' на прописную 'М':

>>> s[7] = 'М'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

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

Хорошо, команда s[7] = 'М' не сработала. Однако, 'М' – что это, строка или отдельный символ? На самом деле 'М' – строка, в Python нет специального типа данных для отдельных символов. Поэтому строки из нескольких символов можно рассматривать, как последовательность строк единичной длины, которые получились в результате их конкатенации ("склеивания"):

>>> 'М' + 'и' + 'р'
'Мир'

Конкатенация, вполне уместно, обозначается символом "+", но строки можно еще и "умножать", т.е. повторять заданное количество раз:

>>> 'Мир!!!' * 7
'Мир!!!Мир!!!Мир!!!Мир!!!Мир!!!Мир!!!Мир!!!'

Строковые функции, методы и операторы

Строки являются последовательностями, а последовательности в языке Python образуют целый класс типов данных, который объединяет наличие общих свойств, а следовательно, общих функций и операторов. Например списки – это последовательности объектов, доступ к которым так же осуществляется по их индексу:

>>> s = 'Hello World!!!'    #  строка
>>> l = [1, 'a', 2.48, 'b']     #  список
>>> 
>>> s[1]
'e'
>>> l[1]
'a'

Списки, так же как и строки могут "складываться" (соединяться) и "умножаться" (дублироваться):

>>> l + l
[1, 'a', 2.48, 'b', 1, 'a', 2.48, 'b']
>>> 
>>> l*3
[1, 'a', 2.48, 'b', 1, 'a', 2.48, 'b', 1, 'a', 2.48, 'b']

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

>>> len(s), len(l)
(14, 4)

Операторы in и not in позволяют выяснить наличие или отсутствие в последовательности некоторого элемента:

>>> 'D' in 'ABCD'
True
>>> 'F' not in 'ABCD'
True
>>> 
>>> 1 in [1, 2, 3]
True
>>> 4 not in [1, 2, 3]
True

С помощью оператора in можно осуществлять перебор всех элементов любой последовательности в цикле for:

>>> for i in 'abcd':    #  поочередно перебираем каждый символ строки
...     print(i.capitalize())     #  и печатаем его в верхнем регистре
... 
A
B
C
D
>>> 
>>> for i in [1, 1.5, 2, 2.5]:    #  поочередно перебираем каждый элемент списка
...     print(i**2)     #  и печатаем его квадрат
... 
1
2.25
4
6.25

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

>>> 'ABCABCABC'.replace('AB', '@')    #  замена подстрок по шаблону
'@C@C@C'
>>> 
>>> 'AA AB AC AD AD AD'.find('AD')    #  индекс первого вхождения
9
>>> 
>>> 'AbCdEfG'.lower()    # все символы в нижнем регистре
'abcdefg'
>>> 
>>> 'AbCdEfG'.upper()    # все символы в верхнем регистре
'ABCDEFG'
>>> 
>>> '1,2.48,1 - 1j'.split(',')    #  разбить в список по разделителю ','
['1', '2.48', '1 - 1j']
>>> 
>>> ','.join(['1', '2.48', '1 - 1j'])    #  собрать из списка по разделителю ','
'1,2.48,1 - 1j'

Что бы посмотреть на список всех доступных строкам функций и методов достаточно передать функции dir() какой-нибудь строковый объект:

>>> import pprint    #  модуль для "красивой печати"
>>> 
>>> #  Сделаем вывод длинющего списка компактным:
... pprint.pprint(dir('s'), compact = True)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__',
 '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
 '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__',
 '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__',
 '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__',
 '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold',
 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format',
 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit',
 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle',
 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition',
 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip',
 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate',
 'upper', 'zfill']

Мы не спроста, воспользовались модулем pprint так как простая команда dir('s') привела бы к выводу длинного списка в виде одной длинной строки. Имейте ввиду, что данный модуль может оказаться очень полезным для организации удобочитаемого вывода данных и в некоторых ситуациях, гораздо проще воспользоваться именно им, чем ломать голову над форматированием строки для удобочитаемого вывода.


Кодировки

Другим любопытным моментом является то, что символы которые мы видим внутри строки на самом деле являются порядковыми номерами в таблице которая ставит в соответсвие этому номеру определенный сивол. Эти таблицы мы называем кодировками. Существует очень много кодировок, но возможно вы слышали названия некоторых из них: ASCII, Latin-1, КОИ-8, utf-8. По умолчанию, в Python используется стандарт "Юникод". И в том что каждому символу соответствует определенный код очень легко убедиться с помощью встроенных функций ord() и chr():

>>> ord('&')    # узнаем соответствующий символу код
38
>>> chr(38)    # узнаем соответствующий коду символ
'&'

Но к превеликому сожалению, это не только любопытно, но еще и очень печально. Представим себе, что наша программа должна обмениваться текстом с другой программой. Так как строки хранятся в виде байтов, то в нашу программу должна прилететь строка, которая может выглядеть, например, вот так b'\xcd\xc9\xd2'. Что же с ней делать?:

>>> s = b'\xcd\xc9\xd2'
>>> 
>>> str(s, encoding = 'utf-8')    #  попробуем преобразовать в Юникод
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcd in position 0: invalid continuation byte

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

>>> str(s, encoding = 'cp866')    #  попробуем преобразовать в cp866
'═╔╥'

Какя-то абракадабра. Пробуем следующую:

>>> str(s, encoding = 'koi8_r')    #  удача
'мир'

Как видите, байтовые строки не несут информации о своей кодировке, хотя в зависимости от происхождения, эта кодировка может быть какой угодно. Рассмотренная проблема встречается очень редко, но все же встречается. Многие научные программы до сих пор используют кодировку ascii по умолчанию, а некоторые операционные системы могут использовать какую-то другую кодировку. Вообще, кодировкой по умолчанию является кодировка операционной системы (можно узнать с помощью функции getdefaultencoding() модуля sys). Так что, если вы создаете интернациональное приложение или сайт, или не знаете с какой операционкой придется работать вашей программе, то наверняка тоже встретитесь с этой проблемой. Повторюсь, проблема редкая, весьма специфичная и Python предоставляет относительно неплохие средства для ее преодоления.

Чтож, вот мы и познакомились со строками. Определение, которое мы дали в начале, могло показаться очень сложным и непонятным (я даже не совсем уверен в его правильности), но тем не менее, на деле, все оказалось довольно простым.


Строки байтов — bytes и bytearray

Определение которое мы дале в самом начале можно считать верным только для строк типа str. Но в Python имеется еще два дугих типа строк: bytes – неизменяемое строковое представление двоичных данных и bytearray – тоже что и bytes, только допускает непосредственное изменение.

Основное отличие типа str от bytes и bytearray заключается в том, что str всегда пытается превратить последовательность байтов в текст указанной кодировки. По умолчанию этой кодировкой является utf-8, но это очень большая кодировка и другие кодировки, например ASCII, Latin-1 и другие являются ее подмножествами. Одни символы кодируются одним байтом, другие двумя, а некоторые тремя и функция str() при декодировании последовательности байтов принимает это во внимание. А вот функциям bytes() и bytearray() до этого нет дела, для них абсолютно все данные состоят только из последовательности одиночных байтов.

Такое поведение bytes и bytearray очень удобно, если вы работаете с изображениями, аудиофайлами или сетевым трафиком. В этом случае, вам следует знать, что ничего магического в этих типах нет, они поддерживоют все теже строковые методы, операции индексирования, а так же операторы и функции для работы с последовательностями. Единственное, что следует держать в уме, так это то, что вы имеете дело с последовательностью байтов, т.е. последовательностью чисел из интервала [0; 255] в шестнадцатеричном представлении, и что байтовые строки отличаются от обычных символом b (режеB) предваряющим все литералы обычных строк.

Например, что бы создать строку типа bytes или bytearray достаточно передать соответствующим функциям последовательности целых чисел:

>>> a = bytes([1, 2, 3])
>>> a
b'\x01\x02\x03'
>>> 
>>> 
>>> b = bytearray([1, 2, 3])
>>> b
bytearray(b'\x01\x02\x03')

Учитывая то, что для кодирования некоторых символов (например ASCII) достаточно всего одного байта, данные типы пытаются представить последовательности в виде символов если это возможно. Например, строка '\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21' будет выведена как b'hello world!':

>>> b'\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21'
b'hello world!'

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

>>> a = b'aA1cZ22.sD'
>>> a
b'aA1cZ22.sD'

Но, следует помнить что это все-таки байты, в чем легко убедиться, если мы обратимся к какому-нибудь символу по его индексу в строке:

>>> a[4]
90

Так как строковые методы не изменяют сам объект, а создают новый, то при работе с очень длинными строками (а в мире двоичных данных это далеко не редкость) это может привести к большому расходу памяти. Собственно, по этой причине и существует тип bytearray, который позволяет менять байты прямо внутри строки:

>>> s = bytearray(b'hello world')
>>> s
bytearray(b'hello world')
>>> 
>>> s[0] = 72           #  код символа 'H'
>>> s[6] = ord('W')     #  функция ord() возвращает код нужного символа
>>> 
>>> s
bytearray(b'Hello World')