Python - wprowadzenie

04 - Łańcuchy znaków - cz. II

Badamy długość łańcucha znaków i uzyskujemy jego fragmenty

Utwórzmy łańcuch znaków reprezentujący fragment RNA:

sekwencja = "UCAGUUUGGUCC"

Sprawdźmy jego długość. W tym celu użyjemy funkcji len():

len(sekwencja)
12

Można oczywiście użyć funkcji print() do wyświetlenia wyniku:

print(f"Długość sekwencji: {len(sekwencja)} nukleotydów.")
Długość sekwencji: 12 nukleotydów.

Z ciągu tekstowego możemy uzyskać pojedynczą literę:

sekwencja[1]
'C'

Zauważ, że pod numerem 1 znajduje się druga litera w kolejności. Jest tak, ponieważ kolejne znaki są ,,ponumerowane’’ począwszy od zera. Te ,,numery’’ nazywamy indeksami. Pierwsza litera ma zatem indeks 0 a ostatnia indeks równy długości łańcucha minus 1.

print(f"Pierwszy nukleotyd: {sekwencja[0]}, ostatni nukleotyd: {sekwencja[len(sekwencja)-1]}")
Pierwszy nukleotyd: U, ostatni nukleotyd: C

Jeśli chcemy uzyskać fragment łańcucha znaków, podajemy indeks pierwszego i ostatniego +1 oddzielone znakiem dwukropka (:). Na przykład chcąc uzyskać pierwsze trzy nukleotydy w sekwencji genu (kodon), możemy wykonać taki kod:

print(f"Pierwszy kodon to: {sekwencja[0:3]}")
Pierwszy kodon to: UCA

Można ten sam efekt uzyskać pomijając 0, w tym przypadku domyślnie będzie zwracany ciąg od początku:

print(f"Pierwszy kodon to: {sekwencja[:3]}")
Pierwszy kodon to: UCA

Ważne aby zapamiętać, że znak o indeksie wyznaczonym przez drugą liczbę, nie jest zwracany. Analogicznie, ostatni kodon możemy uzyskać w ten sposób:

print(f"Ostatni kodon to: {sekwencja[len(sekwencja)-3:len(sekwencja)]}")
Ostatni kodon to: UCC

W tym przypadku również można uprościć wyrażenie:

print(f"Ostatni kodon to: {sekwencja[len(sekwencja)-3:]}")
Ostatni kodon to: UCC

Albo jeszcze bardziej:

print(f"Ostatni kodon to: {sekwencja[-3:]}")
Ostatni kodon to: UCC

Łatwo się domyślić, że cały łańcuch można uzyskać tak:

print(f"Cała sekwencja to: {sekwencja[:]}")
Cała sekwencja to: UCAGUUUGGUCC

Można również do wyrażenia dodać kolejny element, oznaczający krok:

print(f"Co trzeci nukleotyd to: {sekwencja[::3]}")
Co trzeci nukleotyd to: UGUU

Otrzymaliśmy co trzeci znak z całego łańcucha. W tym przypadku pierwsze nukleotydy kodonów.

Można też wpisać ujemną wartość kroku, wtedy uzyskamy odwrócony łańcuch:

print(f"Odwrócona sekwencja to: {sekwencja[::-1]}")
Odwrócona sekwencja to: CCUGGUUUGACU

Podsumujmy:

Wyrażenie Wynik
lancuch[:] cały łańcuch
lancuch[start:stop] znaki od miejsca start do znaku o indeksie stop-1
lancuch[start:] znaki od miejsca start do końca łańcucha
lancuch[:stop] znaki od początku łańcucha do znaku o indeksie stop - 1
lancuch[::n] co n-ty znak z całego łańcucha począwszy od pierwszego
lancuch[start:stop:n] co n-ty znak z części łańcucha od miejsca start do stop -1
lancuch[::-1] odwrócony łańcuch

Proste manipulacje na łańcuchach znaków

Dodawanie i mnożenie w łańcuchach znaków

Dodajmy na końcu sekwencji dodatkowy kodon:

sekwencja = sekwencja + "UAA"
print(sekwencja)
UCAGUUUGGUCCUAA

