Skip to content

Podstawy Pythona 🐍

Quickstart

Shebang

Jest to pierwsza linia skryptu (nie tylko w pythonie), która określa za pomocą czego ma być wykonany skrypt (może być to bash, zsh, python) Mamy tutaj 2 możliwości:

Ta bardziej uniwersalna (działa z wieloma systemami operacyjnymi)

#!/usr/bin/env python3

W tym wypadku używając flagi -S możemy dodać dodatkowe flagi do interpretera

#!/usr/bin/env -S ptpython -i

Można też użyć bezpośredniej ścieżki, gdzie dodawanie parametrów jest nieco łatwiejsze.

#!/usr/bin/python3

Interaktywny Python

Po dodaniu parametru -i nadal pozostaniemy w pythonie po wykonaniu skryptu, co pozwoli nam np zajrzeć do zmiennych, które były użyte, lub łatwo użyć klas, albo metod, które zostały zdefiniowane w skrypcie.

Jednak do nieco bardziej złożonych operacji w trybie interaktywnym warto rozważyć użycie innego pythona. Można tutaj np. użyć ptpython, Ipython lub bpython. Dla wygody mozemy też zmienić interpreter w shebangu.

Jeśli nie chcemy używać innego interpretera możemy sobie nieco poprawić czytelność pythona w terminalu za pomocą biblioteki rich.

Funkcja main

wykonuje się, gdy skrypt jest uruchamiany jako samodzielny program, a nie jako moduł czegoś innego

#!/usr/bin/python3 #warto to dać, aby system widział, że to skrypt w pythonie a nie np. w shellu
def main():
    print("Witaj świecie!")

if __name__ == "__main__":
    main()

Argumenty programu

# Print total number of arguments
print ('Total number of arguments:', format(len(sys.argv)))

# Print all arguments
print ('Argument List:', str(sys.argv))

# Print arguments one by one
print ('First argument:',  str(sys.argv[0]))
print ('Second argument:',  str(sys.argv[1]))

Do parsowania bardziej skomplikowanych argumentów warto użyć biblioteki argparse

Zwracanie wartości

sys.exit(numer)

Funkcje we/wy

pobrany_napis = input()
print("Twój napis to: " + pobrany_napis)

input() jest funkcją, która pobiera napis podany przez użytkownika ze standardowego wejścia (do entera) zwraca zmienną typu str

Dla strumieni:

import sys

for line in sys.stdin:
    sys.stdout.write(line)

Importowanie bibliotek etc

TODO

Przykładowe importy.

import math

from collections import namedtuple

from django.contrib.auth.forms import (
    AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm,
)

więcej informacji w opisie paczek.

Podstawy

Komentarze

"""
Komentarz na
kilka linii
"""
#komentarz na jedną linię

Zmienne

Deklarujemy je be określania ich typu a=5 (interpreter automatycznie uzna, że to będzie int) W odróżnieniu od takich języków jak C++, C# czy Java, w języku Python występuje typowanie dynamiczne. Oznacza to, że konkretny identyfikator, konkretna zmienna, np. a, może raz przechowywać napis ("Ala ma kota") by po chwili przechowywać liczbę całkowitą (3). Do sprawdzania typu danych służy funkcja type().

logiczna = True
print(type(logiczna))
## <class 'bool'>

