Spis treści Skorowidz Poziom główny Poziom nadrzędny ©
«« Początek »» Koniec

Obiekty, czyli nowe zabawki są lepsze

W niniejszym rozdziale rozszerzymy znajomość typów danych w stosunku do zawartości rozdziału 6., i zintegrujemy ją z umiejętnością tworzenia kodu źródłowego, opisaną w rozdziale 7. Daje to możliwość korzystania ze złożonych struktur danych. Wykorzystanie gotowych obiektów ilustrujemy przykładami. W ramach uzupełnienia opisujemy koncepcję klas, pozwalającą tworzyć własne typy obiektów, i podajemy elementarne przykłady jej wykorzystania.

Obiekty integrują dane z przypisanym ich obsłudze kodem.

W Pythonie wszystko jest obiektem — zobaczymy… (chociaż nie o wszystkim warto myśleć jak o obiekcie).

Python pozwala deklarować własne klasy obiektów (osobny podrozdział 10.5. dla chętnych).

Właściwości obiektów

Pola

Niektóre obiekty reprezentują dane złożone. W takim przypadku ich części składowe są reprezentowane przez pola.

Pola

Przykładem pól są zmienne z.real i z.imag, wchodzące w skład obiektu z typu complex (czyli reprezentacji liczby zespolonej).

Metody

Oprócz pól, stanowiących o wartości złożonej, obiekty są wyposażone w zestaw typowych operacji, zwanych metodami. Są to procedury lub funkcje przypisane do obiektu i operujące na nim. Oto krótka charakterystyka metod:

Zmienna jako odwołanie do obiektu

Stwierdzenie, że zmienna a przechowuje wartość, mimo że w zasadzie poprawne, nie oddaje całej prawdy. Wartość jest w Pythonie obiektem przechowywanym w pamięci, a zmienna jedynie umożliwia dostęp do niego. Ideę tę dobrze ilustruje schemat:

zmienna ------> wartość

Dwie lub więcej zmiennych może reprezentować ten sam obiekt. Mówimy o nich, że są tożsame.

Tożsamość jest warunkiem znacznie silniejszym niż równość. O obiektach równych możemy powiedzieć, że są takie same, czyli że przyjmują takie same wartości. Obiekty tożsame są istocie tym samym obiektem.

Dwie zmienne reprezentują dwa różne obiekty
a ------> wartość
b ------> wartość

Dwie zmienne reprezentują ten sam obiekt
a ------> wartość <------ b

Do sprawdzania tożsamości obiektów służy operator is. Istnieje też operator is not będący jego zaprzeczeniem.

is jest słowem kluczowym Pythona.

a == b 	# sprawdza równość obiektów (czy zmienne mają te same wartości)
a is b	# sprawdza identyczność obiektów (czy zmienne są tym samym obiektem w pamięci)
a is not b	# sprawdzenie, czy dwie wartości reprezentują dwa różne obiekty
# uwaga: a is not b nie jest tym samym, co a is (not b)

Poniższe przykłady ilustrują subtelną, lecz istotną różnicę między równością wartości a tożsamością obiektów:

1 == 1.0
True
1 is 1.0
False

a = 1.0
b = a	# po operacji przypisania zmienne są tożsame (bo odnoszą się do tego samego obiektu)
a == b
True
a is b
True

a = 1.0
b = 1.0
a == b
True
a is b
False

a = 1.0
b = 0.1*10
a == b
True
a is b
False

a = 'Ala'
b = 'As'
c = a + ' i ' + b
c == 'Ala i As'
True
c is 'Ala i As'
False

W przypadku obiektów złożonych sprawa tożsamości nabiera pierwszorzędnej wagi:

a = [1,2,3]
b = a
a == b
True
a is b
True
c = [1,2,3]
a == c
True
a is c
False
a[1] = 99
a is b
True
b
[1, 99, 3]
b = c
a is c
False
b is c
True

Operacja przypisania nadaje tożsamość zmiennej (choć może jej nie zmienić, np. a = a).

rysunek

Operacje

a = wartość
b = wartość

sprawią, że zmienne a i b będą miały wartości wyliczone identycznym sposobem, ale niekoniecznie będą wskazywać ten sam obiekt. Zależy to od sposobu określenia wartości: czy będzie się on wiązał z utworzeniem nowego obiektu.

