fbpx

Cześć!

Siódma część kursu Python 3 by MR to niejako kontynuacja poprzedniego wpisu. Ostatnim razem opowiadałem o wyrażeniach i podstawach funkcji w Python. Dziś pochylę się bardziej szczegółowo nad bardziej zaawansowanymi aspektami w funkcjach. 

W tej części dowiesz się między innymi o tym:

  • Czym jest obiekt i w jaki sposób przekazywać obiekty do funkcji?
  • Czym jest enum?
  • Jak przekazać funkcję do innej funkcji jako argument?
  • Czym są zasięgi globalne i lokalne funkcji?
  • Co to jest argument kluczowy, pozycyjny oraz wielowartościowy?
  • Czym jest kopiowanie płytkie i głębokie?

Dawno nie było tak dużo materiału. Zabierajmy się do kodowania!

Zatrzymaj się!


Książka „Programistą być” to obowiązkowa pozycja dla każdego zainteresowanego programowaniem!

Jest to zdecydowanie najlepsza na polskim rynku książka na temat programowania! Zyskasz przewagę w branży IT i osiągniesz dużo jako deweloper.

Z książki dowiesz się między innymi o tym:

  • Czy matematykastudia techniczne i język angielski są konieczne do tego, by rozpocząć pracę jako programista?
  • Gdzie szukać informacji o programowaniu i w jaki sposób się uczyć?
  • Jak znaleźć pierwszą pracę i w jaki sposób rozwijać swój programistyczny potencjał?
  • Czym na co dzień zajmuje się programista?
  • Czy każdy może zostać programistą?

I wiele, wiele więcej…


Zaawansowane aspekty funkcji — obiekty w funkcji

Aby w ogóle mówić o obiektach w kontekście funkcji, musimy dowiedzieć się, czym taki obiekt w Python jest. Wyobraź sobie zmienną, niech to będzie zmienna x = 'Mateusz’. Właśnie ta zmienna, którą utworzymy, będzie obiektem w kodzie. Na takim obiekcie możemy robić dużo więcej, niż nam się wydaje na pierwszy rzut oka. 

Potraktuj obiekt jak coś fizycznego, Wyobraź sobie, że masz obiekt, który będzie samochodem. Możesz w nim otworzyć drzwi, włączyć silnik, ruszyć do przodu używając metod, które wykonają jakąś akcję jak na przykład samochod.otworzDrzwi(), samochod.wlaczSilnik() i tak dalej.

Dodatkowo możesz wyciągać informacje z obiektu. Przecież ma kolor, wielkość, markę, model i wszystkie te powyższe informacje możesz z obiektu wyciągnąć, używając konstrukcji marka(samochod), kolor(samochod) i tak dalej. Widzisz zależność?

Aby coś zrobić na obiekcie, wywołujesz metodę za pomocą kropki. Jeśli chcesz wyciągać jakieś informacje o obiekcie, podajesz obiekt do metody w nawiasie.

Teraz na naszym obiekcie x mogę wywołać szereg funkcji, które mogą bardzo wiele zrobić z wartością w obiekcie. Dla przykładu użyłem funkcji lower oraz upper, które zamieniają odpowiednio wszystkie litery na małe oraz na duże litery. Użyłem też funkcji len oraz type, gdzie przekazałem do nich obiekt x. Funkcja zwróciła informacje o długości zmiennej oraz jej typie.

Zobacz poniżej kod oraz wynik na konsoli. 

Kod pokazujący dlaczego zmienna to obiekt

immutable vs mutable

Przy okazji obiektów muszę Ci wspomnieć o czymś takim, co nazywa się immutable oraz mutable. W żargonie programistycznym często można usłyszeć, że obiekt jest mutowalny lub nie. Pojawiają się takie pytania na rozmowach o pracę, więc koniecznie musisz to wiedzieć.

Tak więc najprostsza definicja brzmi: 

Obiekt, którego stan wewnętrzny można zmienić, jest mutable. Z drugiej strony, immutable nie pozwala na żadne zmiany w obiekcie po jego utworzeniu. Jest to mało logiczne, bo przecież w programowaniu normalnie można zmienić wartość. Ale czy aby na pewno?

Poznajmy jeszcze typy obiektów mutowalnych i niemutowalnych w Python. 