Najważniejsze typy zmiennych:

  • bool, wartość logiczna, tak lub nie, prawda (True) albo fałsz (False)
  • int, liczba całkowita, np. -7, 0 czy 3:
  • float, czyli wartość zmiennopozycyjna, można ją utożsamiać z wartościami rzeczywistymi, np. -0.67, 1.0, 3.14
  • complex, czyli liczba zespolona, np. -7+8.5j. Warto pamiętać, że w Pythonie jednostką urojoną jest j, a nie i
  • str, powszechnie znany w innych językach jako string, czyli napis. Napis poznajemy głównie po tym, że jest zapisany w cudzysłowie (i to odróżnia go choćby od nazwy zmiennej czy liczby, które nie są w cudzysłowach). Dodamy, że w języku Python nie ma znaczenia, czy posługujemy się pojedynczymi (') czy podwójnymi (") cudzysłowami

Konwersja typów analogiczna dla przykładu poniżej

print(int(5.3))
## 5

Operatory:

  • dodawanie (+)
  • odejmowanie (-)
  • mnożenie (*)
  • dzielenie (/)
  • reszta z dzielenia, modulo (%) Analogiczne co C operatory -=, *=, /=, %=... UWAGA! Brak operatorów ++ i --
  • ** potęga
  • 2+3j liczba zespolona

Operatory Logiczne zwracają wartość logiczną (True/False)

  • < - mniejsze
  • <= - mniejsze równe, zwróćmy uwagę, że zapisujemy tak jak czytamy, nie ma operatora =< (równe mniejsze)
  • > - większe
  • >= - większe równe
  • == - równa się. Tutaj bardzo ważne jest, aby odróżniać operator przypisania (=) od operatora porównania równa się (==). To bardzo częsty błąd wśród początkujących programistów. Operator = nie jest symetryczny, ma na celu przekopiowanie wartości z prawej do lewej. W niektórych językach zapisuje się go jako <- (ale nie w Pythonie). Operator = nie ma nic wspólnego z wartościami logicznymi. Za to operator == jest operatorem zwracającym wartość logiczną, a co więcej, jest on operatorem symetrycznym (nie ma znaczenia zamienienie kolejnością argumentów).
  • != - nie równa się Operator koniunkcji, and, utożsamiany z polskim i. Zwraca wartość True wtedy i tylko wtedy, gdy oba argumenty są równe True Operator alternatywy, or, utożsamiany z polskim lub. Zwraca wartość True wtedy i tylko wtedy, gdy przynajmniej jeden argument jest równy True Operator zaprzeczenia, not, utożsamiany z polskim nie. Zwraca wartość przeciwną, niż argument

Można łączyć kilka operatorów np: czy_wiek_produkcyjny = 18 <= wiek <= 65 - w pythonie porównania rozwijane są tak jak w matematyce, czyli np możemy też napisać 2 <= 4 < 8, co zwróci nam True

Inne operatory:

  • is - czy dwie zmienne są różnymi instancjami tego samego obiektu
s1={}
s2={}

s1==s2
#True

s1 is s2
#False ##ponieważ to nie jest ten sam słownik
  • in używany do sprawdzenia, czy dana wartość/obiekt zawiera się w liście/słowniku/secie...
zbior = {1, 3, 5}
zbior_pusty = {}

print(1 in zbiorPusty)
## False
print(1 in zbior)
## True

Instrukcja warunkowa if

if warunek:
    instrukcja1
    instrukcja2
elif warunek2: # gdy warunek nieprawdziwy, sprawdź warunek2
    instrukcja3
    instrukcja4
elif warunek3: #gdy warunek2 nieprawdziwy, sprawdź warunek3
    instrukcja5
    instrukcja6
else: #gdy zaden z warunków nie byl prawdziwy
    instrukcja7
    instrukcja8

Warto także pamiętać o jedno-linijkowym wariancie if-a:

# a if condition else b
print("Prawda") if True else print("Fałsz")
#Prawda

Pusta zmienna

Czasem _ jest używane jako pusta zmienna jest to swego rodzaju odpowiednik /dev/null

TODO dopisać i omówić przykłady: https://stackoverflow.com/questions/5893163/what-is-the-purpose-of-the-single-underscore-variable-in-python

Pętle

Po for (foreach) może się znajdować lista (obrót dla każdego elementu) Po while warunek logiczny (obroty dopóki prawda)

for i in range(0, 4):
print(i)

## 0
## 1
## 2
## 3

#lub
i = 0
while i < 4:
print(i)
i+=1

liczby = [2, 3, 5]
for liczba in liczby:
print(liczba)
## 2
## 3
## 5

Słowo kluczowe continue przerywa dany obrót pętli Słowo kluczowe break przerywa całą pętlę

Jeśli iterujemy po liście krotek możemy sobie je rozbić

for i, line in enumerate(strings_list): #enumerate zwraca dla danej listy krotkę zawierającą numer i element z listy
    ###jakiś kod

Pętle jednolinijkowe (List Comprehensions)

Jednolinijkowe pętle zwracające np listę dobre dla prostych operacji, użyteczne do szybkiego generowania tablic.

newlist = [expression for item in iterable if condition == True]
#albo bez if-a
newlist = [x for x in range(10)]

fruits = ["apple", "banana", "cherry", "kiwi", "mango"]
newlist = []

for x in fruits:
  if "a" in x:
    newlist.append(x)

#daje to samo co:

newlist = [x for x in fruits if "a" in x]

Krotka (tuple)

Pewną specyficzną dla języka Python strukturą jest krotka. Polega ona na grupowaniu paru wartości w jeden byt. Warto zaznaczyć, że krotka, która raz została stworzona, nie może być modyfikowana: nie możemy podmienić jednej ze składowych krotki

krotka = (2,3)
print(krotka)
## (2, 3)

Gdybyśmy chcieli uzyskać poszczególne składowe krotki, możemy to zrobić przy użyciu operatora kwadratowych nawiasów. W Pythonie, tak jak w wielu językach programowania, numerujemy składowe od 0:

pierwsza = krotka[0]
druga = krotka[1]
print(pierwsza)
## 2
print(druga)
## 3
W krotce możemy mieszać typy danych:
krotka = (2, "Napis")
print(krotka)
## (2, 'Napis')
len(krotka) #długość krotki
## 2

Kiedy krotka może być przydatna? Np. gdy chcemy zwrócić więcej niż jedną wartość w funkcji.

def f(x):
    y0 = x + 1
    y1 = x * 3
    y2 = y0 ** y3
    return (y0, y1, y2)

Jeśli chcemy możemy rozbić krotkę na poszczególne zmienne

a,b,c = (1,2,3)

named tuple

Specjalne obiekty działające jak krotki i kompatybilne z nimi.

from collections import namedtuple
Point = namedtuple('Point', 'x y')
pt1 = Point(1.0, 5.0)
pt2 = Point(2.5, 1.5)

from math import sqrt
# wskazanie poprzez indeks
line_length = sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)
 # rozpakowanie krotki
x1, y1 = pt1

Funkcja zip

Do operowania na krotkach przydatna jest funkcja wbudowana zip zwracająca iterator (nie listę) pozwalający iterować po krotkach tworzonych z obiektów danych w funkcji.

a = ("John", "Charles", "Mike")
b = ("Jenny", "Christy", "Monica", "Vicky") # wartości nadmiarowe są pomijane

for kr in zip(a, b):
    print(kr)

#('John', 'Jenny')
#('Charles', 'Christy')
#('Mike', 'Monica')

Lista

Podobną, przynajmniej na pozór, strukturą danych do krotki jest lista. Tutaj także możemy grupować dane oraz nie muszą one być tego samego typu. Jednak główną różnicą jest to, że listę możemy modyfikować. Możemy dodawać nowe elementy czy zastępować dotychczasowe.

lista = [1, False, "Napis"]
print(lista)
## [1, False, 'Napis']
print(len(lista))
## 3
lista.append(2.5+3.7j)
print(lista)
## [1, False, 'Napis', (2.5+3.7j)]
lista.extend([97,98,99]) # metoda podobna po append, która przyjmuje jako argument całą listę i dodaje z niej kolejne elementy
print(lista)
## [1, False, 'Napis', (2.5+3.7j), 97, 98, 99]

Operatory + i * mają zdefiniowane działanie w kontekście list. +, tak jak w przypadku napisu, to konkatenacja, czyli połączenie dwóch list w jedną * zaś pozwala nam powielić daną listę:

print(lista + [7,8,9])
## [1, False, 'Napis', (2.5+3.7j), 97, 98, 99, 7, 8, 9]
print([7,8,9] * 3)
## [7, 8, 9, 7, 8, 9, 7, 8, 9]

Odwołanie się do konkretnego elementu następuje tak jak w krotce:

print(lista[2])
## Napis
! ujemne indeksy oznaczają pozycje liczone od tyłu
print(lista[-1])
## 99

Ogólna składnia indeksowania wygląda tak: [od:do:krok]. Domyślnie od to 0, do to długość listy, a krok to 1.

Odwoływanie się do wycinka listy
print(lista[2:5])
## ['Nowy', (2.5+3.7j), 97]
Elementy o numerach 2, 3, 4 (bez 5)

"ala ma kota"[-4::] #elementy od trzeciego od końca aż do ostatniego
#"kota"

"123456789"[::2] #co drugi element
#13579
"123456789"[::-1] #odwrócenie listy
#987654321

Podmiana

lista[0:3] = [98,99,101,102]
print(lista)
## [98, 99, 101, 102, (2.5+3.7j), 97, 98, 99]

Usuwa 3 pierwsze elementy i na ich miejsce wstawia te podane

Wstawienie elementu w innym miejscufor klucz in bazaDanychPolakow.keys():

print(klucz)

for wartosc in bazaDanychPolakow.values():
print(wartosc)

for klucz, wartosc in bazaDanychPolakow.items(): ##Tutaj items() zwraca krotkę
print(klucz)
print(wartosc)
print("-----")