Jak widać, łańcuch jest dłuższy o sekwencję UAA.

Można też zapisać to polecenie krócej:

sekwencja += "UAA"
print(sekwencja)
UCAGUUUGGUCCUAA

Spróbujmy teraz zmienić jedną literę:

sekwencja[0] = "A"
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-18-a2c86a882dc6> in <module>
----> 1 sekwencja[0] = "A"


TypeError: 'str' object does not support item assignment

Tym razem operacja się nie udała. Dlaczego można było wydłużyć sekwencję a podmiana jednej litery zakończyła się niepowodzeniem? W rzeczywistości łańcuchy znaków są niemodyfikowalne (ang. immutable), czyli nie można ich zmienić. Dlaczego więc udało nam się dodać dodatkowe znaki do sekwencji? Przyjrzyjmy się jeszcze raz wyrażeniu, które użyliśmy:

sekwencja = sekwencja + "UAA"

Po prawej stronie operatora = wywołaliśmy zmienną sekwencja, która zwróciła łańcuch znaków. Dodając do niego sekwencję UAA stworzyliśmy nowy łańcuch, który przypisaliśmy do zmiennej sekwencja. Zatem po całej operacji zmienna wskazywała na nowy łańcuch. Może się wydawać, że to na jedno wychodzi, ale drugi przykład pokazał, że nie. Nie możemy zmienić łańcucha, ale można utworzyć nowy ciąg znaków i np. przypisać go do zmiennej czy wyświetlić na ekranie. Dalsze przykłady manipulacji na łańcuchach znaków będą więc w rzeczywistości polegać na tworzeniu nowych ciągów tekstowych, choć dla uproszczenia będę używał sformułowań w rodzaju ,,zmiana liter w łańcuchu’’ czy ,,zmiana wielkości znaków w ciągu tekstowym’'.

Wcześniej łączyliśmy łańcuchy znaków, teraz spróbujmy je ,,pomnożyć’':

kodon = "CUA"
trzy_kodony = kodon * 3
print(f"kodon: {kodon}\ntrzy kodony: {trzy_kodony}")
kodon: CUA
trzy kodony: CUACUACUA

Zmiana wielkości znaków w łańcuchu

Zmiana wielkości znaków, polega na wywołaniu metod:

Metoda Wynik
lower() zmiana znaków na małe
upper() zmiana znaków na duże
title() pierwsze znaki w wyrazach duże, pozostałe małe
swapcase() odwrócenie wielkości liter

Zanim przejdziemy dalej, dobrze by było wyjaśnić czym jest metoda i czym różni się od funkcji:

Metoda odpowiada funkcji ale jest wywoływana na obiekcie. Czyli np. funkcję wywołujemy ,,samodzielnie’’ np: print("Hello!") a metodę używając nazwy obiektu, np. sekwencja.lower()), gdzie sekwencja to nazwa obiektu (np. zmiennej). Konkretne metody są właściwe dla danego typu obiektu. Np. te, które teraz omawiamy, możemy wywołać na obiektach typu String (str).

sekwencja = "UCAGUUUGGUCC"
print(sekwencja.lower())
ucaguuuggucc

Sprawdźmy czy na pewno sekwencja się nie zmieniła:

print(sekwencja)
UCAGUUUGGUCC

Jak widać łańcuch się nie zmienił.

Wspomniałem wcześniej, że konkretne metody są właściwe dla danego typu obiektu. Obiekty różnych typów mogą mieć wspólne metody, ale zwykle dany typ obiektu posiada jakieś metody właściwe tylko dla tego typu. Więc ich wywołanie na obiektach innego typu się nie powiedzie, albo przyniesie inny rezultat jeśli akurat oba typy obiektów będą miały metody o takiej samej nazwie, ale będzie za nimi stał inny kod.

Sprawdźmy to próbując wywołać metodę właściwą dla obiektów typu str na obiekcie typu int.

liczba = 8
print(f"Typ zmiennej liczba to: {type(liczba)}")
print(liczba.lower())
Typ zmiennej liczba to: <class 'int'>

