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