Pythone to i owo

Wykład zacząłem od stwierdzenia, że zmienne pojawiają się w każdym języku programowania. W kolejnych rozdziałach opisałem pętle, instrukcje warunkowe i inne pojęcia, które również są obecne w różnych językach programowania. Python ma jednak swoje specyficzne konstrukcje, które nadają specyficzny charakter programom w Pythonie.

*args - zmienna liczba argumentów

Mam nadzieję, że wykorzystujesz już funkcje w swoich programach. To bardzo ważne - funkcje pozwalają na lepsze zorganizowanie kodu. Z czasem coraz większa część Twojego programu będzie “schowana” w funkcjach; sekcja __main__ bedzie je tylko wywoływać. Główną wadą funkcji jest to, że trzeba ją wywołać dokładnie tak, jak została zadeklarowana. Jako przykład niech posłuży funkcja obliczająca długość wektora norm():

Powyższa funkcja oblicza długość wektora (x,y) z przestrzeni dwuwymiarowej, korzystając ze wzoru: \(n = \sqrt(x^2 + y^2)\). Zapewne chcielibyśmy mieć też odpowiednią funkcję dla trzech wymiarów obliczającą \(n = \sqrt(x^2 + y^2 + z^2)\), czyli norm(x,y,z). Napiszmy zatem:

Okazuje się jednak, że druga funkcja nadpisała tą pierwszą! Funkcja dwuargumentowa zniknęła! Po uruchomieniu tego programu zobaczysz komunikat:

Traceback (most recent call last):
  module script_28 line 9
    print(norm(2.3,-1.2))
TypeError: norm() missing 1 positional argument: z

Uwaga

W skrypcie w języku Python może istnieć tylko jedna funkcja o danej nazwie.

Jak obejść to ograniczenie?

  • można mieć dwa oddzielne programy, jeden do obliczeń w dwóch a drugi w trzech wymiarach; w każdym z nich funkcje będą niezależne i będą mogły mieć takie same nazwy

  • stworzyć dwie funkcje o róznych nazwach, np. norm2() oraz norm3()

  • stworzyć własne klasy: oddzielnie dla dwóch i trzech wymiarów; dwie różne klasy mogą mieć bowiem metody o identycznych nazwach

  • stworzyć własne moduły, które w tym kontekście działają jak klasy

  • zastosować funkcje o zmiennej liczbie argumentów

Poniższy przykład ilustruje to ostatnie podejście:

Zauważ, że tym razem lista argumentów funkcji zawiera *args. Tak, ta gwiazdka jest bardzo ważna, bez niej nie będzie to działać. Oznacza ona ‘rozpakowaną listę’: w definicji funkcji pojawia się *args a w środku funkcji zaś dostajesz listę o nazwie args. Dzięki temu funkcja może policzyć długość wektora ze wzoru:

\[n = \sqrt(\sum_{i=1}^{k} x_i^2)\]

gdzie \(k\) to wymiar przestrzeni (liczba współrzędnych wektora). W wywołaniu funkcji jednak nie korzystamy z listy, tylko zwyczajnie wymieniamy argumenty funkcji po przecinku. Listę argumentów zwyczajowo nazywamy args, choć to akurat nie jest obowiązkowe; mozna użyć dowolnej nazwy.

Jak już wspomniałem, w Pythonie nie możemy zadeklarować dwóch różnych funkcji o takiej samej nazwie. Czasem *args jest jedynym możliwym rozwiązaniem. Na poprzednim wykładzie deklarowaliśmy własne klasy. Klasa ta miała konstruktor czyli specjalną metodę o nazwie __init__(). Konstruktor musi nazywać się dokładnie tak; nie możemy stworzyć dwóch różnych konstruktorów, deklarując metody __init1__() oraz __init2__(). Powróćmy zatem do przykładu klasy Vec3 i rozbudujmy konstruktor tej klasy o nowe możliwości. Chcielibyśmy, aby przyjmował on:

  • nic - czyli pustą listę parametrów; w takim przypadku wszystkie współrzędne wektora zostaną zainicjowane zerami

  • jeden argument, np c - w takim przypadku x, y, z = c, c, c

  • trzy argumenty - wartości składowych wektora

Powyższy przykład pokazuje też, że można łączyć argumenty “wymagane”, takie jak self oraz argumenty dodatkowe. Funkcja __init__() wymaga podania co najmniej jednego argumentu, czyli self, wszystkie inne podane wartości zostaną wstawione do listy *args. A co by było w przypadku, gdybyśmy chcieli dodać jeszcze jeden wymagany parametr? Przypuśćmy że każdy wektor musi mieć swój identyfikator (id) - liczbę nadawaną przez użytkownika w trakcie tworzenia wektora. Wtedy wymieniamy dodatkowy argument na liście argumentów funkcji przed *args. Definicja funkcji wygląda teraz nastepująco: def __init__(self, id, *params). Zauważ też, że zamiast *args pojawiło się *params, co jednak nie wpływa na działanie programu - lista przechowująca argumenty wolne może się nazywać dowolnie, liczy się tylko gwiazdka

Owa gwiazdka zwana jest czasem operatorem rozpakowania. Może ona bowiem rozpakować dowolną listę i zamienic je na wartości, na przykład:

Widać, że o ile pierwsza instrukcja print() drukuje listę wartości, to druga już tylko te wartości. Druga instrukcja print() jest więc równoważna trzeciej, czyli print(1,2,3,4,5)