Typy immutable to:

  • str;
  • int;
  • bool;
  • float;
  • range;
  • bytes;
  • complex;
  • frozenset;
  • tuple;

Typy mutable to:

  • obiekty utworzone przez programistę
  • dict
  • set
  • list
  • bytearray

Teraz wyobraź sobie taki kod, gdzie tworzę zmienną, następnie zmieniam jej wartość. Wszystko pięknie, wartość została zmieniona. Drugim przypadkiem niech będzie lista cyfr. Na końcu listy dodaję cyfrę. Obiekt zmienił się, ponieważ została dodana liczba zgodnie z założeniami.

Zobacz jednak na kod na poniższym rysunku. Zwróć uwagę, że przy typie niemutowalnym, jakim jest str, zmienna x co prawda ma nową wartość, bo już nie jest 'Mateusz’, tylko 'Rus’. Zobaczmy jednak jej adresację w pamięci. Po użyciu funkcji ID widzimy, że pierwsza zmienna x ma inny adres niż druga. Co to oznacza? A no to, że niby ta sama zmienna, ale inna. Poprzednia zmienna x jest nadal w pamięci z wartością 'Mateusz’. Później mechanizm zwalniania pamięci o nazwie Garbage Collector usunie ją i zwolni pamięć. 

W drugim przypadku do zmiennej y, która jest mutowalną listą, dodałem liczbę 5 na końcu. Po sprawdzeniu adresu w pamięci możemy zauważyć, że y przed i po dodaniu wartości to nadal ten sam obiekt. I to jest właśnie ta magiczna różnica między mutable i immutable!

Kod zawierający wyjaśnienie mutalbe i immutable

Możesz zapytać o to, kiedy używać obiektów mutowalnych, a kiedy niemutowalnych. Mutowalne idealnie nadawać się będą do dynamicznych danych. Gdy coś się zmienia, zostaje dodane do obiektu, usunięte. Pomyśl tylko, że za każdym razem miałby powstawać nowy obiekt, gdy do listy będziesz dodawać nowy element. Przy pętli for, która dodaje jedynkę iteracja po iteracji na końcu zmiennej o typie str, powstałoby pięć obiektów, w których każdy znajdowałby się w innym miejscu w pamięci. Niby jedna zmienna, a jednak pięć!

Przykładowe iteracje

  • Pierwsza iteracja: x = 1 [adres w pamięci 99999990]
  • Druga iteracja: x = 11 [adres w pamięci 99999991]
  • Trzecia iteracja: x = 111 [adres w pamięci 99999992]
  • Czwarta iteracja: x = 1111 [adres w pamięci 99999993]
  • Piąta iteracja: x = 11111 [adres w pamięci 99999994]

Do tego idealnie sprawdzi się obiekt mutowalny taki jak lista, który mimo rozbudowy o nowy element, będzie występował jeden raz w pamięci.

  • Pierwsza iteracja: x = [1] [adres w pamięci 99999990]
  • Druga iteracja: x = [1,1] [adres w pamięci 99999990]
  • Trzecia iteracja: x = [1,1,1] [adres w pamięci 99999990]
  • Czwarta iteracja: x = [1,1,1,1] [adres w pamięci 99999990]
  • Piąta iteracja: x = [1,1,1,1,1] [adres w pamięci 99999990]

Obiekty niemutowalne z kolei nadają się do przechowywania wartości, które przez całe życie programu będą miały stałe wartości. Raz przypiszemy zmienną x na początku i później będziemy z niej korzystać bez zmiany jej wartości.

Uwaga! Immutable i mutable to przyczyny wielu programistycznych bugów!  Zobacz co się stanie, gdy utworzysz listę a, następnie listę b i zrobisz przypisanie a = b. Po tej operacji zrób dodanie do listy b jednego elementu i sprawdź co się teraz znajduje w a oraz b. Wynik Cię zaskoczy, ale wynik będzie logiczny, gdy pomyślisz, że jest to obiekt mutowalny! Napisz w komentarzu, do jakiego doszedłeś wniosku.

Zaawansowane aspekty funkcji — enum

Enum w programowaniu jest nazywany typem wyliczeniowym. Co to znaczy? Jest to specjalnie utworzony obiekt, który zawiera listę stałych, które mają przypisane niezmienne wartości. Enum działa podobnie jak klasa, jednak nią nie jest. Więcej o klasach opowiem w kolejnym wpisie z serii Python 3 by MR. Do zrozumienia enumów w Python nie potrzebujemy aktualnie tej wiedzy. Wystarczy nam fragment kodu, by zrozumieć koncepcję.