-------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [3], in <cell line: 3>()
      1 liczba = 8
      2 print(f"Typ zmiennej liczba to: {type(liczba)}")
----> 3 print(liczba.lower())
AttributeError: 'int' object has no attribute 'lower'

Obiekty typu int nie posiadają metody lower(), zatem próba jej wywołania na takim obiekcie generuje informację o błędzie.

Jak widać, warto znać typ obiektu, żeby prawidłowo używać go w tworzonym programie.

Jeśli chcemy zmienić wielkość liter w ciągu przechowywanym przez zmienną, trzeba nowy łańcuch przypisać na nowo do zmiennej:

sekwencja = sekwencja.lower()
print(sekwencja)
ucaguuuggucc

Jeśli będziemy chcieli z powrotem zmienić ciąg na duże litery, użyjemy drugiej z wspomnianych metod:

sekwencja = sekwencja.upper()
print(sekwencja)
UCAGUUUGGUCC

Użyjmy teraz łańcucha z dużymi i małymi literami testując cztery wymienione metody:

lancuch = "Lubię programować w Pythonie"
print(lancuch.lower())
print(lancuch.upper())
print(lancuch.title())
print(lancuch.swapcase())
lubię programować w pythonie
LUBIĘ PROGRAMOWAĆ W PYTHONIE
Lubię Programować W Pythonie
lUBIĘ PROGRAMOWAĆ W pYTHONIE

Obcinanie białych znaków

Białe znaki to takie znaki, które nie mają kształtu na ekranie czy druku. Należą do nich np. spacja, znak tabulacji czy znak końca linii. Czasami istnieje potrzeba, żeby z początku i końca ciągu tekstowego usunąć białe znaki. Jest tak na przykład, gdy chcemy porównać ciągi znaków, na przykład wyszukać wpisaną przez użytkownika krótką sekwencję w dłuższej sekwencji. W taki przypadku, jeśli użytkownik wpisze np. AACTGA (ze spacją na początku) a dłuższa sekwencja wygląda np. tak: GGTTTAACTGACC to ciąg nie zostanie odnaleziony, ponieważ w sekwencji nie ma spacji. Należy zatem zacząć od obcięcia wszelkich białych znaków z przodu i z tyłu ciągu. Możemy tu użyć trzech metod:

  • lstrip() - usuwa białe znaki z początku łańcucha (lewej strony)
  • rstrip() - usuwa białe znaki z końca łańcucha (prawej strony)
  • strip() - usuwa białe znaki z obu końców łańcucha

Sprawdźmy jak to działa, wykrzyknik dodajemy do łańcucha aby pokazać działanie na prawej stronie ciągu:

tekst = "\t Zea mays  "
print(tekst+"!")
print(tekst.lstrip()+"!")
print(tekst.rstrip()+"!")
print(tekst.strip()+"!")
	 Zea mays  !
Zea mays  !
	 Zea mays!
Zea mays!

Szukanie i zamiana w łańcuchach znaków

Python udostępnia kilka pożytecznych metod pozwalających znajdywać fragmenty łańcuchów znaków, a także je modyfikować.

Pierwsza z nich startswith() pozwala sprawdzić czy szukany znak, lub ciąg, występuje na początku badanego łańcucha. W zależności od wyniku wyszukiwania, zwracana jest wartość True lub False:

sekwencja = "UCAGUUUGGUCC"
print(sekwencja.startswith("UCA"))
True

Podobna metoda endswith() sprawdza koniec łańcucha:

sekwencja = "UCAGUUUGGUCC"
print(sekwencja.endswith("UCA"))
False

Zwróć uwagę, że jeśli szukamy pustego łańcucha, zwracana jest wartość True. Należy o tym pamiętać, ponieważ może to prowadzić do mylącego wyniku działania programu.

sekwencja = "UCAGUUUGGUCC"
print(sekwencja.endswith(""))
True

Kolejna metoda count() zwraca liczbę szukanych znaków, lub ciągów znaków:

print(sekwencja.count("U"))
5
print(sekwencja.count("UC"))
2

Liczenie można ograniczyć do fragmentu łańcucha, podając indeks znaku od którego należy liczyć:

print(sekwencja.count("U",5))
3

Można też podać indeks, miejsca do którego należy liczyć, przy czym znak znajdujący się pod indeksem końcowym nie wchodzi w zakres wyszukiwania:

print(sekwencja.count("U",0,6))
3

Czasem nie wystarcza nam sprawdzenie czy dany ciąg znaków występuje, ale chcielibyśmy też poznać jego położenie w łańcuchu.

Metoda find() wyszukuje od początku łańcucha i zwraca indeks pierwszej litery, pierwszego znalezionego ciągu znaków:

print(sekwencja.find("UC"))
0

Podobnie działa metoda rfind(), tyle, że wyszukuje od końca, otrzymujemy zatem położenie ostatniego wystąpienia szukanego ciągu:

print(sekwencja.rfind("UC"))
9

Co się stanie, jeśli szukany łańcuch nie zostanie znaleziony?

print(sekwencja.find("XYZ"))
-1

Szukanie można ograniczyć do części łańcucha. Podając jeden dodatkowy argument, ograniczamy je do fragmentu rozpoczynającego się od wskazanego indeksu:

print(sekwencja.find("UC",2))
9

Można też podać indeks miejsca w łańcuchu w którym szukanie powinno się zakończyć:

print(sekwencja.find("U",1,6))
4

Należy jednak pamiętać, że znak pod indeksem końcowym (podobnie jak przypadku wycinania czy liczenia), nie wchodzi w zakres poszukiwań:

print(sekwencja.find("A",0,2))
-1

Podobnie, można ograniczyć zakres wyszukiwania metody rfind(), jeśli podamy jeden indeks, to szukanie rozpoczyna się od wskazanego miejsca do końca, ale zwracane jest oczywiście ostatnie położenie szukanego łańcucha:

print(sekwencja.rfind("U",0,7))
print(sekwencja.rfind("U",4))
6
9

Kolejna użyteczna metoda replace() pozwala, jak wskazuje jej nazwa, zamienić znaki lub dłuższe fragmenty ciągu znaków:

print(sekwencja.replace("U","T"))
TCAGTTTGGTCC
print(sekwencja.replace("GU","XX"))
UCAXXUUGXXCC

Jeśli podamy dodatkową liczbę jako parametr, będzie ona oznaczała maksymalną liczbę dokonanych zamian:

print(sekwencja.replace("U","T",3))
TCAGTTUGGUCC

Trzeba pamiętać oczywiście, że powyższe polecenie nie zmienia oryginalnego ciągu znaków:

print(sekwencja)
UCAGUUUGGUCC

Funkcja index() działa podobnie jak find() ale jeśli nie znajdzie szukanego ciągu znaków to zwraca błąd:

print(sekwencja.index("Z"))
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-82-679c2407de3c> in <module>
----> 1 print(sekwencja.index("Z"))


ValueError: substring not found

Podsumujmy poznane metody. str oznacza szukany znak lub sekwencję znaków, START - początek zakresu, KONIEC - koniec zakresu, N - liczba zmian, parametry opcjonalne zamknięte są w nawiasach kwadratowych []:

Metoda Działanie
startswith(str) Wyszukiwanie sekwencji znaków na początku łańcucha
endswith(str) Wyszukiwanie sekwencji znaków na końcu łańcucha
count(str [,START [, KONIEC]]) Liczba wystąpień sekwencji znaków
find(str [,START [, KONIEC]]) Wyszukiwanie sekwencji znaków od początku łańcucha. Zwracany jest indeks pierwszego wystąpienia. W przypadku nie znalezienia sekwencji zwracana jest wartość -1.
rfind(str [,START [, KONIEC]]) Wyszukiwanie sekwencji znaków od końca łańcucha. Zwracany jest indeks pierwszego wystąpienia od końca. W przypadku nie znalezienia sekwencji zwracana jest wartość -1.
index(str [,START [, KONIEC]]) Podobnie jak find(), ale w przypadku nie znalezienia sekwencji zwracany jest błąd.
replace(str1, str2 [,N]) Zmienia wystąpienia ciągu str1 na str2 w łańcuchu znaków. Jeśli poda się liczbę (N) to dokona się maksymalnie N zmian, w przypadku braku tego parametru, zmienią się wszystkie znalezione sekwencje znaków.