Uwaga

Argumenty dodatkowe *args muszą być wymieniowne w definicji funkcji za argumentami wymaganymi

**kwargs - argumenty nazwane

Język Python oferuje jeszcze jedno udogodnienie przy tworzeniu funkcji: argumenty nazwane. Aby przekazać dodatkowe parametry, piszemy w wywołaniu funkcji nazwa=wartosc. Konstrukcja ta pojawiła się już na pierwszym wykładzie, przy okazji drukowania końca linii instrukcją print(). Właśnie dzięki **kwargs mogliśmy napisać print(zmienna,end="") albo print(zmienna,end=","). Argumenty nazwane bardzo często stosujemy, aby dostarczyć funkcji dodatkowe parametry, które zmieniają jej działanie. Jeżeli użytkownik nie podał **kwargs, funkcja wykorzystuje domyślne wartości parametrów.

Jak wprowadzić **kwargs do swojej funkcji? Jak stworzyć funkcję, która wykorzystuje **kwargs? Należy zadeklarować je jako ostatni element w definicji funkcji:

Na przykładzie tym widzimy, że:

  • wewnątrz funkcji używamy kwargs choć w deklaracji funkcji było **kwargs

  • kwargs jest po prostu słownikiem, który omówiliśmy wcześniej

  • kwargs przypominają nieco wartości domyślne argumentów funkcji: jeżeli użytkownik je podał, do korzystamy z dodatkowych wartości; jeżeli nie, to nic się nie stało

  • niestety użytkownik, który wywołuje funkcję korzystającą z **kwargs musi znać nazwy parametrów; w tym konkretnym przypadku wywołania funkcji: scal_napisy("zupa", "drugie", separator=",") albo scal_napisy("zupa", "drugie", separ=",") nie zadziałają.

Funkcja, która scala tylko dwa napisy jest mało użyteczna. Możemy ją jednak łatwo usprawnić, korzystając z *args:

Uwaga

Argumenty nazwane *kwargs muszą być wymieniowne jako ostatnie w definicji funkcji, a więc za argumentami wymaganymi oraz za argumentami dowolnymi, o ile takie są.

Podsumowanie

Najbardziej ogólna postać definicji funkcji to def superfunkcja(arg1, arg2, *args, **kwargs). Użytkownik musi podać co najmniej dwa argumenty, inaczej będzie błąd. Kolejne argumenty trafią do listy args, argumenty nazwane zaś do słownika kwargs.

Dynamiczne sprawdzanie typu : isinstance

Funkcja w Pythonie nie wie, jakiego typu argumenty dostała. Okazuje się jednak, że Python umożliwia sprawdzenie, jakiego typu jest zmienna. Sprawdzenie to nazwiemy dynamicznym, jego wynik zależy od aktualnej wartości zmiennych. Służy do tego funkcja isinstance(co, typ). Pierwszym jej argumentem jest badana zmienna a drugim nazwa typu, na przykład:

Z przykładu tego jasno wynika, że 9.0 nie jest typu int (linia 5). Instrukcja isinstance() szczególnie przydaje się do odróżniania czy wartość jest naprawdę wartością czy też może listą wartości.

Tak dla przykładu: funkcja scalająca napisy będzie znacznie wygodniejsza w użyciu, gdy można podać jej listę napisów jako argument:

Powyższa funkcja dla każdego argumentu sprawdza, czy jest listą. Jeżeli tak, to ją rozpakowuje i rekurencyjnie wywołuje samą siebie dla rozpakowanych argumentów. Aktualnie funkcja może przyjąć dowolnie wiele napisów a także list zawierających napisy. Najważniejsze jest jednak to, że wciąż działa dla dwóch argumentów, jak działała na początku tego rozdziału.

Wyjątki

Czasami w życiu programu zdarzy się sytuacja wyjątkowa, np. dzielenie przez 0 lub wykroczenie poza zakres tablicy. Sytuacje te skutkują wypisaniem komunikatu na ekran i natychmiatowym zakończeniem programu. Oto prosty przykład:

Czy można taki wyjątek jakoś “przeżyć” i iść dalej? Owszem można, o ile ów potencjalnie kłopotliwy fragment programu zamknięty zostanie w bloku tryexcept:

Dzięki przejęciu wyjątku program biegnie do końca. Straciliśmy jednak informację o błędzie, która poprzednio pojawiała się na ekranie! Przejmując wyjątek musimy sami ją wydrukować, np tak:

W powyższym przykładzie tworzymy zmienną e która jest obiektem klasy Exception, która jest klasą bazową dla wyjątków w Pytonie. Tak więc IOError czy ZeroDivisionError również jest typu Exception i zostałyby przejete w jednym i tym samym bloku except. Niekiedy chcemy rozdzielić obsługę wyjątków dla poszczególnych przypadków. Możemy to zrobić jak poniżej:

Uwaga

Celem wyjątków jest poradzenie sobie z nieprzewidzianym problemem, który spowoduje błąd wykonania programu w czasie jego działania. Wyjątek nie pomoże w przypadku błędu składniowego, np for k range(10) (gdzie brakuje słowa kluczowego in). Wyjątek nie wykryje też błędu w algorytmie, np średnia_z_5 = (1.2 + 1.3 + 0.1 + 2.1 +0.8) / 4.0 (błędnie policzona średnia)