Программирование и научные вычисления на языке Python/§13

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



## Основные операции

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



### Выделение подстрок

Первое, что мы рассмотрим — выделение подстрок с помощью срезов. Выражение `s[i:j]` подобно срезу, воздействующему на список, вырезает символы из строки `s`, начиная с `i` по `j-1` включительно:



```
>>> s = 'Berlin: 18.4 C at 4 pm'
>>> s[8:] # с индекса 8 до конца строки
'18.4 C at 4 pm'
>>> s[8:12] # символы с индексами 8, 9, 10 и 11
'18.4'
```



### Поиск подстрок

Вызов `s.find(s1)` в случае обнаружения в строке `s` подстроки `s1` возвращает индекс, с которого начинается подстрока. В случае, если подстрока не найдена, возвращается значение -1:



```
>>> s.find('Berlin') # где начинается 'Berlin'?
0
>>> s.find('pm')
20
>>> s.find('Oslo') # не найдет
-1
```


Иногда нужно только проверить имеется ли такая подстрока в строке, для этого, как и ранее, используется оператор `in`:



```
>>> 'Berlin' in s:
True
>>> 'Oslo' in s:
False
```


Типичный пример использования такой конструкции:



```
>>> if 'C' in s:
... print 'C found'
... else:
... print 'no C'
...
C found
```


Два других, уже личных для строк, метода, отличающихся от списков, это методы `startswith` (начинается с) и `endswith` (заканчивается с), действие которых ясно из названия:



```
>>> s.startswith('Berlin')
True
>>> s.endswith('am')
False
```



### Замещение

Вызов `s.replace(s1, s2)` заменяет в строке `s` подстроку `s1` на `s2`, везде, где встретит:



```
>>> s.replace(' ', '_')
'Berlin:_18.4_C_at_4_pm'
>>> s.replace('Berlin', 'Bonn')
'Bonn: 18.4 C at 4 pm'
```


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



```
>>> s.replace(s[:s.find(':')], 'Bonn')
'Bonn: 18.4 C at 4 pm'
```



### Разбиение

Вызов `s.split()` разбивает строку `s` на слова, разделенные пробелами, табуляциями и символами новой строки:



```
>>> s.split()
['Berlin:', '18.4', 'C', 'at', '4', 'pm']
```


Дробление строки на слова, разделенные символом или текстом `t` осуществляется с помощью передачи этого текста в качестве аргумента `s.split(t)`:



```
>>> s.split(':')
['Berlin', ' 18.4 C at 4 pm']
```


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



```
>>> t = '1st line\n2nd line\n3rd line'
>>> print t
1st line
2nd line
3rd line
>>> t.splitlines()
['1st line', '2nd line', '3rd line']
```



### Верхний и нижний регистр

Перевести все символы в нижний регистр легко позволит `s.lower(`), обратный случай, перевод букв в верхний регистр — `s.upper()`



```
>>> s.lower()
'berlin: 18.4 c at 4 pm'
>>> s.upper()
'BERLIN: 18.4 C AT 4 PM'
```



### Строки это константы

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



```
>>> s[18] = 5
...
TypeError: 'str' object does not support item assignment
```


Если же вы действительно хотите заменить символ `s[18]`, придется создать новую строку, например, с помощью срезов:



```
>>> s[:18] + '5' + s[19:]
'Berlin: 18.4 C at 5 pm'
```



### Проверка на цифры

Для того, чтобы определить имеются ли в строке только символы цифр, существует простой метод `isdigit`:



```
>>> '214'.isdigit()
True
>>> ' 214 '.isdigit()
False
>>> '2.14'.isdigit()
False
```



### Проверка на whitespace

Точно также можно проверить содержатся ли пробелы, символы табуляции и пустой строки (все это и называется whitespace) с помощью `isspace`:



```
>>> ' '.isspace() # пробелы
True
>>> ' \n'.isspace() # символ новой строки
True
>>> ' \t '.isspace() # табуляция
True
>>> ''.isspace() # пустая строка
False
```


Метод isspace удобен для нахождения в файле пустых строк. Альтернативный способ — использование метода `strip`, отрезающего лишние whitespace-символы по краям строки:



```
>>> line = ' \n'
>>> line.strip() == '' # осталась ли только пустая строка
True
>>> s = ' text with leading/trailing space \n'
>>> s.strip()
'text with leading/trailing space'
>>> s.lstrip() # left strip
'text with leading/trailing space \n'
>>> s.rstrip() # right strip
' text with leading/trailing space'
```



### Объединение строк

Противоположный методу `split` метод `join`, объединяет строки из списка в одну строку. Использование ясно из примера и без пояснений:



```
>>> strings = ['Newton', 'Secant', 'Bisection']
>>> t = ', '.join(strings)
>>> t
'Newton, Secant, Bisection'
```


Хотя методы `split` и `join` прямо противоположны, вместе они могут работать очень полезно. Например, нам нужно, вырезать из строки первые два слова:



```
>>> line = 'This is a line of words separated by space'
>>> words = line.split()
>>> line2 = ' '.join(words[2:])
>>> line2
'a line of words separated by space'
```



## Пример: Чтение пар чисел

Пусть у нас есть файл, состоящий из пар действительных чисел, записанных в формате (*a*, *b*). Такое представление часто используется для задания на плоскости точек, векторов, комплексных чисел. Пусть наш файл `read_pairs1.dat` будет содержать такие пары:



(1.3,0) (-1,2) (3,-1.5) (0,1) (1,0) (1,1) (0,-0.01) (10.5,-1) (2.5,-2.5)


Наша задача считать эти пары чисел во вложенный список, состоящий из представленных в виде кортежей пар действительных чисел. Чтобы решить эту задачу мы должны строка за строкой прочитать файл, каждую строку разбить на слова по whitespace. Далее в словах надо избавиться от скобок, разбить слово по символу запятой и конвертировать слова в числа. И вот как этот алгоритм выполнится на Python:



```
lines = open('read_pairs1.dat', 'r').readlines()
pairs = [] # список для пар чисел (n1, n2)
for line in lines:
words = line.split()
for word in words:
word = word[1:-1] # отрезаем скобки
n1, n2 = word.split(',')
n1 = float(n1); n2 = float(n2)
pair = (n1, n2)
pairs.append(pair) # добавить кортеж в список
infile.close()
import pprint
pprint.pprint(pairs)
```


После запуска программы имеем список



```
[(1.3, 0.0),
(-1.0, 2.0),
(3.0, -1.5),
(0.0, 1.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, -0.01),
(10.5, -1.0),
(2.5, -2.5)]
```


Но данная задача будет так решаться в случае, если между скобками нет пробелов. Если же пробелы имеются, то `split`, разбивая строку по пробелам, разобьет и наши пару, на '(a' и 'b)'. Что мы можем тогда сделать?

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



```
infile = open('read_pairs2.dat', 'r') # уже файл с пробелами
lines = infile.readlines()
pairs = 
for line in lines:
line = line.strip() # удаляем whitespace на концах строк
line = line.replace(' ', '') # удаляем все пробелы
words = line.split(')(') # разбиваем по )(
# обрезаем открывающую и закрывающую скобки на концах:
words[0] = words[0][1:] # (-1,3 -> -1,3
words[-1] = words[-1][:-1] # 8.5,9) -> 8.5,9
for word in words:
n1, n2 = word.split(',')
n1 = float(n1); n2 = float(n2)
pair = (n1, n2)
pairs.append(pair)
infile.close()
import pprint
pprint.pprint(pairs)
```


Третий возможный вариант — нотация в виде пар чисел, разделенных запятыми:



(1, 3.0), (-1, 2), (3, -1.5) (0, 1), (1, 0), (1, 1)


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



```
[(1, 3.0), (-1, 2), (3, -1.5),
(0, 1), (1, 0), (1, 1)]
```


А когда что-то похоже на код самого Python, стоит подумать о волшебной функции `eval`, той самой, что принимает строковый аргумент и исполняет код. Выглядеть это будет так:



```
infile = open('read_pairs3.dat', 'r')
listtext = '['
for line in infile:
# добавить строку, без перехода на новую (line[:-1]), плюс запятая:
listtext += line[:-1] + ', '
infile.close()
listtext = listtext + ']'
pairs = eval(listtext)
infile.close()
import pprint
pprint.pprint(pairs)
```


Как видно из краткости и простоты кода, это вообще весьма неплохая идея использовать форматирование, наиболее близкое к Python, что позволяет обрабатывать такие данные с помощью `eval` или `exec` прямо-таки «на лету».



## Пример: Чтение координат

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



x=-1.345 y= 0.1112 z= 9.1928 x=-1.231 y=-0.1251 z= 1001.2 x= 0.100 y= 1.4344E+6 z=-1.0100 x= 0.200 y= 0.0012 z=-1.3423E+4 x= 1.5E+5 y=-0.7666 z= 1027


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

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



### Решение 1: Вырезание подстрок

Форматирование файла довольно однообразно: каждый столбец выделяется тем, что содержит `x=, y=, z=`. При этом столбцы выравнены по левому краю, значит текст, им соответствующий, в каждой строке начинается с одних и тех же индексов:



```
x_start = 2
y_start = 16
z_start = 31
```


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



```
x = line[x_start+2:y_start]
y = line[y_start+2:z_start]
z = line[z_start+2:]
```


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



```
infile = open('xyz.dat', 'r')
coor = [] # список кортежей (x,y,z)
for line in infile:
x_start = 2
y_start = 16
z_start = 31
x = line[x_start+2:y_start]
y = line[y_start+2:z_start]
z = line[z_start+2:]
print 'debug: x="%s", y="%s", z="%s"' % (x,y,z)
coor.append((float(x), float(y), float(z)))
infile.close()
from numpy import *
coor = array(coor)
print coor.shape, coor
```



### Решение 2: Поиск вместо подсчета

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



```
x_start = line.find('x=')
y_start = line.find('y=')
z_start = line.find('z=')
```



### Решение 3: Разбиение строк

Разбиение строк это вообще мощный инструмент, также и в нашем случае. Разбиение строк по символу равенства, даст нам в первой строке слова

```
['x', '-1.345 y', ' 0.1112 z', ' 9.1928']
```


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



```
infile = open('xyz.dat', 'r')
coor = 
for line in infile:
words = line.split('=')
x = float(words[1][:-1])
y = float(words[2][:-1])
z = float(words[3])
coor.append((x, y, z))
infile.close()
from numpy import *
coor = array(coor)
print coor.shape, coor
```



## Русский язык в Python

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

Итак, действия эти несложные. Наиболее естественным решением, служит задание кодировки с помощью строки в начале текста нашей программы. Далее, чтобы строка печаталась в указанной кодировке, перед строкой приписываем букву `u`:



```
# -*- coding: utf-8 -*-
print u'Ох ух уж эти кодировки!'
```