Wyobraź sobie, że musisz na podstawie marki samochodu wypisać kraj pochodzenia danej marki. Problem abstrakcyjny, ale do zaimplementowania. 

Możesz zrobić prostego if, gdzie będziesz wyliczać, że jeśli Audi, to Niemcy, jeśli Toyota to Japonia i tak dalej. Będzie to wyglądać następująco.

def car_country(car):
    if car == 'Audi':
        print('Niemcy')
    elif car == 'Mercedes':
        print('Niemcy')
    elif car == 'Opel':
        print('Niemcy')
    elif car == 'BMW':
        print('Niemcy')
    elif car == 'Toyota':
        print('Japonia')
    else:
        print('Brak')


car_country('Toyota')
car_country('Opel')
car_country('Kia')

Zwróci nam następujące dane:

Japonia
Niemcy
Brak

Niby działa, ale czujemy pewien niedosyt. Coś w tym kodzie fajnie byłoby poprawić. Pomyśl, że takich warunków w kodzie masz kilka. Teraz okazuje się, że w dziesięciu miejscach musisz zmienić pierwszy IF. Trochę słabo.

Tu z pomocą przychodzi ENUM. Wystarczy lekko zmodyfikować kod i od teraz wystarczy zmiana w jednym enum, która zostanie rozpropagowana na wszystkie miejsca w kodzie. Program teraz będzie wyglądał następująco:

from enum import Enum

Enum_Samochody = Enum('Samochody',
                      {'AUDI': 'Audi', 'MERCEDES': 'Mercedes', 'OPEL': 'Opel', 'BMW': 'BMW', 'TOYOTA': 'Toyota'})


def car_country(car):
    if car == Enum_Samochody.AUDI.value:
        print('Niemcy')
    elif car == Enum_Samochody.MERCEDES.value:
        print('Niemcy')
    elif car == Enum_Samochody.OPEL.value:
        print('Niemcy')
    elif car == Enum_Samochody.BMW.value:
        print('Niemcy')
    elif car == Enum_Samochody.TOYOTA.value:
        print('Japonia')
    else:
        print('Brak')

car_country('Toyota')
car_country('Opel')
car_country('Kia')

Importujemy bibliotekę enum, następnie tworzymy enum o nazwie Enum_Samochody, który zawiera kolekcję z samochodami. Po lewej stronie mamy klucz, po prawej wartość.

  • AUDI — klucz;
  • Audi — wartość;

Następnie w metodzie car_country w ifach zastępujemy tekstowe wartości enumem. Proste prawda? Wynik będzie identyczny, jak w pierwszym przypadku.

Gdy podejrzymy sobie funkcją print obiekt Enum_Samochody  to zobaczymy, że jest on listą elementów.