lista.insert(1, "Nowy2")
print(lista)
## [98, 'Nowy2', 99, 101, 102, (2.5+3.7j), 97, 98, 99]
Usunięcie elementu
del lista[1]
print(lista)
## [98, 99, 101, 102, (2.5+3.7j), 97, 98, 99]
Napisy
Zmienna w napisie
zmienna = 7
napis = f"wartość zmiennej to {zmienna}"
print(napis)
## wartość zmiennej to 7
wieleWierszy = """Tutaj pierwszy
a tu drugi
tutaj trzeci"""
print(wieleWierszy)
## Tutaj pierwszy
## a tu drugi
## tutaj trzeci

Kolejność alfabetyczna

print("a" < "b")
## True

Więcej na: https://www.kodolamacz.pl/blog/wyzwanie-python-3-algorytmy-i-struktury-danych/

Zbiór (set)

to tablica (tyle, że bez indeksowania), w których nie ma dwóch lub więcej identycznych elementów. (jest szybszy od listy)

zbiorPusty = set()
zbior = {1, 3, 5}
print(zbiorPusty)
## set()
print(zbior)
## {1, 3, 5}
zbior.add(2) ##Dodawanie elementu
zbior.discard(2) ##Usuwanie elementu
Można w nim łatwo sprawdzać, czy dany element należy do zbioru
print(1 in zbiorPusty)
## False
print(1 in zbior)
## True
print(1 not in zbiorPusty)
## True

Możemy na nich wykonać typowe operacje teoriomnogościowe, jak suma, różnica czy przecięcie dwóch zbiorów

print({1,5,8} | {1,5,9}) # suma
## {1, 5, 8, 9}
print({1, 5, 8} - {1, 5, 9}) # różnica

## {8}
print({1, 5, 8} & {1, 5, 9}) # przecięcie
## {1, 5}
Można zapytać, czy jeden zbiór jest podzbiorem drugiego
print({1, 5}.issubset({1, 5, 9}))
## True

Słownik

Jest to rozszerzenie idei zbioru. Słownik zawiera pary klucz-wartość. Wyszukiwanie po kluczu jest szybkie, tak jak w zbiorze, jednak gdy już odnajdziemy klucz, możemy odzyskać także stowarzyszoną z nim wartość. Gdy usuniemy ze słownika wartości, a zostawimy same klucze, otrzymamy zbiór. Tak jak w zbiorze, w słowniku klucze nie mogą się powtarzać.

bazaDanychPolakow = {"89082911111" : ["Jan", "Kowalski", 29],
"95092200000" : ["Ania", "Nowak", 23],
"98122422222" : ["Adam", "Mickiewicz", 220]
}
bazaDanychPolakow["88081244444"] = ["Magda", "K", 30] #Dodanie nowej pary klucz-wartość
del bazaDanychPolakow["88081244444"] #Usunięcie danej pary

Tak jak w zbiorze możemy sprawdzać, czy klucz znajduje się w słowniku.

Uzyskanie wartości stowarzyszonej z kluczem

print(bazaDanychPolakow["98122422222"])
## ['Adam', 'Mickiewicz', 220]
Gdy chcemy zabezpieczyć się przed odwołaniem do nieistniejącego elementu (i w tym wypadku zwrócić wartość domyślną), użyjemy metody get():
print(bazaDanychPolakow.get("89082911111", "wartość domyślna"))
## ['Jan', 'Kowalski', 29]
print(bazaDanychPolakow.get("95092200022", "wartość domyślna"))
## wartość domyślna

Przejście w pętli

for klucz in bazaDanychPolakow.keys():
print(klucz)

for wartosc in bazaDanychPolakow.values():
print(wartosc)

for klucz, wartosc in bazaDanychPolakow.items(): ##Tutaj items() zwraca krotkę
print(klucz)
print(wartosc)
print("-----")

Przy printowaniu słowników (zwłaszcza tych skomplikowanych) warto użyć pprint

import pprint
pprint.pprint(duzy_slownik)

Funkcje

Funkcje

def nazwaFunkcji(parametry, oddzielone, przecinkami):
return wynik
Wartości domyślne parametrów
def potega(podstawa, wykladnik=1):
#ciało funkcji
Argumenty nazwane Możemy podawać argumenty w dowolnej kolejności, gdy podamy ich nazwy
print(potega(wykladnik = 4, podstawa = 3))
## 81
def printinfo( name, age = 35 ):
   "Prosta funkcja z domyślnymi wartościami"
   print("Name: ", name)
   print("Age ", age)
   return

Funkcja może także przyjmować wiele argumentów

#** - zmienne będą interpretowane jako krotka
def printinfo( arg1, *krotka ):
   print("Output is: ")
   print(arg1)
   for var in krotka:
      print(var)
   return
#** - zmienne będą interpretowane jako słownik
def printinfo2( arg1, **slownik ):
   print( "Output is: ")
   print(arg1)
   for key in slownik.keys():
      print(key)
   return

printinfo( 10 )
printinfo( 70, 60, 50 )
printinfo2("argum1",klucz1=wart1,klucz2=wart2,klucz3=523,pusty=None) #ważne, aby słownik był definiowany jako słownik, czyli klucz=wartosc

W pythonie jedna funkcja może zwracać różne rzeczy, obiekty, zmienne, nic.

def returnOrNot(return_bool=True):
    if return_bool:
        return True
    return

Adnotacje

W wypadku funkcji możemy też dodać adnotacje (typowanie zmiennych) do ich argumentów oraz wartości zwracanych (nie są one wykorzystywane przez interpreter, ale ułatwiają dokumentowanie). (Uwaga, tej funkcji nie ma w pythonie 2, dla niego jest workaround o nazwie type comments )

def funkcja( liczba1:99=12 , slowo1:str="sl", slowo2:"inne slowo"="inne") -> str:
    #some code
    return "slowo"


slowo: str = "inne_slowo"

Można potem je sprawdzić poprzez sięgnięcie do atrybutu __annotations__

>>>funkcja.__annotations__
{'liczba1': 99, 'slowo1': <class 'str'>, 'slowo2': 'inne slowo', 'return': <class 'str'>}

W niektórych wypadkach może pojawić się potrzeba zaimportowania oczekiwanych elementów