Python oferuje znacznie więcej możliwości pracy z łańcuchami znaków, część z nich będziemy omawiać w dalszej części kursu.

Zadania

Zadanie 1

Napisz program, który dla danej sekwencji zasad poda: sekwencję, długość sekwencji, liczbę wystąpień każdej z zasad, ich udział procentowy z dokładnością do 1 miejsca po przecinku oraz liczbę i udział % zasad purynowych (A, G) i pirymidynowych (T,C).

Przykładowy wynik działania programu:

Sekwencja: CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGA
Długość sekwencji: 54

Zasady:
A - wystąpień:     13, udział:  24.1%
T - wystąpień:     21, udział:  38.9%
G - wystąpień:      9, udział:  16.7%
C - wystąpień:     11, udział:  20.4%

Rodzaj zasad:
purynowe     -  wystąpień 22, udział  40.7%
pirymidynowe -  wystąpień 32, udział  59.3%

Przykładowe rozwiązanie:

dna = 'CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGA' 
dl_dna = len(dna)
# Liczby wystąpień poszczególnych liter
l_A = dna.count('A')
l_T = dna.count('T')
l_G = dna.count('G')
l_C = dna.count('C')
# Rodzaje zasad
puryny = l_A + l_G
pirymidyny = l_T + l_C

print(f"""
Sekwencja: {dna}
Długość sekwencji: {dl_dna}

Zasady:
A - wystąpień: {l_A:6}, udział: {l_A/dl_dna*100:5.1f}%
T - wystąpień: {l_T:6}, udział: {l_T/dl_dna*100:5.1f}%
G - wystąpień: {l_G:6}, udział: {l_G/dl_dna*100:5.1f}%
C - wystąpień: {l_C:6}, udział: {l_C/dl_dna*100:5.1f}%

Rodzaj zasad:
purynowe     -  wystąpień {puryny}, \
udział {puryny/dl_dna*100:5.1f}%
pirymidynowe -  wystąpień {pirymidyny}, \
udział {pirymidyny/dl_dna*100:5.1f}%
""")

Wynik:

Sekwencja: CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGA
Długość sekwencji: 54

Zasady:
A - wystąpień:     13, udział:  24.1%
T - wystąpień:     21, udział:  38.9%
G - wystąpień:      9, udział:  16.7%
C - wystąpień:     11, udział:  20.4%

Rodzaj zasad:
purynowe     -  wystąpień 22, udział  40.7%
pirymidynowe -  wystąpień 32, udział  59.3%

Zadanie 2

Napisz program, który dla danej sekwencji DNA, np. AGGTACCTAC, poda komplementarną sekwencję RNA (UCCAUGGAUG).

Program powinien wypisać na ekranie sekwencję wejściową (DNA) oraz wyjściową (RNA):

DNA: AGGTACCTAC
RNA: UCCAUGGAUG

Przykładowe rozwiązania:

dna = 'AGGTACCTAC'
# Zmiana liter na małe. Pozwala to uniknąć konfliktu tych 
# samych liter w DNA i RNA
rna = dna.lower()
# Zmieniamy litery oznaczające zasady w DNA na litery
# oznaczające komplementarne zasady w RNA
rna = rna.replace('a','U')
rna = rna.replace('t','A')
rna = rna.replace('c','G')
rna = rna.replace('g','C')
# Wydruk sekwencji wejściowej DNA i komplementarnej RNA
print(f'DNA: {dna}')
print(f'RNA: {rna}')

Wynik:

DNA: AGGTACCTAC
RNA: UCCAUGGAUG

Można też zapisać kod krócej:

dna = 'AGGTACCTAC'
rna = dna.lower().replace('a','U').replace('t','A')\
.replace('c','G').replace('g','C')
print(f'DNA: {dna}')
print(f'RNA: {rna}')

Wynik:

DNA: AGGTACCTAC
RNA: UCCAUGGAUG
Last updated on 28 Sep 2021
Published on 22 Oct 2020