a = 2
b = 2
c = 1+1
a is b       # w pamięci Pythona istnieje tylko jeden „egzemplarz” danej wartości typu int
True
a is c
True
a = 1.0
b = 1.0
a is b       # natomiast poszczególne wartości typu float mogą mieć więcej „wcieleń”
False
rysunek

Zmienne a i b po wykonaniu operacji

a = wartość
b = a

na pewno będą odwoływały się do tego samego obiektu. Ten sam skutek uzyskamy stosując skrótową notację podstawienia

b = a = wartość
rysunek

Taki „łańcuch przypisań” może mieć dowolną długość.

W liście utworzonej techniką zwielokrotnienia, czyli przez „pomnożenie” listy przez liczbę całkowitą, wszystkie elementy są ze sobą tożsame. W przypadku, kiedy otrzymano tą techniką listę list, należy być bardzo ostrożnym podczas jej modyfikacji:

dane = [[0,0]]*5
dane
[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]
dane[0] is dane[1]
True
dane[0][0] = 1.0	# uwaga!
[[1.0, 0], [1.0, 0], [1.0, 0], [1.0, 0], [1.0, 0]]

Napisy jako obiekty

Tak, napis jest obiektem

dir(str) daje spis metod dostępnych dla napisów. Dla nas liczą się tylko te, których nazwa nie rozpoczyna się od znaku podkreślenia:

dir(str)
[…
'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 
'find', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 
'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 
'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 
'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

Niektóre są całkiem użyteczne, np. encode(), split() albo replace() .

Kodowanie

Konwersja napis → tablica

Konstrukcja tablica = napis.split(separator) rozbija napis na tablicę napisów. Jest ona bardzo wygodna np. podczas czytania danych liczbowych.

wierszdanych = '1\t12.22\t-10.55\n'
wektor = wierszdanych.split()
wektor
['1', '12.22', '-10.55']
wektor = [int(wektor[0]), float(wektor[1]), float(wektor[2])]
wektor
[1, 12.22, -10.55]

Jak widać, elementy tablicy otrzymanej za pomocą split() nadal pozostają tekstami.

Domyślnie zestaw separatorów obejmuje ' \t\n', czyli tzw. białe znaki. W domyślnym trybie pomijane są puste ciągi danych (tj. sąsiadujące separatory są traktowane jak pojedynczy separator).

'1\t\t\t12.22\t-10.55'.split()
['1', '12.22', '-10.55']

Można też wymusić separowanie za pomocą dowolnego innego znaku.

wierszdanych = '1,12.22,-10.55'
wektor = wierszdanych.split(',')
wektor
['1', '12.22', '-10.55']
wektor = [int(wektor[0]), float(wektor[1]), float(wektor[2])]
wektor
[1, 12.22, -10.55]

W tym przypadku każde wystąpienie separatora jest traktowane osobno, zatem pustych ciągów nie pomija się.

'1,,,12.22,-10.55'.split(',')
['1', '', '', '12.22', '-10.55']

Separatorem może także być ciąg znaków.

dane = '1trututu2trututu3\n\n4trututu5trututu6\n'
dane
'1trututu2trututu3\n\n4trututu5trututu6\n'
dane.split()
['1trututu2trututu3', '4trututu5trututu6']
dane.split('\n')
['1trututu2trututu3, '', ,4trututu5trututu6', '']
dane.split('trututu')
['1', '2', '3\n\n', '5', '6\n']

Operacja odwrotna do split() polegałaby na połączeniu tekstów z danej listy w jeden tekst, przy czym w miejscach „sklejenia” znajdowałaby się jakaś fraza separująca. Odpowiednia metoda jest przypisana, co jest nieco nieoczekiwane, do typu napisowego, a nie listowego, i nosi nazwę join. Napis będący jej „właścicielem” będzie pełnił rolę separatora w połączonym tekście.

By nie rozwodzić się nad tym zbyt długo, zakończymy typowym przykładem konwersji tekstu na listę

tekst = 'Adam\tKowalski\t2008-12-22\tWrocław'
tekst.split('\t')
['Adam', 'Kowalski', '2008-12-22', 'Wrocław']

oraz odpowiadającej jej konwersji w stronę przeciwną

rekord = ['Adam', 'Kowalski', '2008-12-22', 'Wrocław']
separator = '\t'
separator.join(rekord)
'Adam\tKowalski\t2008-12-22\tWrocław'

Zamiana fraz w napisie

Metoda napis.replace(fraza1, fraza2) tworzy nową daną typu napisowego, jaką otrzymamy z wartości napis przez zastąpienie w niej wszystkich wystąpień tekstu fraza1 tekstem fraza2.

zdanie = 'Ala ma Asa.'
zmienione = zdanie.replace('A', 'O')
zmienione
'Ola ma Osa.'
zdanie.replace('A', 'Be')
'Bela ma Besa.'

Wynik nie modyfikuje oryginalnego napisu — wiemy już, że dane tekstowe w Pythonie są niemodyfikowalne. Jednak przypisanie

zdanie = zdanie.replace('A', 'O')

jest jak najbardziej legalne; spowoduje ono „zbudowanie” nowej wartości typu str, zapamiętanie jej w zmiennej zdanie, a w konsekwencji — „zapomnienie” przez tę zmienną jej pierwotnej zawartości (co oznacza też zerwanie tożsamości z innymi zmiennymi reprezentującymi tę zawartość, i ile takie istniały).

Zamiana fraz dopasowanych do wyrażeń regularnych jest także możliwa. Dane typu napisowego nie mają odpowiedniej metody; celowi temu służy funkcja re.sub(wzorzec, zast, napis) z modułu re.

Listy i tablice jako obiekty

Tak, lista jest obiektem. append, insert i sort są metodami.

dir(list) daje spis metod dostępnych dla list. Nie ma ich wiele, ale są użyteczne (dla nas „liczą się” tylko metody o nazwach nie rozpoczynających się znakiem podkreślenia):

dir(list)
[…
 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

Dodajmy, że metody zmieniające stan listy zachowują jej tożsamość jako obiektu. Tak więc operacje wykonywane za ich pomocą

a = [1, 2, 3]
b = a            # a i b wskazują na ten sam obiekt
a.append(4)      # a zachowuje dotychczasową tożsamość; operacje wykonane na a „widać” także na b
a
[1, 2, 3, 4]
b
[1, 2, 3, 4]

nie są równoważne podobnym operacjom używającym działań

a = [1, 2, 3]
b = a            # a i b wskazują na ten sam obiekt
a = a + [4]      # a dostaje nową tożsamość; od tej chwili a jest czym innym niż b
a
[1, 2, 3, 4]
b
[1, 2, 3]

mimo że końcowa zawartość listy a jest w obu przykładach identyczna. Wypada dodać, że w przypadku użycia operatora += w następujący sposób: a += [4] tożsamość listy a zostanie zachowana.

Pliki

Pliki służą przede wszystkim do trwałego przechowywania informacji w pamięci masowej. Zarządzanie plikami leży w gestii systemu plików właściwego dla danego urządzenia i odbywa się za pośrednictwem systemu operacyjnego. Plik jest identyfikowany na podstawie nazwy, ścieżki dostępu lub adresu sieciowego (URI), będących danymi typu tekstowego.

Z istniejącego pliku możliwe jest pobieranie informacji w drodze operacji zwanej odczytywaniem. W istniejącym lub nowo tworzonym pliku można umieszczać informację w drodze operacji zwanej zapisywaniem.

Do operowania na zawartości plików w Pythonie służą obiekty specjalnego typu plikowego (file). Obiekt plikowy jest wiązany z konkretnym plikiem systemowym lub sieciowym, wskazanym przez nazwę lub ścieżkę dostępu, podczas operacji otwarcia (open).

Odczyt lub zapis zawartości pliku mogą być wykonywane w czasie, kiedy obiekt plikowy jest otwarty. Z chwilą zamknięcia (close) obiektu plikowego tracimy tę możliwość.

Pamiętajmy, że znak '\' ma specjalne znaczenie w napisach. W nazwach plików bezpieczniej jest używać ukośników '/' niż '\' (np. '/home/Ja/dane/proba1.txt' lub 'c:/Users/Ja/dane/proba1.txt', 'ftp://mat.up.wroc.pl/pub/dane/proba1.txt'). Będą one poprawnie rozumiane we wszystkich systemach operacyjnych.

Można też pisać '\\' zamiast '\' (np. 'c:\\Users\\Ja\\dane\\proba1.txt'), ale tylko w systemach używających znaku '\' jako separatora w ścieżkach dostępu — czyli w systemach Windows.

Użycie / jest zalecane, jeżeli program ma być przenośny między systemami operacyjnymi (MacOS X, *nix, Windows, inne). Użycie \\ jest zalecane tylko w sytuacji, kiedy pracujemy w Windows, ale pliki otwieramy za pośrednictwem zewnętrznej aplikacji nie akceptującej / (np. przez Component Object Model, patrz podrozdział 11.4.).

Otwieranie pliku

plik = open(nazwa, tryb)

plik
nazwa tworzonego obiektu reprezentującego plik i operacje na nim przeprowadzane
nazwa
nazwa / ścieżka dostępu do pliku, na którym będziemy operować
tryb
  • 'r' (‘read’, ‘czytaj’)
  • 'w' (‘write’, ‘nadpisz’)
  • 'a' (‘append’, ‘dopisuj’)
  • tryby rozszerzone ('+') pozwalają na jednoczesny odczyt i zapis; nie omawiamy
  • tryb binarny ('b') pozwala na odczytywanie/zapisywanie bloków bajtów; nie omawiamy

Python 3 przewiduje dodatkowe argumenty funkcji open(), pozwalające m.in. zadeklarować system kodowania znaków obowiązujący w otwieranym pliku.

Zamykanie pliku

Do zamknięcia otwartego pliku służy operacja plik.close(). Po jej wykonaniu program traci dostęp do zawartości pliku.

Zamknięcie pliku jest niezbędne dla pomyślnego ukończenia operacji systemowych, zwłaszcza związanych z zapisywaniem danych.

Odczyt z pliku

Odczyt jest możliwy tylko z pliku uprzednio otwartego w trybie do odczytu (czyli 'r').

Plik przypomina taśmę z zapisanym ciągiem znaków. Zawartość pliku jest odczytywana znakami lub wierszami po kolei. Wygodnie jest wyobrażać sobie głowicę czytającą ten ciąg. Bezpośrednio po otwarciu głowica stoi na początku taśmy, i w miarę kolejnych instrukcji dotyczących odczytu przesuwa się wzdłuż zawartości pliku. Głowica nie może się cofnąć, nie może też przeskoczyć w przód inaczej, niż czytając zawartość.

tekst = plik.read()
całą zawartość pliku umieszcza w jednej zmiennej napisowej, przesuwa głowicę za ostatni znak. Na ogół odczytana zawartość będzie dłuuuga i stosunkowo trudna do analizy…
tekst = plik.readline()
umieszcza zawartość pojedynczego wiersza, wraz z kończącym go znakiem końca wiersza, w zmiennej napisowej i przesuwa głowicę na początek następnego wiersza. Nadaje się do pętli i do tworzenia listy wierszy
tablica = plik.readlines()
umieszcza zawartość pliku wiersz po wierszu w tablicy napisów; przesuwa głowicę po odczycie każdego wiersza

Przy próbie czytania z pliku, którego głowica umieszczona jest już przy jego końcu, otrzymamy pusty napis, czyli ''. Zauważmy, że w każdym innym przypadku odczytany tekst nie będzie pusty. Np. czytanie za pomocą readline() daje zawsze zawartość kolejnego wiersza pliku wraz z kończącym go znakiem końca wiersza. Zatem odczytując pusty wiersz dostaniemy '\n'.

W wyniku czytania z pliku otrzymuje się wyłącznie dane napisowe (str). Konwersję do innych typów danych trzeba wymusić, np. w przypadku kiedy pojedynczy wiersz pliku zawiera jedną liczbę całkowitą: wartość = int(plik.readline()).

Jeżeli wiersz pliku zawiera kilka danych, to trzeba je wyłuskać z odczytanego tekstu, np.:

wiersz = plik.readline()
wiersz = wiersz.split()
wektor = [float(wiersz[0]), float(wiersz[1]), float(wiersz[2])]

przy czym

Zapis do pliku

Zapisywać da się tylko do pliku otwartego do zapisu (w trybie 'w' lub 'a'). Tryb 'w' usuwa poprzednią zawartość pliku. Tryb 'a' wymusza dopisywanie do końca istniejącego pliku.

plik.write(tekst)
dopisuje tekst do końca pliku. Jeżeli chcemy dopisać coś, co nie jest tekstem, trzeba to wcześniej skonwertować za pomocą str(). write() nie dopisuje nic „od siebie”, nawet separatora.
plik.writelines(tablica)
dopisuje do pliku listę napisów pobraną z tablicy, nie rozdzielając ich żadnym separatorem. Jeżeli chcemy je umieścić w osobnych wierszach, to powinny kończyć się znakiem \n
plik.writeline(tekst)
chciałoby się, ale nic takiego nie ma… Jeżeli chcemy zapisać do pliku pełny wiersz, to dopisywaną treść trzeba po prostu zakończyć symbolem \n

Zapisywanie do pliku dotyczy tylko wartości napisowych. Zapis wartości innych typów trzeba poprzedzić konwersją, np. plik.write(str(wartość_liczbowa)).

W Pythonie 2 w celu zapisania wartości wyrażenia do pliku można było wykorzystać instrukcję print w postaci

print >>plik wyrażenie

(Notacja ta przypomina nieco przekierowanie strumienia danych w powłoce systemowej). Także i w tym przypadku plik musi być obiektem plikowym otwartym do zapisu.

Praktyczne aspekty wczytywania zawartości plików

Jak odczytać zawartość pliku?

Odpowiedź zależy od tego, jakiego rodzaju dane plik zawiera, i co zamierzamy z nimi zrobić. Ograniczymy się tylko do plików znakowych, zwracając większą uwagę na zbiory danych o charakterze tabelarycznym, niż na dokumenty.

Najprostsza sytuacja dotyczy programów typu filtr, które odczytują plik danych, i w miarę jego odczytywania budują i wyprowadzają informację wynikową. Nie ma wtedy żadnej potrzeby, by w całości przechowywać zawartość pliku w pamięci; wystarczy, że będziemy ją analizować wiersz po wierszu. Dlatego wszystko odbywa się w cyklu: przeczytaj, przetwórz, wyrzuć, powtarzanym tak długo, aż osiągnięty zostanie koniec pliku. Pomocne będą przy tym następujące schematy postępowania:

Przeciwna sytuacja ma miejsce, kiedy musimy mieć jednoczesny dostęp do wszystkich danych, zaś podział pliku na wiersze nie jest istotny. Jest to np. przypadek, kiedy plik zawiera niesformatowany tekst o ustalonej szerokości wierszy.

W naszym przypadku najbardziej typowa jest sytuacja, kiedy plik zawiera tabelę danych, przy czym znaki końca wiersza rozdzielają poszczególne rekordy. Taki plik, jeżeli zadanie na to pozwala, warto czytać „na raty”, zgodnie z pierwszą wskazaną metodą. Jeżeli jednak musimy wczytać go w całości, zapewne zależy nam na zbudowaniu tablicy obiektów odpowiadających zawartości wierszy pliku. Możemy to zrobić kilkoma sposobami:

Następnym etapem jest odzwierciedlenie zawartości poszczególnych wierszy pliku w strukturze danych. Omawiamy przypadek, w którym plik zawiera typową tabelę składającą się z jednakowo zbudowanych wierszy, poprzedzonych ewentualnie jedno- lub kilkuwierszowym nagłówkiem. W tym celu musimy przetworzyć każdy wiersz po kolei, zamieniając go z typu tekstowego na rekord (obrazowany np. przez listę).

Przy podziale wiersza na pola pomoże nam funkcja split(). Postać separatora, będąca argumentem funkcji split, winna być dopasowana do formatu odczytywanych danych. W otrzymanej liście warto od razu wymusić typ pól zgodny z sensem danych. Wyglądać to może mniej więcej tak:

plik = open(nazwa, 'r')
# znamy format pliku i wiemy, że nagłówek zajmuje dwa pierwsze wiersze...
naglowek = plik.readline() + plik.readline()
# ... a reszta aż do końca to dane tabelaryczne
dane = []
for wiersz in plik.readlines():
    # zamieniamy tekst na listę; jeżeli trzeba, przekazujemy do split() postać separatora
    wiersz = wiersz.split()
    # modyfikujemy dane z bieżącego wiersza stosownie do naszych potrzeb
    wiersz = ...
    # po czym dołączamy rekord do tabeli
    dane.append(wiersz)
# kiedy skończymy, mamy gotową tabelę
plik.close()

Przykłady

[]

różne techniki czytania i pisania

[]

czytanie i przetwarzanie wiersz po wierszu, bez zapamiętywania całości

[]

wczytanie listy wierszy

[]

wczytanie całego pliku, potem podział na wiersze

Więcej o nazwach plików

Jak wiadomo, plik jest lokalizowany w systemie plików na podstawie ścieżki dostępu, a w ramach pojedynczej kartoteki — za pomocą nazwy. Odwołanie się do pliku na podstawie samej tylko nazwy nie gwarantuje powodzenia, gdyż domyślna ścieżka dostępu (kartoteka bieżąca, current working directory) przydzielona procesowi przez system zależy od okoliczności uruchomienia programu.

Opanowanie reguł odwoływania się do plików za pomocą nazw, ścieżek względnych i ścieżek bezwzględnych wymaga zrozumienia podstawowych reguł uruchamiania procesów pod kontrolą systemu operacyjnego, nie ma natomiast głębszego związku z językiem programowania.

Podstawowe informacje o ścieżkach dostępu do plików i o kartotekach bieżących znajdziesz np. w podrozdziale 1.3. pierwszej części opracowania.

Posługiwanie się nazwą pliku albo ścieżką względną

W przypadku używania samych nazw bez ścieżek, pliki będą poszukiwane, odczytywane i zapisywane w kartotece bieżącej, a w przypadku ścieżki względnej: w kartotece o położeniu ustalonym względem kartoteki bieżącej.

Początkowa wartość kartoteki bieżącej zależy od okoliczności uruchomienia programu, i w trakcie jego wykonywania może być zmieniana.

Posługiwanie się ścieżką bezwzględną dostępu do pliku

W tym przypadku jednoznacznie identyfikujemy plik w całym systemie.

Przenośność danych.

Nośniki wymienne.

Zarządzanie kartoteką bieżącą

Funkcja getcwd z modułu os zwraca pełną ścieżkę dostępu do kartoteki bieżącej.

import os
os.getcwd()
'/home/jan/cwiczenia/2011/testy'

Funkcja chdir z modułu os powoduje zmianę kartoteki bieżącej działającego procesu.

import os
os.getcwd()
'/home/jan/cwiczenia/2011/testy'
os.chdir('/home/jan/dane')
os.getcwd()
'/home/jan/dane'

Funkcja expanduser z modułu os.path zastępuje znajdujące się w tekście frazy ~ oraz ~user ścieżkami dostępu do kartoteki osobistej, odpowiednio bieżącego i wskazanego użytkownika.

import os
os.getcwd()
'/home/jan/cwiczenia/2011/testy'
os.path.expanduser('~')
'/home/jan'
os.path.expanduser('~adam')  # ten użytkownik ma kartotekę osobistą w innym miejscu
'/student/adam'
os.path.expanduser('~ktosiu')  # użytkownika o tej nazwie nie ma w systemie
'~ktosiu'
os.chdir(os.path.expanduser('~/dane'))
os.getcwd()
'/home/jan/dane'

Odczytywanie zawartości kartotek

Funkcja listdir z modułu os generuje spis zawartości wybranej kartoteki. Ma on postać listy nazw plików. W celu uzyskania ścieżek wystarczy poprzedzić je ścieżką do kartoteki, nie zapominając o separatorze.

import os
os.listdir('/home/jan/cwiczenia/2011/dane')
['temp_sty2010.txt', 'wilg_sty2010.txt', 'temp_lut2010.txt', 'wilg_lut2010.txt', 'stacje.zip']

Funkcja glob z modułu glob generuje listę ścieżek dostępu do plików o nazwach zgodnych z podanym wzorcem.

import glob
glob.glob('/home/jan/cwiczenia/2011/dane')
['/home/jan/cwiczenia/2011/dane']
glob.glob('/home/jan/cwiczenia/2011/dane/temp*')
['/home/jan/cwiczenia/2011/dane/temp_sty2010.txt', '/home/jan/cwiczenia/2011/dane/temp_lut2010.txt']

Pliki z sieci

Funkcja open() umożliwia dostęp do plików znajdujących się w lokalnym systemie plików. Pliki umieszczone na zdalnych systemach, dostępne za pomocą protokołów sieciowych, takich jak ftp czy http, otwiera się nieco inaczej. Zamiast funkcji open(nazwa, tryb), użyjemy w tym celu funkcji urlopen(adres) z biblioteki urllib. Jest ona częścią standardowej instalacji Pythona. Zamiast nazwy pliku należy podać wtedy jego adres URI. Na przykład instrukcja

plik = urllib.urlopen('ftp://mat.up.wroc.pl/pub/dane/dane.txt')

pozwala odczytać dane bezpośrednio z pliku udostępnionego na serwerze, bez potrzeby uprzedniego kopiowania go do lokalnego systemu plików.

Użytkownik pragnący korzystać z programu posługującego się tą metodą powinien mieć uprawnienia pozwalające na dostęp do takiego pliku. Pozostałe szczegóły obsługi tak otwartego pliku pozostają bez zmian.

Za pomocą biblioteki urllib możemy również otwierać, choć jedynie do odczytu, zwykłe pliki z systemu plików naszego komputera. W tym celu należy użyć bezwzględnej ścieżki dostępu w postaci adresu URL z przedrostkiem file://, np.

# w systemach uniksowych
plik = urllib.urlopen('file:///home/adam/dane/dane.txt')
# w systemach Windows
plik = urllib.urlopen('file:///c:/Users/adam/dane/dane.txt')

Pliki skompresowane

Nowe typy danych, czyli wymyślamy nowe zabawki

W skład obiektu wchodzą pola będące zmiennymi przechowującymi istotne parametry obiektu, oraz metody będące funkcjami odwzorowującymi działania przypisane do obiektu. Zarówno pola, jak metody noszą nazwy.

Klasy

Klasy służą do tworzenia nowych typów obiektów. Można powiedzieć, że klasa jest szablonem pozwalającym tworzyć obiekty.

Deklaracja klasy odbywa się za pomocą dyrektywy class.

We współczesnym Pythonie podstawowa forma definicja klasy ma postać

class nazwaklasy(object):
    definicja_klasy

Gdzieniegdzie spotyka się przestarzałe deklaracje postaci

class nazwaklasy:
    definicja_klasy

których nie należy stosować w nowo tworzonym kodzie.

class jest słowem kluczowym Pythona.

Użytkownik deklaruje własne klasy, by na ich podstawie tworzyć konkretne obiekty dopasowane do własnych potrzeb. Obiekt taki tworzy się używając klasy w następujący sposób:

obiekt = nazwaklasy(argumenty)

przy czym argumenty zależą od szczegółów zadeklarowania danej klasy, które zostaną skrótowo wyjaśnione poniżej. Wszystkie obiekty zbudowane za pomocą jednej klasy są w pewnym sensie „podobne” do siebie.

Wszystkie zmienne zadeklarowane wewnątrz klasy stają są właściwościami (polami) wspólnymi dla wszystkich obiektów powoływanych za pomocą tej klasy. Przy dostępie do właściwości obiektów posługujemy się nazwami postaci obiekt.właściwość.

Funkcje zadeklarowane wewnątrz klasy stają się metodami obiektu. Przy wywoływaniu metody stosowana jest podobna konwencja obiekt.metoda(argumenty_wywołania).

Istnieje też mechanizm pozwalający na zróżnicowanie poszczególnych obiektów generowanych na podstawie samej klasy.

Na przykład wszystkie obiekty reprezentujące prostokąty, generowane na podstawie klasy prostokat, miałyby swoje indywidualne właściwości szer i wys, reprezentujące ich wymiary. Jest to konieczne, gdyż od różnych prostokątów oczekujemy, by mogły różnić się wymiarami. Winny one natomiast posiadać wspólną metodę pole(), która mnożyłaby te wymiary w celu obliczenia powierzchni figury. Jest jasne, że metoda musi pobierać wymiary od tego prostokąta, do którego sama należy.

W związku z tą potrzebą, przyjęto w Pythonie następującą konwencję dotyczącą argumentów metod. Pierwszy argument w każdej deklaracji metody odpowiada obiektowi będącemu jej właścicielem. Tradycyjnie dla tego argumentu używana jest nazwa self.

Jeżeli więc wewnątrz klasy prostokat zdefiniowano metodę o nagłówku pole(self), to argument self będzie reprezentował prostokąt, którego pole zamierzmy obliczyć. W tym momencie nie ma różnicy między metodą a zwykłą funkcją. Różnica pojawi się w chwili wywołania; w przypadku metody będzie ono miało postać

a = prostokat()
a.szer = 2.0
a.wys = 3.0
...
a.pole()

gdzie a jest konkretnym obiektem typu prostokat (obiekt-właściciel „wchodzi” jaki pierwszy argument wywołania należącej do niego metody). Sama metoda pole() w przypadku prostokąta może być zorganizowana następująco:

class prostokat(object):
    szer = 0.0
    wys = 0.0
    def pole(self):
	return self.szer * self.wys

Wyjaśnijmy: w celu znalezienia pola danego prostokąta (czyli tego, z którego została wywołana), metoda oblicza iloczyn wymiarów tego właśnie prostokąta.

Jeżeli dany obiekt typu prostokat miał nadane właściwości szer oraz szer, to właśnie one będą użyte przy obliczeniu pola. Jeżeli takich wartości nie ustalono, to będą użyte wartości domyślne, opisane wewnątrz deklaracji klasy (czyli: nowo utworzony za pomocą naszej klasy prostokąt ma pole 0).

Niektóre nazwy funkcji deklarowanych jako metody wewnątrz klasy mają szczególne znaczenie. Najważniejszą wśród nich jest funkcja o nazwie __init__(). Jest ona wywoływana w chwili tworzenia nowego obiektu za pomocą przypisania postaci

obiekt = klasa(argumenty)

Argumenty wywołania klasy są przekazywane funkcji __init__(). Lista argumentów zaprojektowana dla tej funkcji może być dowolna, jednak — tak samo jak w przypadku innych metod — pierwszy z nich odpowiada tworzonemu obiektowi (tradycyjnie nosi on nazwę self). Wynika stąd, że liczba argumentów wywołania klasy winna być o jeden mniejsza od liczby argumentów jej funkcji __init__().

Na przykład w rozpatrywanym wyżej przypadku klasy prostokat, tworzone za jej pomocą obiekty powinny otrzymywać wymiary szerokości i wysokości. Szkic takiej klasy mógłby wyglądać następująco:

class prostokat(object):
    def pole(self):
	return self.szer*self.wys

    def __init__(self, a, b):
	self.szer = a
	self.wys = b

Pierwszy argument zadeklarowany w nagłówku funkcji __init__(self, a, b) odnosi się do generowanego obiektu. Wartości pozostałych dwóch argumentów zostaną przejęte z wywołania klasy, i przypisane do obiektu jako jego właściwości szer oraz wys. Oto, jak można użyć powyższej klasy do utworzenia trzech różnych obiektów typu prostokat i sprawdzenia ich pola powierzchni:

a = prostokat(1.0, 2.5)
b = prostokat(2.0, 4.0)
b = prostokat(1.5, 3.0)
print(a.pole(), b.pole(), c.pole())

Funkcja __init__() jest właściwym narzędziem w każdym przypadku, w którym tworzone obiekty winny różnić się wartościami atrybutów.

Nie będziemy wgłębiać się w dalsze szczegóły deklaracji klas i obiektów. Poniżej zamieszczamy kilka elementarnych przykładów, które poglądowo zilustrują ideę klas i obiektów.

Przykłady

[]

Deklaracja klasy reprezentującej punkty płaszczyzny

[]

Deklaracja klasy reprezentującej prostokąty o kanonicznej orientacji

[]

Demonstracja użycia zdefiniowanych wyżej klas

Klasy zadeklarowane tym sposobem są obiektami typu type (czyli de facto typami danych), zaś typami tworzonych za ich pomocą obiektów są te właśnie klasy.

Pytania kontrolne

  1. Kiedy dwie zmienne wskazują ten sam obiekt; jak z tego korzystać?
  2. Podaj sensowne przykłady zastosowania metod obiektów typu list
  3. Jak odczytywać dane z plików tekstowych?
  4. Jak tworzyć pliki tekstowe z wynikami?
  5. Kiedy warto tworzyć własne typy danych?
  6. Co to są klasy i do czego służą?
© Copyright 2000–2012 by Jan Jełowicki, Katedra Matematyki Uniwersytetu Przyrodniczego we Wrocławiu
Ostatnia modyfikacja w grudniu 2012 r.
janj@aqua.up.wroc.pl
http://karnet.up.wroc.pl/~jasj