[<Samochody.AUDI: 'Audi’>, <Samochody.MERCEDES: 'Mercedes’>, <Samochody.OPEL: 'Opel’>, <Samochody.BMW: 'BMW’>, <Samochody.TOYOTA: 'Toyota’>]

Korzystanie z enumów poprawia również czytelność kodu, więc polecam Ci jak najwięcej używać takich konstrukcji we własnych programach. 

Zaawansowane aspekty funkcji — funkcja jako argument innej funkcji

Czas na trochę wyższy poziom abstrakcji. W języku Python (w wielu innych językach również) jest możliwość przekazania jednej funkcji do drugiej funkcji jako argument i wywołanie tej argumentowej funkcji w środku głównej funkcji. Pokręcone prawda? 

Jest to jednak normalna sprawa, ponieważ funkcja zgodnie z tym, co ustaliliśmy wcześniej, jest w Python obiektem. Obiekty natomiast możemy przekazywać do funkcji jako argumenty. Często u początkujących programistów takie zakręcenie może mrozić krew w żyłach i przerażać. Głównym powodem, dla którego warto tak budować swój kod, jest czytelność kodu.

Drugim, bardzo poważnym powodem, dla którego używa się takich konstrukcji, jest programowanie funkcyjne. Mam w planach zrobić o tym małą serię, ale na dziś wykracza to poza zakres tego artykułu. Zapamiętaj, że taki rodzaj programowania wspomaga mapowanie, filtrowanie czy sortowanie.

Zobacz na kod poniżej i spróbuj oswoić się z taką konstrukcją. Wynikiem będzie suma 2+2, czyli 4.

def func_a(func_b):
    func_b()

def func():
    print(2 + 2)

func_a(func)

Zaawansowane aspekty funkcji — zasięgi globalne i lokalne funkcji

Rozmawiając o funkcjach, nie możemy zapomnieć o tak istotnym temacie, jakim są zasięgi zmiennych. Zasięgi zmiennych są jedną z najczęstszych przyczyn błędów w kodzie przy kompilacji. Słynne name 'x’ is not defined to klasyk pojawiający się nawet w memach. Czasem podchodzących pod czarny humor…

Mem programistyczny

Zasięgi zmiennych w Python dzielimy na zasięgi lokalne i globalne. Zasięg lokalny dotyczy zmiennych zdefiniowanych w funkcji i tylko w niej mogą być używane. Globalne natomiast są dostępne z każdego miejsca w aplikacji.

Jak zaraz zobaczysz, zmienne globalne mogą być nadpisane zmiennymi lokalnymi. Odwrotnie się to nie dzieje. Najpierw zadeklarujemy zmienną globalną x równą 1990. Następnie stworzymy funkcję print_x, w niej lokalnie nadpiszemy zmienną x, która wyniesie 1992 i wypiszemy funkcją print tą zmienną. 

Później spróbujemy nadpisać globalnie zmienną x, wywołać funkcję i lokalnie wywołać print. Zawsze będziemy nadpisywać x lokalnie.

x = 1990

def print_x():
    x = 1992
    print(x)

print(x)

x = 1990
print_x()

Spokojnie natomiast możemy dostać się do zmiennej globalnej z poziomu funkcji. Wystarczy zdefiniować funkcję, w niej zrobić print ze zmienną zadeklarowaną globalnie i wywołać tę funkcję po deklaracji zmiennej.

def print_y():
    print(y)

y = 42
print_y()

Skomplikujmy nieco nasz przykład. Deklarujemy funkcję, w której do zmiennej globalnej, lokalnie chcemy dodać 100 i na końcu wypisać wartość. W pierwszym przypadku dostaniemy błąd local variable 'z’ referenced before assignment, który mówi nam, że nie możemy modyfikować zmiennej lokalnej w użyciu ze zmienną globalną. Kompilator chroni nas przed różnego rodzaju pomyłkami z tego typu operacją. Aby to obejść, musimy niejako wymusić dobranie się do zmiennej globalnej w funkcji przed rozpoczęciem operacji. Pomoże nam w tym słowo kluczowe global. Jest to jednak zła praktyka i zalecam jej nie używać na co dzień.

# ZLE
def print_z():
    z = z + 100
    print(z)

z = 42
print_z()

# DOBRZE
def print_z():
    global z
    z = z + 100
    print(z)

z = 42
print_z()

 

Zaawansowane aspekty funkcji — argumenty pozycyjne, kluczowe, oraz wielowartościowe

Argumenty pozycyjne to takie argumenty, gdzie liczy się kolejność przekazywania do funkcji. Wyobraź sobie, że masz funkcję, która przyjmuje trzy parametry. Pierwszy o typie str, drugi niech będzie int, a trzeci bool. Konieczne będzie przekazanie tych parametrów według ściśle określonej kolejności. Zobacz kod poniżej.

def arguments(x, y, z):
    if z:
        print(y + 10)
        print(x)

arguments('Mateusz', 100, True)

No dobrze, ale co zrobić w przypadku, gdy nie chcemy podawać w takiej kolejności, jak są parametry zdefiniowane w funkcji? Do tego przydaje się właśnie argument kluczowy, czyli podczas wywołania funkcji podajemy klucz każdego z argumentów. Jeśli tak zrobimy, nie musimy dbać o kolejność. Lekko zmodyfikowałem nasz kod.

def arguments(x, y, z):
    if z:
        print(y + 10)
        print(x)

arguments(z=True, x='Mateusz', y=100)

Ostatnim, bardzo fajnym typem argumentów są argumenty wielowartościowe. Czasami potrzebujesz stworzyć funkcję, która może przyjąć różną liczbę parametrów. Raz 1, czasem 10. Do tego sprawdzi się idealnie argument wielowartościowy, który oznaczamy znakiem * przed nazwą argumentu w definicji funkcji. Dzięki temu możemy mieć jeden parametr w definicji funkcji, a przy wywołaniu funkcji po przecinku możemy podać dowolną ilość argumentów. Sprawdź kod poniżej.

def arguments(*args):
    for x in args:
        print(x)


arguments(1, 2, 3, 4)

Zaawansowane aspekty funkcji — kopiowanie płytkie i głębokie

Wyżej już wspominałem o zmiennych mutowalnych. Gdy rozmawiamy o zmienności zmiennych, to trzeba wyjaśnić, czym jest kopiowanie płytkie, a czym głębokie. Aby rozpocząć dywagacje na ten temat, zobacz na kod poniżej. Tworzę funkcję, która na wejście dostaje listę, następnie modyfikuje pierwszy element w liście. 

Przed wywołaniem funkcji lista my_list będzie mieć inną pierwszą wartość niż po wywołaniu funkcji. Wynik poniższego programu będzie następujący:

[0, 1, 2, 3, 4, 5]
[1111, 1, 2, 3, 4, 5]

def change_function(myList):
    myList[0] = 1111


my_list = [0, 1, 2, 3, 4, 5]
print(my_list)
change_function(my_list)
print(my_list)

Aby zabezpieczyć się przed zmianą mutowalnego obiektu, musimy wykonać jego kopię. Użyjemy do tego funkcji copy. Wtedy będziemy mieć pewność, że są to dwa różne elementy w pamięci. Możesz to sprawdzić, używając u siebie funkcji id, którą używaliśmy wyżej w kodzie.

def change_function(myList):
    myList[0] = 1111


my_list = [0, 1, 2, 3, 4, 5]
print(my_list)
change_function(my_list.copy())
print(my_list)

No dobrze, ale czy to już załatwia nam sprawę? Otóż nie! Taki rodzaj kopiowania jak powyżej nazywany jest kopiowaniem płytkim. O ile id listy będzie różne, o tyle już poszczególne elementy będą miały ten sam id. Zobacz na rysunek poniżej.

Ok, więc co zrobić, by kopia była głęboka? Czyli taka, gdzie zarówno obiekt listy, jak i jej elementy znajdowały się w innym miejscu w pamięci?

Do tego potrzebujemy zaimportować bibliotekę copy i użyć zamiast funkcji copy() funkcję deepcopy. Zobacz na kod poniżej oraz rysunek, który zobrazuje Ci, co się zadzieje pod spodem kodu w pamięci komputera.

Dobrze zapamiętaj taką różnicę w kopiowaniu, bo po pierwsze, może pojawić się to na Twojej rozmowie o pracę. Po drugie, jest to przyczyna częstych błędów, gdzie zmienne mają inne wartości, niż te, których oczekujemy.

import copy

def change_function(myList):
    myList[0] = 1111


my_list = [0, 1, 2, 3, 4, 5]
print(my_list)
change_function(copy.deepcopy(my_list))
print(my_list)

Zaawansowane aspekty funkcji — podsumowanie

W ostatnim wpisie roku 2021 postanowiłem dać Ci jeszcze więcej wartości. Ten wpis to spora porcja wiedzy, więc musisz go bardzo dokładnie przeanalizować. Cały kod znajdziesz na moim koncie GitHub, na który serdecznie Cię zapraszam.

Na blogu pojawi się sporo nowości w 2022 roku, więc obserwuj bacznie 🙂 jeśli mogę mieć prośbę, to podziel się moimi postami ze znajomymi. Zapraszam Cię też do mojego podcast, bo za chwilę wpadnie tam kilka ciekawych, nowych odcinków.

Newsletter

Nie przegap i dołącz już dziś do 815 osób będących w tym Newsletter! Otrzymuj co niedzielę o godzinie 20 listę kilku ciekawych tematów, które miałem okazję obserwować w mijającym tygodniu.

Tematy będą głównie techniczne, ale czasami pojawi się coś, co może wprowadzi Cię w stan zadumy i zmusi do dyskusji w szerszym gronie. Zero spamu!

Źródła

https://docs.python.org/3/

Autor

Programista .NET i Python. Autor książki "Programistą być".

1 Komentarz

Napisz komentarz

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

Programistą być to przewodnik po programistycznym świecie, który każdy zainteresowany programowaniem powinien przeczytać!

X