from typing import Tuple
def foo(x:int, y:int) -> Tuple[int, int]:
    return (x*y, y//2)

Możliwe są też adnotacje dla elementów klasy PEP-526

class Starship:
    stats: ClassVar[dict[str, int]] = {} # class variable - pole klasy
    damage: int = 10                     # instance variable - pole instancji

W razie problemów spowodowanych kolejnością deklaracji różnych elementów w pliku warto użyć specjalnego importu

from __future__ import annotations # bez niego poniższy kod by nie działał

def fun(i: A): #moglibyśmy otzymać błąd mówiący o odnoszeniu się do nieistniejącej klasy
    pass

class A:
    pass

Wyrażenia lambda

Lambdy to są funkcje, które można w dość podręczny sposób zdefiniować

#Ogólna definicja
lambda arg1, arg2, arg3: nasze_wyrazenie #ta lambda zwróci wartość naszego wyrażenia

nasza_lambda = lambda x: x*2
nasza_lambda(2)
#>4

Argumenty w lambdach można zapisywać tak samo jak w zwykłych funkcjach, mogą tam być wartości domyślne,

Zasięg i zmienne globalne

W pythonie na ogół funkcje nie mogą edytować (mogą mieć dostęp, ale nie edytować) zmienne poza swoim zakresem.

c = 1 # global variable

def add():
    print(c)
    c = c + 2 # increment c by 2
    print(c)

add()
#>2
#>UnboundLocalError: local variable 'c' referenced before assignment

Aby móc je jednak zmieniać jest używane słowo kluczowe global.

c = 0 # global variable

def add():
    global c
    c = c + 2 # increment by 2
    print("Inside add():", c)

add()
print("In main:", c)
#>Inside add(): 2
#>In main: 2

Przekazywanie dowolnych argumentów (**kwargs i *args)

Pozwalają one na umieszczeanie argumentów o dowolnej liczbie i nazwie w naszej funkcji. args przyjmuje je jako listę kolejnych elementów, zaś kwargs przyjmuje je jak słownik.

def parametr_args(*args):
    print("zawartość args: {}".format(args))

parametr_args('python', 'spam', 'eggs', 'test')
###zawartość args: ('spam', 'eggs', 'test')

def parametr_kwargs(argument, **kwargs):
    print("argument: {}".format(argument))
    print("zawartość kwargs: {}".format(kwargs))

parametr_kwargs(dodatkowy=48, nastepny=111, argument=12)

# argument: 12
# zawartość kwargs: {'dodatkowy': 48, 'nastepny': 111}

//TODO https://printpython.pl/poczatki/zadanie-z-gwiazdka/

Dekoratory funkcji

Jest to element pozwalający na opakowanie naszej funkcji za pomocą innej funkcji, aby wzbogacić jej funkcjonalność.
Funkcja dekorująca najczęściej przyjmuje funkcję dekorowaną i zwraca nową, wynikową funkcję, która ma zostać wykonana.

#foo jest dekoratorem, który wzbogaci naszą funkcję
def foo(to_be_wrapped):
    def new_func(args,**kwargs):
        print("uwaga, będzie sześcian")
        return to_be_wrapped(*args,**kwargs)  # warto je dodać aby argumenty zostały przekazane dalej do funkcji docelowej

    return new_func

@foo #jeśli dodamy ten dekorator to użycie tej funkcji zostanie zmienione, tzn zamiast oryginalnej funkcji cube() otrzymamy "wzbogacone" cube drukujące komunikat przed drukowaniem
def cube(d):
    return d ** 3 #podniesienie do potęgi 3

cube(2)
#uwaga, będzie sześcian
#8

Ale warto pamiętać, że jeśli chcemy przygotowywać takie dekoratory wewnątrz klas to musimy pamiętać, żę nieco inaczej wygląda dostęp do self

class Myclass:
    # wydaje mi się, że tutaj dekorator musi być zdefiniowany jako pierwszy
    def _add_loaded_location_to_token(decorated_fun, *args, **kwargs):
        def output_fun(*args, **kwargs):
            t = decorated_fun(*args, **kwargs)
            t.location = args[0].current_location # po prostu self jest schowany pod pierwszym z argumentów
            return t

        return output_fun

    @_add_loaded_location_to_token
    def get_token(self):
        #kod
        return token

overload (przeciążanie funkcji)

W Pythonie możliwe jest także przeciążanie funkcji oraz metod. Można do tego celu użyć dekotarota @overload.
Pozwala on zdefiniować alternatywne implementacje funkcji.

from overloading import overload
from collections import Iterable

def flatten(ob):
    """Flatten an object to its component iterables"""
    yield ob

@overload
def flatten(ob: Iterable):
    for o in ob:
        for ob in flatten(o):
            yield ob

@overload
def flatten(ob: basestring):
    yield ob

//TODO zweryfikować czy używanie tego jest dobrą praktyką

Statyczne pola funkcji

Odpowiednikiem zmiennych typu static wewnątrz funkcji jest pole funkcji. źródło

def myfunc():
  myfunc.counter += 1
  print myfunc.counter

# atrybut musi być gdzieś zainicjalizowany
myfunc.counter = 0

Można też dodać atrybut w ten sposób:

def myfunc():
  if not hasattr(myfunc, "counter"):
     myfunc.counter = 0  # it doesn't exist yet, so initialize it
  myfunc.counter += 1

Obiektówka

Obiekt

class Osoba: #Definicja klasy o nazwie Osoba
    ile = 0 # pole klasy
    imie: str # adnotacja typu dla pola instancji
    def __init__(self, imie, nazwisko, wiek): #Definicja konstruktora
        self.imie = imie
        self.nazwisko = nazwisko
        self.wiek = wiek
    def przedstaw_sie(self):
        print(f"Jestem {self.imie} {self.nazwisko}. Mam {self.wiek} lat.")
    def urodziny(self):
        wiek_przed = self.wiek
        self.wiek += 1
    return wiek_przed
    def __del__(self): # destruktor, czyli kod, który wykonuje się podczas niszczenia obiektu
    @staticmethod
    def policz():
        return Osoba.ile

Obiekty w pythonie nie mają pól statycznych działających tak jak w innych językach (jak np C++). Tutaj zmiana pola klasy nie zmienia wartości tego pola dla innych instancji link.

Metody

Metodę możemy poznać min. także po pierwszym argumencie: self. W języku Python metody przyjmują jako pierwszy parametr obiekt, na rzecz którego są wywoływane. W samym wywołaniu nie musimy go sami podawać. Wystarczy, że metoda jest napisana po kropce. Następnie następują trzy zwykłe parametry: imie, nazwisko oraz wiek.

Konstruktor i destruktor

Jest to taka metoda, która jest wywoływana, gdy obiekt jest tworzony. Jej celem jest zainicjowanie pól w instancji. Tu są definiowane parametry klasy. Konstruktor poznajemy po jego specjalnej nazwie: __init__. Analogicznie działa destruktor (nazwa: __del__) Przy wywołaniu pomijamy argument self.

Jan = Osoba("Jan", "Nowak", 48)
Jan = None #Wymuszenie destrukcji obiektu

Kiedy interpreter napotka kod Foo() dzieją się następujące rzeczy:

  • Wołana jest metoda __call__() dla klasy po której Foo dziedziczy (domyślnie to jest klasa type)
  • metoda __call__() woła odpowiednio:
  • __new__() - new tworzy nową instancję klasy (po więcej szczegółów sprawdź metaklasy )
  • __init__() - init ją inicjalizuje (z tego powodu on nie musi nic zwracać)
Własne operatory

W pythonie można w prosty sposób dodawać własne operatory do klas. Możemy w prosty sposób sprawić, że korzystanie z naszych klas będzie wygodniejsze.

class C:
    __str__(self):
Operator Nazwa funkcji Opis
str() __str__(self) Funkcja generująca stringa dla danej klasy. Przydatne do debugowania i nie tylko
+ __add__(a,b) Dodawanie
[] __getitem__(self,index) Te kilka jest potrzebne do poprawnego zaimplementowania operatora []
[] __setitem__(self,index,item)
[] __delitem__(self.index)

Pełniejsza lista operatorów TODO popraw i rozbuduj

Widoczność elementów

W języku Python nie ma pól prywatnych w klasie: nie jesteśmy w stanie w praktyce czegokolwiek “ukryć”. Jednak są pewne zasady nazewnictwa, które działają raczej na zasadzie porozumienia, niż będące prawdziwą barierą. I tak, gdy poprzedzimy nazwę jednym znakiem podkreślenia _, oznajmiamy, że dany element nie jest uwzględniony w dokumentacji, może się zmienić, raczej nie należy z niego korzystać, a środowisko programistyczne nie będzie nam go podpowiadać. Przykładowo pole _imie, np. self._imie, czy self._metoda().

Gdy użyjemy dwóch znaków podkreślenia __, zachowanie jest trochę inne: dane pole czy metoda nie będzie widoczna pod tą nazwą wcale, ale za to będzie można się do niego odwołać (dla nazwy __element) poprzez _nazwaklasy.__element.

Statyczne

Dla odmiany są one tworzone poza konstruktorem. Do pola tego odwołujemy się poprzez nazwę klasy. Np Osoba.ile Sama metoda statyczna ma nad sobą napis @staticmethod. To tzw. dekorator. Metoda statyczna nie może odwoływać się do instancyjnych pól (czyli tych zwykłych, jak imie z poprzedniego przykładu), a jedynie do statycznych. Wynika to z faktu, że metoda statyczna nie jest wywoływana na rzecz konkretnego obiektu, który by takie właśnie pola miał.

Jeśli jednak chcemy aby nasza metoda miała jakieś informacje na temat naszej klasy można użyć dekoratora @classmethod, który różni się tym, że klasaobiektu (nie instancja) jest przekazywana dalej. Porównanie na stacku

class A(object):
    def foo(self, x):
        print(f"executing foo({self}, {x})")

    @classmethod
    def class_foo(cls, x):
        print(f"executing class_foo({cls}, {x})")

    @staticmethod
    def static_foo(x):
        print(f"executing static_foo({x})")

Dziedziczenie

class Zwierze:
    def __init__(self, nazwa, wiek, waga):
        self.nazwa = nazwa
        self.wiek = wiek
        self.waga = waga
    def przedstaw_sie(self):
        print(f"Jestem zwierzęciem {self.nazwa}, mam {self.wiek} lat oraz wazę {self.waga} kg.")
    def urodziny(self):
        self.wiek += 1

class Mrowka(Zwierze):
    pass #Oznacza, że ciało jest puste

class Slon(Zwierze):
    def przedstaw_sie(self):
        print(f"Jestem słoniem {self.nazwa}, mam {self.wiek} lat oraz wazę {self.waga} kg.")

class Lew(Zwierze):
    def przedstaw_sie(self):
        super().przedstaw_sie()
        print("A tak w ogóle to jestem lwem")

class Papuga(Zwierze):
    def __init__(self, nazwa, wiek, waga, kolor):
        super().__init__(nazwa, wiek, waga)
        self.kolor = kolor
    def przedstaw_sie(self):
        super().przedstaw_sie()
        print(f"Jako papuga mój kolor to {self.kolor}")

super() zwraca nam instancję klasy bazowej: są to wszystkie pola i metody naszego obiektu, jakie otrzymaliśmy dzięki klasie bazowej.

Polimorfizm

Pozwala na używanie klasy dziedziczącej wszędzie tam, gdzie może być użyta klasa bazowa. Oznacza to, że instancja klasy dziedziczącej jest uznawana za instancję klasy bazowej. W języku Python sprawdzenie przynależności danego obiektu do klasy wykonuje się metodą isinstance():

def main():
    Dumboo = Slon("Dumboo", 77, 6000)
    Simba = Lew("Simba", 24, 100)
    Jago = Papuga("Jago", 32, 3, "czerwony")
    jakis_zwierz = Zwierze("Katarzyna", 31, 80)
    print(f"isinstance(Dumboo, Slon): {isinstance(Dumboo, Slon)}")
    print(f"isinstance(Dumboo, Lew): {isinstance(Dumboo, Lew)}")
    print(f"isinstance(Jago, Papuga): {isinstance(Jago, Papuga)}")
    print(f"isinstance(Jago, Zwierze): {isinstance(Jago, Zwierze)}")
    print(f"isinstance(jakis_zwierz, Zwierze): {isinstance(jakis_zwierz, Zwierze)}")
    print(f"isinstance(jakis_zwierz, Papuga): {isinstance(jakis_zwierz, Papuga)}")

if __name__ == "__main__":
    main()

## isinstance(Dumboo, Slon): True
## isinstance(Dumboo, Lew): False
## isinstance(Jago, Papuga): True
## isinstance(Jago, Zwierze): True
## isinstance(jakis_zwierz, Zwierze): True
## isinstance(jakis_zwierz, Papuga): False

Abstrakcja

Uniemożliwia tworzenie instancji danej klasy. Przydatne przy klasach bazowych dokumentacja

from abc import ABC

class Zwierze(ABC):
    def __init__(self,nazwa, wiek, waga):
        self.nazwa=nazwa
        self.wiek=wiek
        self.waga=waga

    @abstractmethod #tutaj wymuszamy implementację tej metody w klasach pochodnych
    def nazwa_gatunku(self):
        pass
    def przedstaw_sie(self):
        print(f"Jestem {self.nazwa_gatunku()}. Mam na imię {self.nazwa}, mam {self.wiek} lat oraz wazę {self.waga} kg.")
    def urodziny(self):
        self.wiek += 1

class Slon(Zwierze):
    def nazwa_gatunku(self):
        return "Słoń"

class Lew(Zwierze):
    def nazwa_gatunku(self):
        return "Lew"

Niestety, mechanizm klas i metod abstrakcyjnych (klasa jest abstrakcyjna gdy ma co najmniej jedną metodę abstrakcyjną) w języku Python jest wprowadzony trochę sztucznie. Klasa bazowa (abstrakcyjna) musi dziedziczyć po sztucznej klasie ABC, a metoda abstrakcyjna jest opatrzona dekoratorem @abstractmethod. Zwróćmy uwagę, że jedno i drugie zostało zaimportowane. Jednak po tych czynnościach rzeczywiście nie jesteśmy w stanie stworzyć instancji klasy bazowej.

Zwróćmy uwagę na ten zaawansowany mechanizm: w klasie Zwierze tworzymy metodę, zakładamy, co ta metoda będzie zwracać, a następnie korzystamy z niej w innej metodzie, pomimo, że prawdziwa jej implementacja nastąpi dopiero w klasie pochodnej. Dzięki temu musimy napisać mniej kodu w klasach pochodnych: musimy jedynie zaimplementować metodę nazwa_gatunku(), jednak nie musimy już od zera pisać kodu na przedstawienie zwierzęta. Jedynie w klasie Papuga, gdzie wprowadziliśmy nowe pole, dopisujemy kod odpowiedzialny za wypisanie jego wartości.

/TODO jak poradzić sobie z tym?

class Zwierze(ABC):
    def __init__(self,nazwa, wiek, waga):
        self.nazwa=nazwa
        self.wiek=wiek
        self.waga=waga

    @abstractmethod #tutaj wymuszamy implementację tej metody w klasach pochodnych
    def nazwa_gatunku(self):
        pass

    def urodziny(self):
        self.wiek += 1

class Slon(Zwierze):
    def __init__(self,nazwa, wiek, waga):
        syper().__init__(self,nazwa, wiek, waga)
    def nazwa_gatunku(self):
        return self.nazwa

class Slon(Zwierze):
    def __init__(self,nazwa, wiek, waga):
        syper().__init__(self,nazwa, wiek, waga)

    # def nazwa_gatunku(self): bez implementowania nazwy, bo przecież zwykły skoń ma dobrą
    #     return self.nazwa

Hermetyzacja

Polega na odcinanie użytkownikowi dostępu do pól, aby operował tylko metodami klasy. Jednak oczywiście używanie metod, zwłaszcza z przedrostkiem get, czy set, jest mniej wygodne. Dlatego nowoczesne języki programowania umożliwiają tworzenie tzw. właściwości (ang. property). Z punktu widzenia możliwości, są to po prostu metody, jednak z punktu widzenia zapisu i wygody, przypominają one pola.

class Zwierze:
    def __init__(self, wiek):
        self.wiek = wiek

    @property
    def wiek(self):
        return self.__wiek
    @wiek.setter
    def wiek(self, wiek):
        if wiek < 0:
            self.__wiek = 0
        elif wiek > 200:
            self.__wiek = 200
        else:
            self.__wiek = wiek
def main():
    jakis_zwierz = Zwierze(202)
    print(jakis_zwierz.wiek)
    jakis_zwierz.wiek = -10
    print(jakis_zwierz.wiek)
    jakis_zwierz.wiek = 30
    print(jakis_zwierz.wiek)

if __name__ == "__main__":
    main()
## 200
## 0
## 30

Metaklasy

Metaklasa (metaclass) jest typem danej klasy. Odpowiada on typowi samej klasy, nie zaś typowi dla jej instancji. link1, link2

class Cl:
    pass

c = Cl()
type(c)
#>> <class '__main__.Cl'> 
#Klasa Cl jest typem dla instancji

type(Cl)
#>> <class 'type'>
# Klasa type jest typem dla klasy jest to tzw. Metaklasa

type(type)
#>> <class 'type'>
#type jest domyślną uniwersalną metaklasą
flowchart BT

type
Cl
c

c --> Cl
Cl --> type
type --> type

Metaklasa type może być wykorzystana także do generowania definicji klas w sposób dynamiczny używając konstruktora: type(<name>, <bases>, <dct>)

  • name -nazwa klasy
  • bases - krotka z klasami po których dziedziczymy
  • dict słownik z polami klasy (czyli polami, funkcjami etc)
Foo = type('Foo', (), dict(length=100))

Własne metaklasy służą przede wszystkim do modyfikowania tworzenia nowych klas, ponieważ metoda __new__ nie może być zmieniana dla domyślnej metaklasy. (opis tworzenia możesz znaleźć w rodziale konstruktory).

class Meta(type):
    def __new__(cls, name, bases, dct):
        x = super().__new__(cls, name, bases, dct)
        x.attr = 100
        return x

class Foo(metaclass=Meta):
    pass

Na ogół Metaklasy są rzadko używanym mechanizmem, ponieważ wiele problemów może być rozwiązanych w prostszy sposób. W wypadku potrzeby ustawienia pewnych elementów wystarczą albo dekoratory dla klas, bądź proste dziedziczenie.

def dekorator(cls):
    class NewClass(cls):
        attr = 100
    return NewClass

@dekorator
class Udekorowana:
    pass

#dziedziczenie
class Base:
     attr = 100

class Pochodna(Base):
    pass

////TODO więcej przykładów

Specjalne typy obiektów

Jest lika szczególnie użytecznych typów obiektów nad którymi warto się pochylić

Enum - Obiekty wyliczeniowe. Jest kilka rodzajów enumów: Enum, IntEnum, Flag oraz IntFlag. link

from enum import Enum, auto
class Color(Enum):
    RED = auto()
    BLUE = auto()
    GREEN = auto()

dataclass - specjalny dekorator dla obiektu dodający automatycznie wiele dodatkowych udogodnień pozwalających uniknąć mozolnego pisania logiki. link

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0
    tags: List[str] = = field(default_factory=list)

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Możliwe jest tutaj podawanie spodziewanych typów oraz domyślnych wartości (ale w wypadku typów mutowalnych należy użyć fabryk )

Taki obiekt może otrzymać (w zależności od paramterów):

  • automatycznie wygenerowany konstruktor:
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand
  • operatory porównania (=, <,> etc.)
  • funkcję haszującą
  • etc...

Inne

Wyjątki

def silnia(n):
    if n < 0:
    raise ValueError("silnia niezdefiniowana dla liczb ujemnych")
    wynik = 1
    for i in range(1, n+1):
        wynik *= i
    return wynik

try:
    print("Pozyskuję zasób")
    print(f"Silnia z -5 to {silnia(-5)}")
except ValueError as e:
    print("Och nie, coś poszło nie tak! Szczegóły poniżej:")
    print(e)
else:
    print("Obyło się bez wyjątków")
finally:
    print("Zwalniam zasób")

Gdy spodziewamy się, że dany fragment kodu może rzucać wyjątkami, opakowujemy go w konstrukcję try-except. Kod, który chcemy wykonać, a który może rzucić wyjątek, zapisujemy po try:. Następnie, na dole tego kodu, piszemy except, po czym piszemy nazwę klasy wyjątku, a także as, po którym mówimy, jakim identyfikatorem (w jakiej zmiennej) chcemy się odnosić do instancji tego wyjątku. Najważniejsza jest nazwa klasy, aby ustalić, jaki typ błędów łapiemy. Konkretna instancja, w przykładzie e, przydaje się, gdy np. chcemy wyświetlić komunikat błędu na ekran. Teoretycznie instancja ma swoje pola, do których możemy się odnieść, jednak rzadko się z nich korzysta.

Listę wbudowanych klas wyjątków znajdziemy tutaj. Szczególnej uwadze polecamy IndexError, gdy odwołujemy się do nieistniejącego elementu listy, FileNotFoundError, gdy plik nie istnieje, ZeroDivisionError dla dzielenia przez zero i wymieniony w przykładzie ValueError, gdy argumenty funkcji są błędne

try:
    fun()
except RuntimeError as err:
    print(f"Dostaliśmy wyjątek Runtime o treści: {err.args[0]}")
except TypeError as err:
    print("Niedozwolona operacja")
except (MojError, NameError):
    print("Wystąpił jeszcze inny error")
finally:
    print("Podczas wykonywania wystąpił błąd, zamykam apkę") # ten wykona się po każdym z wyjątków

With

Słowo kluczowe with pozwala na alternatywną (czystszą i czytelniejszą obsługę wyjątków)

# file handling

# 1) without using with statement
file = open('file', 'w')
file.write('hello world !')
file.close()

# 2) without using with statement
file = open('file', 'w')
try:
 file.write('hello world')
finally:
 file.close()

# using with statement
with open('file', 'w') as file:
 file.write('hello world !')

Słowo with pozwala na automatyczne zwalnianie zasobów (na przykładzie powyżej widać, że nie trzeba wołać close()) przy wyjątku. Mechanizm ten korzysta z metod __enter__() i __exit__() dla używanego obiektu.

Możemy wykorzystać ten mechanizm we własnych klasach

# a simple file writer object

class Manager(object):
 def __init__(self, file_name):
  self.file_name = file_name

 def __enter__(self):
  self.file = open(self.file_name, 'w')
  return self.file

 def __exit__(self):
  self.file.close()

with Manager('file.txt') as xfile:
 xfile.write('hello world')

Pliki

ścieżka_do_pliku = r"C:\przykladowy.txt"
#r sprawia, że / nie jest znakiem specjalnym
f = open(sciezka_do_pliku)
print(f.read())
f.close()

Tutaj mamy wszystko: open() służy otworzeniu połączenia do pliku. Jest to wspomniane wcześniej pozyskanie zasobu. Następnie następuje użycie metody read(). Odczytuje ona całą treść pliku za jednym zamachu. Po pojedynczym odczytaniu, drugie wywołanie zwróci nam napis pusty. Na końcu jest close(), zamknięcie połączenia do pliku. Jest to zwolnienie zasobu.

Jednak istnieje jeszcze drugi zapis. Używamy słowa kluczowego with. Wtedy definiujemy zasób, mówimy co chcemy zrobić, (open/write) gdy go pozyskamy, a na końcu, po wykonaniu całej klauzuli, zasób jest zwolniony, niezależnie od tego, czy wydarzyła się sytuacja wyjątkowa czy też nie

try:
    with open(sciezka_do_pliku) as f:
    print(f.read())
    print(2/0)
except ZeroDivisionError as e:
    print(e)
with open(sciezka_do_pliku, 'w') as f:
f.write("Trzeci wiersz")
f.write("Czwarty wiersz")#jeżeli nie damy \n to oba wiersze są zapisane w tej samej linijce
#Pozostałe przydatne metody
f.readline()
f.read()
f.closed #informuje czy już zamknięte

With open flagi:

  • r -read
  • w -write otwiera plik (i nadpisuje, jeżeli tam już coś jest)
  • a -append (otwiera do zapisu i zaczyna na końcu tzn dopisuje)

Dane o plikach

import os
print(os.path.exists(sciezka_do_pliku))
## True
print(os.listdir("C:\folder\"))
## ['Wyzwanie6', 'pomysly.txt', inne pliki...]
print(os.path.join("folder1", "folder2", "plik.txt"))
## folder1/folder2/plik.txt #funkcja ta wstawia \ / zależnie od systemu
os.remove(sciezka_do_pliku) #usuwamy plik
import datetime
(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(sciezka_do_pliku)
data_modyfikacji = datetime.datetime.fromtimestamp(mtime)
print(data_modyfikacji)
## 2018-12-21 10:46:54

Poprawianie wydajności

Najczęściej, aby poprawić wydajność aplikacji pisanych w pythonie trzeba się skoncentrować na pełniejszym wykorzystaniu mocy procesora. Jest na to kilka sposobów.

Wielowątkowość

Wielowątkowość w pythonie jest nieco śliskim tematem, ponieważ w najpopularniejszej implementacji pythona (CPython) mamy do czynienie z mechanizmem GIL, który uniemożliwia pracę wielu wątkom jednocześnie. dłuższy artykuł
Z tego powodu domyślnie wątki są dobrym pomysłem w wypadku operacji IO, czy też innych zadaniach, które działając w tle nie konsumują czasu procesora.
Jeśli zaś chcemy w naszej pracy wykorzystać wiele rdzeni procesora równocześnie to warto użyć biblioteki multiprocessing

import threading

def foo(argument):
  print("Hello threading! with argument: ", argument)

my_thread = threading.Thread(target=foo, args = ("slowo"))

my_thread.start()
#>> Hello threading with argument: slowo
# kolejne uruchomienie za pomocą start rzuci nam RuntimeError

Przy dłuższym czasie wykonywania możemy poczekać na wątki za pomocą join

my_thread.join()

lub

my_thread.join(timeout=10)

Wieloprocesowość

Biblioteka multiprocessing opiera się na obrabianiu danych w ramach różnych procesów, dzięki czemu każdy proces ma własnego GIL-a, który nie wchodzi w drogę innym procesom. Poza tym jest w obsłudze dość podobna do wątków. (niby są takie metody w bibliotece jak os.fork(), ale są one dość niskopoziomowe i niezbyt przenośne)

from multiprocessing import Process
import os

def work(id):
    print(f'this is process {os.getpid()} called by {id}')


def main():
    procesy = [Process(target=work, args=(number, )) for number in range(5)]
    for proces in procesy:
        proces.start()

    while procesy:
        procesy.pop().join()

#> this is process 10749 called by 0
#> this is process 10750 called by 1
#> this is process 10751 called by 2
#> this is process 10753 called by 4
#> this is process 10752 called by 3

Komunikacja międzyprocesowa

W tym wypadku nie mamy już wspólnej pamięci z której możemy korzystać.
Dlatego stworzone są następujące mechanizmy:

  • multiprocessing.Queque - prawie to samo co zwykła kolejka (queque.Queque. Praktycznie nie ma różnic w użyciu.
  • multiprocessing.Pipe - mechanizm nieco zbliżony do gniazd sieciowych
  • multiprocessing.sharedtypes - pozwala stworzyć typy z C we wspólnej puli pamięci międzyprocesowej

Do łatwego zarządzani pulą dostępnych procesów mamy obiekt Pool. Pozwala ustalić liczbę procesów, które możemy stworzyć jednocześnie. Dla wygody pracy i łatwego przełączania się między procesami i wątkami przy użyciu tego API mamy paczkę multiprocessing.dummy.Pool (te same interfejsy, tylko, że działa na wątkach).

Pipe

Pipe - jest to w pełni dupleksowe połączenie działające podobnie do gniazd (patrz socket module ) z tą różnicą, że pozwala on na wysyłanie nie tylko ciągów bajtów, lecz także serializowalne obiekty (patrz pickle ).

Konstruktor tworzy dwa obiekty do odbioru i nadawania, które możemy przekazać do naszego procesu.

from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())   # prints "[42, None, 'hello']"
    p.join()

Sharedtypes

Mamy tutaj klasy Value i Array, warto tu pamiętać o tym, że aby zabezpieczyć się przed równoczesnym pisaniem i czytaniem trzeba używać dołączonych mutexów (Lock, RLock, Semaphore).

class Point(Structure):
    _fields_ = [('x', c_double), ('y', c_double)]

def modify(n, x, s, A):
    n.value **= 2
    x.value **= 2
    s.value = s.value.upper()
    for a in A:
        a.x **= 2
        a.y **= 2

if __name__ == '__main__':
    lock = Lock()

    n = Value('i', 7)
    x = Value(c_double, 1.0/3.0, lock=False)
    s = Array('c', b'hello world', lock=lock)
    A = Array(Point, [(1.875,-6.25), (-5.75,2.0), (2.375,9.5)], lock=lock)

    p = Process(target=modify, args=(n, x, s, A))
    p.start()
    p.join()

    print(n.value)
    print(x.value)
    print(s.value)
    print([(a.x, a.y) for a in A])

#> 49
#> 0.1111111111111111
#> HELLO WORLD
#> [(3.515625, 39.0625), (33.0625, 4.0), (5.640625, 90.25)]

Asynchroniczność

Wykorzystuje ona korutyny (coroutines).

Biblioteką wykorzystywaną do asynchroniczności jest asyncio. Tutoriale pythondocs, realpython.

>>> import asyncio

>>> async def main():
...     print('hello')
...     await asyncio.sleep(1) #w tym momencie oddajemy kontrolę na sekundę
                               #w tym czasie CPU może popracować nad czymś innym
...     print('world')

>>> asyncio.run(main())
hello
world

Słowa kluczowe:

  • async i async with - służą do oznaczania funkcji, które są korutynami
  • await - służy do oznaczania momentu w którym musimy poczekać na wykonanie jakiejś korutyny. W tym momencie przekazujemy kontrolę do pętli zdarzeń.
async def g():
    # Pause here and come back to g() when f() is ready
    r = await f()
    return r
async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

TODO ukończyć ten rozdział

Rzeczy na które należy uważać⚠️

Różne cechy oraz właściwości pythona na które trzeba uważać podczas pisania w pythonie zwłaszcza, gdy nie jest on pierwszym językiem.

Wszystko jest referencją

Warto pamiętać, że w pythonie prawie wszystko jest referencją.
Np przekazując coś do funkcji przekazujemy tan naprawdę referencję do tego obiektu, z tego powodu wewnątrz funkcji nasz obiekt może ulec zmianie.

Warto tutaj zapoznać się z pojęciem mutowalności.
Obiekty niemutowalne to takie, których nowe instancje są tworzone podczas zmiany wartości.

def fun(x):
    print(id(x)) #id() wypisuje identyfikator danej zmiennej
    x+=1
    print(id(x))

num=32
print(id(num))
fun(num)

>> 9789984
>> 9789984 # Widać tutaj, że funkcja operuje na tej samej instancji liczby
>> 9790016 # po zmianie wartości do identyfikatora x jest już przypisany inny obiekt

Z tego powodu przekazywanie mutowalnej wartości do funkcji można traktować jako przekazanie kopii, a przekazanie niemutowalnej jako przekazanie referencji lub wskaźnika (w języku C++).

Niemutowalne Mutowalne
int lista
float słownik
complex bytearray
bool obiekty użytkownika
string
tuple
range
frozenset
bytes

Przykłady działania:

def fun(num):
    num = num+10
    print(num)

liczba = 1
fun(liczba)
# 10
print(liczba)
#1
class Numer:
    def __init__(self,num):
        self.num = num

n = Numer(1)

def fun2(numer):
    numer.num = numer.num+10
    print(numer.num)

print(n.num)
# 1
fun2(n)
# 11
print(n.num)
# 11

Jeśli chcemy tutaj zapobiec takim problemom warto zastanowić się nad użyciem modułu copy

import copy

x = copy.copy(y)        # płytka kopia
x = copy.deepcopy(y)    # kopia głęboka rekursywnie kopiująca wszystkie elementy naszej klasy

Inicjalizowanie zmiennych

Skoro wszystko jest referencją to trzeba też o tym pamiętać przy podawaniu domyślnych wartości dla funkcji.
link

from dataclasses import dataclass

@dataclass
class Node(object):
    num: int
    children: list
    def __init__(self, num, children=[]):
        self.num = num
        self.children = children


n1 = Node(1)
n2 = Node(2)
print(n1)
# Node(num=1, children=[])
print(n2)
# Node(num=2, children=[])

n1.children.append(3)
print(n1)
#Node(num=1, children=[3])
print(n2)
#Node(num=2, children=[3])

Dlatego też wielu uważa, że lepiej dać None jako domyślną wartość i inicjalizować to dopiero, wewnątrz konstruktora.

//TODO lista: mixin, importowanie, biblioteka sys, instance methods // yield, operator := // from future import annotations (ewaluacja definicji z kodu, które pojawiają się później)