Zasięgi
Zasięg (ang. scope) określa, gdzie dana zmienna lub funkcja jest widoczna i dostępna. Zasady te pomagają w organizacji kodu oraz uniknięciu błędów wynikających z konfliktów nazw.
Główne zasady zarządzania zasięgiem opierają się na regule LEGB oraz specjalnych instrukcjach global
i nonlocal
.
Przypisane nazwy są lokalne
Kiedy przypisujemy wartość zmiennej w funkcji, staje się ona lokalna dla tej funkcji. Oznacza to, że poza tą funkcją nie będzie widoczna.
def funkcja():
x = 10 # Zmienna lokalna, widoczna tylko w funkcji
print(x)
funkcja() # Wyświetli 10
# Natomiast taka operacja nie ma już sensu, zmienna x
# istnieje tylko wewnatrz funkcji
print(x)
Reguła LEGB
Reguła ta określa w jakiej kolejności Python wyszukuje zmienne i funkcje, gdy napotka na nie w kodzie. Jest to ważne, ponieważ pozwala programiście zrozumieć, skąd Python pobiera wartość zmiennej, zwłaszcza gdy zmienne o tej samej nazwie występują w różnych zakresach (np. wewnątrz funkcji, w funkcjach zagnieżdżonych, w skrypcie globalnym).
LEGB to akronim od 4 typów zasięgów, przez które Python przechodzi w następującej kolejności:
- Local - zmienne zadeklarowane w bieżącej funkcji lub wewnętrznej funkcji (najbardziej wewnętrzny zasięg).
- Enclosing - zmienne zadeklarowane w zewnętrznej funkcji, jeśli funkcje są zagnieżdżone.
- Global - zmienne deklarowane w skrypcie głównym, poza funkcjami, czyli na poziomie modułu.
- Built-in - zmienne i funkcje wbudowane, dostępne globalnie (np. print, len).
# W danym skrypcie / w danym module x jest
# zmienną globalną
x = "global"
def zewnetrzna():
x = "enclosing"
# Tak, funkcje mogą być zagnieżdżone
def wewnetrzna():
x = "local"
print(x) # Wydrukuje "local", bo najpierw przeszukuje lokalne zmienne
wewnetrzna()
print(x) # Wydrukuje "enclosing"
zewnetrzna()
print(x) # Wydrukuje "global"
Instrukcje global
i nonlocal
global
- pozwala zadeklarować zmienną globalną w funkcji, zmieniając jej wartość globalnie.nonlocal
- umożliwia przypisanie wartości do zmiennej z otaczającego zakresu (nie globalnego), co przydaje się w funkcjach zagnieżdżonych.
Przykład użycia global
:
y = 5
def zmien_globalne():
global y
y = 10
zmien_globalne()
print(y) # Wydrukuje 10
# Gdyby nie zastosowanie instrukcji global to y byłoby
# dalej równe 5, bo równe 10 byłoby jedynie lokalnie w funkcji
Przykład użycia nonlocal
:
def zewnetrzna():
x = "original"
def wewnetrzna():
nonlocal x
x = "modified"
wewnetrzna()
print(x) # Wydrukuje "modified"
# Pomimo tego, że x zostało zmodyfikowane jedynie
# wewnątrz funkcji zagnieżdżonej
zewnetrzna()
Funkcje zagnieżdżone, funkcje fabrykujące
Funkcje zagnieżdżone (ang. nested functions) to po prostu funkcje zdefiniowanie wewnątrz innych funkcji. Mogą być także wywoływane wewnątrz, ale nie tylko, bo mogą być również zwracane, używane jako argument czy być prywatnymi funkcjami pomocniczymi.
Przykłady dla funkcji zagnieżdżonych
Wywołanie funkcji wewnątrz:
def zewnetrzna(x):
def wewnetrzna(y):
return x + y
# Funkcja wewnetrzna jest dostępna tylko w zewnetrzna
return wewnetrzna(x)
print(zewnetrzna(5)) # Wydrukuje 10
print(wewnetrzna(3)) # Spowoduje błąd, bo wewnetrzna nie jest dostępna poza zewnetrzna
Użycie funkcji zagnieżdżonej jako funkcji fabrykującej:
def funkcja_fabrykujaca(wiadomosc):
def funkcja_zagniezdzona():
return f"Twoja wiadomość to: {wiadomosc}"
return funkcja_zagniezdzona
# Tworzymy nową funkcję, która pamięta "wiadomosc"
nowa_funkcja = funkcja_fabrykujaca("Witaj, świecie!")
print(nowa_funkcja()) # Wydrukuje "Twoja wiadomość to: Witaj, świecie!"
Przekazanie funkcji zagnieżdżonej jako argument:
def obliczenia(operator):
def dodaj(x, y):
return x + y
def mnoz(x, y):
return x * y
# Zwracamy funkcję zagnieżdżoną w zależności od operatora
if operator == "dodaj":
return dodaj
elif operator == "mnoz":
return mnoz
else:
raise ValueError("Nieznany operator")
# Teraz mamy funkcję dodaj i mnoz dostępną spoza obliczenia
wybrana_funkcja = obliczenia("dodaj")
print(wybrana_funkcja(2, 3)) # Wydrukuje 5
wybrana_funkcja = obliczenia("mnoz")
print(wybrana_funkcja(2, 3)) # Wydrukuje 6
Tworzenie prywatnych funkcji pomocniczych:
def przetwarzaj_dane(lista):
def filtruj(x):
return x % 2 == 0
def kwadrat(x):
return x ** 2
przefiltrowane = filter(filtruj, lista)
return [kwadrat(x) for x in przefiltrowane]
print(przetwarzaj_dane([1, 2, 3, 4, 5, 6])) # Wydrukuje [4, 16, 36]
Funkcje fabrykujące (ang. factory functions) są więc jednym z typów funkcji zagnieżdżonych, gdzie zwracana jest funkcja wewnętrzna. Funkcje fabrykujące wykorzystują zamknięcia, co oznacza, że funkcje, które zwracają, pamiętają kontekst, w którym zostały stworzone (np. wartości zmiennych z zakresu otaczającego). To pozwala im zachować stan, co czyni je bardzo przydatnymi do tworzenia funkcji z predefiniowanymi ustawieniami lub konfiguracjami.
Przykłady dla funkcji zagnieżdżonych
Predefiniowanie parametrów:
def mnoznik(factor):
def pomnoz(x):
return x * factor
return pomnoz # Zwracamy nową funkcję
# Tworzymy nową funkcję z zapamiętanym "factor"
pomnoz_2 = mnoznik(2)
pomnoz_3 = mnoznik(3)
print(pomnoz_2(5)) # Wydrukuje 10 (5 * 2)
print(pomnoz_3(5)) # Wydrukuje 15 (5 * 3)
Liczniki:
def licznik():
count = 0
def zwieksz():
nonlocal count
count += 1
return count
return zwieksz
licznik1 = licznik()
licznik2 = licznik()
print(licznik1()) # Wydrukuje 1
print(licznik1()) # Wydrukuje 2
print(licznik2()) # Wydrukuje 1 (nowy licznik)
Konfiguracje w ramach wzorca fabrykującego:
def formatuj_text(style):
def formatuj(wiadomosc):
if style == "uppercase":
return wiadomosc.upper()
elif style == "lowercase":
return wiadomosc.lower()
elif style == "capitalize":
return wiadomosc.capitalize()
else:
return wiadomosc
return formatuj
uppercase_formatter = formatuj_text("uppercase")
lowercase_formatter = formatuj_text("lowercase")
print(uppercase_formatter("hello world")) # Wydrukuje "HELLO WORLD"
print(lowercase_formatter("HELLO WORLD")) # Wydrukuje "hello world"
Walidatory:
def walidator_dlugosci(min_dlugosc, max_dlugosc):
def waliduj(text):
return min_dlugosc <= len(text) <= max_dlugosc
return waliduj
maly_tekst = walidator_dlugosci(1, 5)
duzy_tekst = walidator_dlugosci(5, 10)
print(maly_tekst("Hi")) # Wydrukuje True
print(maly_tekst("HelloWorld")) # Wydrukuje False
print(duzy_tekst("Python")) # Wydrukuje True
Funkcje złożonych obliczeń:
def procent_calculator(procent):
def oblicz(calkowita_kwota):
return calkowita_kwota * (procent / 100)
return oblicz
podatek_vat = procent_calculator(23)
zysk_kapitalowy = procent_calculator(19)
print(podatek_vat(1000)) # Wydrukuje 230.0
print(zysk_kapitalowy(1000)) # Wydrukuje 190.0
Złożone filtry i sortowania:
def filtruj_po_min_wartosci(min_wartosc):
def filtruj(lista):
return [x for x in lista if x >= min_wartosc]
return filtruj
filtruj_od_10 = filtruj_po_min_wartosci(10)
print(filtruj_od_10([5, 10, 15, 20])) # Wydrukuje [10, 15, 20]
Zadania
- Napisz funkcję fabrykującą
stworz_funkcje_potegujaca(wykladnik)
, która przyjmuje jeden argument: wykładnik potęgi. Zwracana przez nią funkcja zagnieżdżonapoteguj(podstawa)
powinna również przyjmować jeden argument – podstawę potęgi – i zwracać wynik podniesienia tej podstawy do potęgi określonej przez wykładnik przekazany do funkcji zewnętrznej.
Tip
Wywołanie takiej funkcji i sprawdzenie powinno wyglądać następująco:
potega_2 = stworz_funkcje_potegujaca(2)` # Tworzy funkcję potęgującą do kwadratu
print(potega_2(4)) # Wynik: 16
Zachowanie stanu
Jest istotnym elementem programowania, szczególnie w przypadku potrzeby utrzymania danych pomiędzy kolejnymi wywołaniami funkcji lub fragmentami kodu.
Jest to też dobry przypadek użycia dla zagadnień poznanych na dzisiejszych zajęciach.
Za przykład posłuży nam accumulator, który zbierać będzie liczby dodawane w kolejnych wywołaniach funkcji. Po każdym dodaniu chcemy mieć możliwość pobrania aktualnej sumy. Poniżej rożne przykłady implementacyjne:
Użycie zmiennej nonlocal
def akumulator_nonlocal():
suma = 0
def dodaj_wartosc(wartosc):
nonlocal suma
suma += wartosc
return suma
return dodaj_wartosc
# Przykład użycia
akum = akumulator_nonlocal()
print(akum(5)) # Wydrukuje 5
print(akum(10)) # Wydrukuje 15
Użycie zmiennej global
Zmienne global
pozwalają na dostęp i modyfikację zmiennych globalnych wewnątrz funkcji. To podejście jest rzadziej stosowane, gdyż zmienne globalne są widoczne w całym module, co może prowadzić do błędów, ale w niektórych sytuacjach może się przydać.
suma_global = 0
def dodaj_do_akumulatora(wartosc):
global suma_global
suma_global += wartosc
return suma_global
# Przykład użycia
print(dodaj_do_akumulatora(5)) # Wydrukuje 5
print(dodaj_do_akumulatora(10)) # Wydrukuje 15
Użycie klasy
Co prawda klasy będą dopiero na przyszłych zajęciach, ale są jednym z najczęstszych sposobów zarządzania stanem. Instancje klas pozwalają na łatwe tworzenie i przechowywanie danych w atrybutach.
class Accumulator:
def __init__(self):
self.suma = 0
def dodaj(self, wartosc):
self.suma += wartosc
return self.suma
# Przykład użycia
akum = Accumulator()
print(akum.dodaj(5)) # Wydrukuje 5
print(akum.dodaj(10)) # Wydrukuje 15
Użycie atrybutu funkcji
def akumulator_funkcyjny(wartosc):
akumulator_funkcyjny.suma += wartosc
return akumulator_funkcyjny.suma
# Inicjalizacja atrybutu funkcji
akumulator_funkcyjny.suma = 0
# Przykład użycia
print(akumulator_funkcyjny(5)) # Wydrukuje 5
print(akumulator_funkcyjny(10)) # Wydrukuje 15
Zadania
-
Napisz funkcję
licznik()
, która za każdym razem, gdy jest wywoływana, zwiększa swoje wewnętrzne liczenie o jeden (licznik stanu). Zaimplementuj cztery wersje tej funkcji, wykorzystując:-
Zmienną
nonlocal
w zagnieżdżonej funkcji -
Zmienną
global
. -
Klasę z atrybutem instancji - wskazówka: zaimplementowanie w klasie funkcji
__init__
oraz__call__
-
Atrybut funkcji - funkcja, jak każdy inny obiekt, może mieć swoje atrybuty
-