Adatszerkezetek#

Az adatszerekezetek vagy adatstruktúrák adattípusok tárolását teszik lehetővé. Az adatszerkezet is egy típus, így egy adatszerkezet egy vagy több további adatszerkezetet is tárolhat. Az adatszerkezetek a következő alapműveleteket mindenképpen lehetővé teszik:

  • az adatszerkezet elemeihez való hozzáférés,

  • új adatot hozzáadása az adatszerkezethez (beszúrás),

  • és onnan elemet vagy elemeket eltávolítása (törlés).

További műveletek is természetesen lehetésgesek, például keresés vagy rendezés az adatszerkezetben. Az adatszerkezetek abban különböznek, hogy a különböző típusú műveleteket, milyen komplexitású algorimussal valósítják meg. Megfelelő adattípus választása egy adott probléma esetén a program tervezés egy fontos eleme. Az adatszerkezetek, a hozzáférést, beszúrás és törlést különböző komlpexitás mellett biztosítják. Komplexitáson itt a művelet végrehajtásához szükséges futási idő és memória igényt értünk első sorban.

A Python nyelv számos alapértelmezett adatszerkezetet támogat; ezek közül a listát, szótárat és vektort ismertetjük.

Lista (list)#

A lista különböző elemeket tartalmazó gyüjtemény. Melynek elemeit az elemhez tartozó indexszel érhetjük el. A lista különböző indexeknél különböző típusokkal térhet vissza: a lista nem csak egy adott adattípusot tartalmazhat.

Kiegészítő anyag

A lista egy egész számú indexszhez megadja az indexszen tárolt értéket. Matematikailg ez magadható a következő leképezéssel:

\(\mathbb{Z} \rightarrow \mathbb{D}\),

ahol \(\mathbb{D}\) az összes adattípus halmaza.

Lista létrehozása#

Nézzünk egy példát egy lista típusú változó létrehozására:

my_list = [1, 2, 3, 4, 5]
print(my_list)
print(type(my_list))
[1, 2, 3, 4, 5]
<class 'list'>

Feladat

Vizsgáljuk meg a lista létrehozásának szintaxisát. Azonosítsuk a következőeket:

  • Milyen karakterekkel jelőljük hogy egy listát hozzunk létre?

  • Milyen módon soroljuk fel a lista elemeit?

  • Mi a lista típus neve a Pythonban (type eredménye)?

A lista adatszerkezet a más nyelvekből megszokott tömb típusnak feleltethető meg, azzal a fontos különbséggel, hogy a lista bármilyen adatípust, bármilyen kombinációban tud tárolni:

my_list = ["I", "am", 22, "! That is ", True]
print(my_list)
['I', 'am', 22, '! That is ', True]

Mivel a list bármit tartalmazhat ezért egy másik listát is:

list_1 = ["1", 2, "3", 4]
list_2 = ["2nd list", "contains 1st", list_1]
print(list_2)
['2nd list', 'contains 1st', ['1', 2, '3', 4]]

Az üres lista egy olyan list, mely nem tartalmaz egyetlen elemet:

empty_list = []
print(empty_list)
[]

Műveletek listán#

A lista hossza a listát tartalmazó elemek száma, melyet a len függvény segítségével kaphatunk meg:

n = len(list_2)
print(n)
3

Az üres lista hossza 0:

empty_list = []
print(len(empty_list))
0

A list egyik elemét indexeléssel érhetjük el:

fun_str = ["Programming", "is", "fun", "!"]
print(fun_str[0])    # lista első eleme, vegyük észre 0-tól indexelünk
print(fun_str[1])    # lista második eleme
Programming
is

Figyelem

Vegyük észre, hogy a Pythonban az indexszelés 0-tól kezdődik.

Lehetőség az utolsó elemtől visszafele történő indexelésre, amennyibe negatív indexszet adunk meg:

fun_str = ["Programming", "is", "fun", "!"]
print(fun_str[-1])   # lista utolsó eleme
print(fun_str[-2])   # lista utolsó előtti eleme
!
fun

A fenti kód kifejezhető a len függvény segítségével következő módon:

fun_str = ["Programming", "is", "fun", "!"]
print(fun_str[len(fun_str)-1])
print(fun_str[len(fun_str)-2])
!
fun

A fenti kifejezés teljesen ekvivalens a negatív indexszeléssel, ezért a negatív indexszelés egyszerűsíti a kódot. Az ilyen nyelvi megoldásokat az angol irodalom “syntax sugar”-nek, vagy magyarul szinatkszis cukornak nevezni.

Matlabhoz hasonlóan használhatunk tartományt is a lista egyik részhalmazának lekérdezéséhez; ezt slicing-nak vagy szeletelésnek hívjuk. A tartományt a : operátor segítéségvel definiálhatjuk:

fun_str = ["Programming", "is", "fun", "!"]
print(fun_str[0:2])   # 0 és 2 indeszek közötti elemek (a 2-es indexű elem nem tartozik bele)
print(fun_str[1:3])   # 1 és 3 indeszek közötti elemek (a 2-es indexű elem nem tartozik bele)
print(fun_str[1:])    # elemek az egyes indexű elemtől az utolsó elemig
print(fun_str[0:-1])  # elemek 0-tól az utolsó indeszig (az utolsó elem nem tartozik bele)
print(fun_str[:-1])   # ha nem adunk meg kezdő ideszeket, akkor az automatikusan 0. eredmény ugyanaz mint az előző sorban`
['Programming', 'is']
['is', 'fun']
['is', 'fun', '!']
['Programming', 'is', 'fun']
['Programming', 'is', 'fun']

Figyelem

Figyeljünk rá, hogy az a:b tartományba a b indexű elem nem tartozik bele, így a matematikában megszokott jelölésmóddal a tartomány: [a, b). Ez a konvenció azonos a range függvénynél ismertettel.

Lehetőség bizonyos lépésközzel végighaladni a tartományon:

fun_str = ["Programming", "Programming", "is", "is", "always", "not", "fun", "!", "!"]
print(fun_str[0:-1:2])   # minden második elem a 0 és az utolsó indeszek közötti (az utolsó elem nem tartozik bele)
print(fun_str[0::2])     # minden második elem a 0 és az utolsó indeszek közötti (az utolsó elem beletartozik)
print(fun_str[::2])      # ha nem adunk meg kezdő ideszeket, akkor az automatikusan 0. eredmény ugyanaz mint az előző sorban
print(fun_str[::3])      # minden harmadik elem a 0 és az utolsó indeszek között (az utolsó elem beletartozik)
['Programming', 'is', 'always', 'fun']
['Programming', 'is', 'always', 'fun', '!']
['Programming', 'is', 'always', 'fun', '!']
['Programming', 'is', 'fun']

Az indeszek a slicing kifejezésben lehetnek változók is:

fun_str = ["Programming", "is", "fun", "!"]
from_idx = 0
to_idx = len(fun_str)
print(fun_str[from_idx:to_idx])
['Programming', 'is', 'fun', '!']

append paranccsal új elemet tudunk hozzáfűzni a listához:

fruits = ["apple", "banana", "cherry"]
fruits.append("orange")
print(fruits)
['apple', 'banana', 'cherry', 'orange']

Figyelem

Vegyük észre, hogy az append függvény a listát helyben (magát a lista tartalmát) változtatja meg. Magának az append függvénynek nincs visszatérési értéke, ezért az alábbi kód None-t for kiírni:

fruits = ["apple", "banana", "cherry"]
fruits = fruits.append("orange")
print(fruits) # Eredmény: None

extend paranccsal két listát tudunk összefűzni:

fruits = ["apple", "banana", "cherry"]
fruits.extend(["orange", "lime"])
print(fruits)
['apple', 'banana', 'cherry', 'orange', 'lime']

Ahogy korábban láttuk, az összeadás műveletét általánosíthatjuk, és nem csak számok között értelmezhetjük. Példát a szövegek esetén láttunk egy példát, ahol az összeadás két szöveg összefűzését (konkatenációját) jelentette. Hasonlóan, itt is, két lista között is értelmezhetjük az összeadást; ekkor azok összefűzését jelenti. Ez egy egyszerűsítés (syntax sugar) az extend függvény használata helyett:

fruits = ["apple", "banana", "cherry"] + ["orange", "lime"]
print(fruits)
['apple', 'banana', 'cherry', 'orange', 'lime']

További az összeadással össefűzött elemek egy új listába kerülnek, és az eredeti listák nem változnak meg:

fruits = "apple", "banana", "cherry"
veggies = "carrot", "broccoli", 

healthy = fruits + veggies

print(fruits)
print(veggies)
print(healthy)
('apple', 'banana', 'cherry')
('carrot', 'broccoli')
('apple', 'banana', 'cherry', 'carrot', 'broccoli')

Feladat

Mit ír ki a következő program?

fruits = ["apple", "banana", "cherry"]
veggies = ["carrot", "broccoli"]

healthy = fruits.extend(veggies)

print(fruits)
print(veggies)
print(healthy)

Vigyázzunk arra, hogy ne keverjük az append és extend parancsokat:

fruits = ["apple", "banana", "cherry"]
fruits.extend("orange")
print(fruits)
['apple', 'banana', 'cherry', 'o', 'r', 'a', 'n', 'g', 'e']

Feladat

Próbáljuk megmagyarázni mi történik a fenti kódban! Gondoljunk vissza az automatikus típuskonverzióra, illetve hogy a hogyan konvertálódna egy szöveg listává!

Egy példa arra amikor valószínűsíthetően az append függvény van használva extend helyett:

fruits = ["apple", "banana", "cherry"]
fruits.append(["orange"])
print(fruits)
['apple', 'banana', 'cherry', ['orange']]

Lista logikai kifejezésben#

Nem üres list True értékként értelmezett:

example_list = ['a', 'b']
if example_list:
   print("True")
else:
  print("False")
True

… és ennek fordítottja; üres lista hamisként értelmezett:

example_list = []
if example_list:
   print("True")
else:
  print("False")
False

Feladat

Mi az eredménye az alábbi kódnak?

example_list = [False]
if example_list:
    print("True")
else:
    print("False")

Szótár (dict)#

A szótár, vagy más néven asszociatív tömb, különböző elemeket tartalmazó gyüjtemény, melynek elemeit az elemhez tartozó kulccsal érhetjük el. A szótár így kulcs-érték párokat tartalmaz. A szótár különböző kulcsoknál különböző típusokot adhat: az érték adattípusai kulcsonként változhatnak. Megkötés viszont, hogy a kulcsoknak megfelelő adattípusúaknak kell lennie: mi egyelőre azzal a szabállyal élünk, hogy egyszerű típusnak kell lennie.

Szótár létrehozása#

Egy példa szótár létrehozására, ahol a kulcs szöveg:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}
print(car)
print(type(car))
{'brand': 'Ford', 'model': 'Mustang', 'year': 1964}
<class 'dict'>

A szótár tehát hasonlóképpen működik mint a hagyományos szótárak. Például egy angol-magyar szótár esetén, egy adott angol szóhoz megkapható a magyar megfelelője.

Feladat

Vizsgáljuk meg a szótár létrehozásának szintaxisát. Azonosítsuk a következőeket:

  • Milyen karakterekkel jelőljük hogy egy listát hozzunk létre?

  • Milyen módon adjuk meg a kulcsot?

  • Milyen módon adjuk meg a kulcshoz tartozó értéket?

  • Mi a szótár típus neve a Pythonban (type eredménye)?

Többszörös kulcs esetén az utolsó előfordulás lesz használva:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
  "year": 2020
}
print(car)
{'brand': 'Ford', 'model': 'Mustang', 'year': 2020}

Nem csak szöveg, hanem más típus is szerepelhet kulcsként:

numbers = {
  0: "nulla",
  1: "egy",
  2: "kettő",
}
print(numbers)
{0: 'nulla', 1: 'egy', 2: 'kettő'}

A kulcsoknak nem kell azonos típusúaknak lennie (de úgynevezett hashable típusúnak kell lennie, mellyel most nem foglalkozunk, és elfogadjuk, hogy egyszerű típusnak kell lennie):

numbers = {
  0: "nulla",
  "one": "egy",
  2: "kettő",
}
print(numbers)
{0: 'nulla', 'one': 'egy', 2: 'kettő'}

Végül nézzük meg az üres szótárat, melynek nincs egyetlen kulcs-érték párja sem:

empty_dict = {}
print(empty_dict)
{}

Műveletek szótáron#

Elemhez való hozzáférés a kulcson keresztül lehetséges a [ és ] használatával hasonlóan a lista szintaxisához:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}
print(car['brand'])
Ford

Az elem hozzáférése után az értéket is módosíthatjuk:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}

car["model"] = "Focus"
print(car)
{'brand': 'Ford', 'model': 'Focus', 'year': 1964}

Kiegészítő anyag

A list esetén a matemtaikai leképzést a következő alakban adtuk meg korábban:

\(\mathbb{Z} \rightarrow \mathbb{D}\).

A szótár esetén az index szerepét, mely csak egész számot vehet fel, a szöveg veheti át, így a szótár a következő leképezéssel megadható:

\(\mathbb{S} \rightarrow \mathbb{D}\),

ahol \(\mathbb{S}\) a szövegek halmaza. Valójában bármilyen típus szerepelhet kulcsként, így pontosabb az alábbi kifejezés:

\(\mathbb{H} \rightarrow \mathbb{D}\),

ahol \(\mathbb{H} \subset \mathbb{D}\) úgynevezett hashable adattípusok halmaza. Tehát a szótárra a lista általánosításaként is tekinthetünk. Bizonyítandó, az alábbi kódban egész számokat használunk kulcsként, majd egy elemére hivatkozunk. Vegyük észre, hogy az elem elérése szintaktikailag ugyanúgy nézz ki mint a lista esetén:

car = {
  0: "Ford",
  1: "Mustang",
  2: 1964,
}
print(car[0])

Új elemet a szótárhoz, vagyis új kulcs-érték párt, az új kulcsra való hivatkozással tudjuk megtenni:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}
car['owner'] = "Anna"
print(car)
{'brand': 'Ford', 'model': 'Mustang', 'year': 1964, 'owner': 'Anna'}

Ez azt jelenti, hogy új szótárat szintaktikailag üres szótárból is feltölthetünk. Például az alábbi kód ugyanazt a szótárt hozza létre, mint feljebb:

car = {}
car["brand"] = "Ford"
car["model"] = "Mustang"
car["year"] = "1964"
car["owner"] = "Anna"

print(car)
{'brand': 'Ford', 'model': 'Mustang', 'year': '1964', 'owner': 'Anna'}

A szótár típuson lévő pop függvény segítségével törölhető egy kulcs-érték pár:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}

car.pop('year')
print(car)
{'brand': 'Ford', 'model': 'Mustang'}

A fenti példában a year kulcsot töröltük.

Figyelem

Ha nem létező elemet próbálunk törölni hibát kapunk, például:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}

car.pop('owner')

Szótár logikai kifejezésben#

Nem üres szótár True értéként értelmezett:

example_list = {'a': 1, 'b': 2}
if example_list:
   print("True")
else:
  print("False")
True

Üres szótár pedig hamis:

example_list = {}
if example_list:
   print("True")
else:
  print("False")
False

Listák és szótárak egymásba ágyazva#

Mivel a szótár egy típus szerepelhet mint a lista egy eleme. Nézzük meg az alábbi példát, ahol egy listába tettünk két szótárat. A lista elemére hivatkozva a megfelelő szótárt kapjuk vissza:

cars = [{
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}, {
  "brand": "Ford",
  "model": "Focus",
  "year": 2018,
}]

print(cars)    # a list kiírása
print(cars[0]) # a lista első szótárának kiírása
print(cars[1]) # a lista második szótárának kiírása
[{'brand': 'Ford', 'model': 'Mustang', 'year': 1964}, {'brand': 'Ford', 'model': 'Focus', 'year': 2018}]
{'brand': 'Ford', 'model': 'Mustang', 'year': 1964}
{'brand': 'Ford', 'model': 'Focus', 'year': 2018}

Kérdés, hogy hogyan tudunk a listában lévő szótár egyik kulcsára hivatkozni. Először lekérdezhejtük a szótárat, és beletehetjük azt egy változóba. Ezt a változót használhatjuk ezután, hogy a szótár egyik kulcsára hivatkozzunk:

cars = [{
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}, {
  "brand": "Ford",
  "model": "Focus",
  "year": 2018,
}]

mustang = cars[0]

print(mustang["year"])
1964

A kód azonban egyszerűsíthető: a mustang változó elhagyható az alábbi módon:

cars = [{
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
}, {
  "brand": "Ford",
  "model": "Focus",
  "year": 2018,
}]

print(cars[0]["year"])
1964

A fenti kód utolsó sorának cars[0]["year"] kifejezésében a következő történik:

  1. cars[0] visszaadja a lista első szótárát,

  2. ezután ezen a szótáron a year kulcsot lekérdezzük,

  3. és az eredmény kiírásra kerül.

Úgy is elképzelhetjük, hogy a cars[0] eredménye egy ideiglenes változóba kerül, melyre a ["year"] hivatkozik.

Mivel a lista is egy típus, ezért lehet érték egy szótárban, például:

car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964,
  "exam": [1992, 1998, 2005, 2009]
}

print(car)
{'brand': 'Ford', 'model': 'Mustang', 'year': 1964, 'exam': [1992, 1998, 2005, 2009]}

Hasonlóan a fentiekhez, a car szótár exam kulcsához tartozó lista második elemének a lekérdezése megfogalmazható a következőképpen:

print(car["exam"][1])
1998

Feladat

Az alábbi kódrészletben a students lista három szótárat tartalmaz. A szótárak a tanulók neveit a name kulcsban, valamint a zh-n elért eredményeiket a tests kulcsban tárolja. A tests kulcshoz tartozó lista a zh-k érdemjegyei a zh-k sorrendjében. Adjunk kódot ami választ ad a következő kérdésekre:

  • Mi a students lista első szótárában található diák neve?

  • Mi a students lista utolsó szótárában található diák neve?

  • Mi Béla második zh-jának az eredménye?

  • Mi Anna utolsó zh-jának az eredménye?

students = [
    {"name": "Anna", "tests": [4, 4, 5]},
    {"name": "Béla", "tests": [3, 2, 4]},
    {"name": "Cecíllia", "tests": [3, 2, 5]}
]

Vektor (tuple)#

A tuple véges elemű rendezett elemek listája. Nagyon hasonló a listához, a legfontosabb különbség, hogy a tuple elemei nem változtathatóak meg. Így ez az adatszerkezet valamilyen elemek statikus gyüjteménye. Akkor használatos, amikor előre ismert hány és milyen elemet akarunk tárolni; ilyen eset például amikor egy függvénynek több visszatérési értéke van.

tuple létrehozása#

Példa egy tuple létrehozására:

fruits = ("apple", "banana", "cherry")
print(type(fruits))
<class 'tuple'>

Feladat

Vizsgáljuk meg a tuple létrehozásának szintaxisát. Azonosítsuk a következőeket:

  • Milyen karakterekkel jelőljük hogy egy tuplet hozzunk létre?

  • Milyen módon soroljuk fel a tuple elemeit?

A tuple a listához hasonlóan bármilyen más típust tárolhat, így listát, vagy szótárat is:

numbers = (1, "two", ["three", "four"])
print(numbers)
(1, 'two', ['three', 'four'])

Korábban tárgyaltuk a többszörös értékadást, mely így nézett ki:

fruit1, fruit2, fruit3 = "apple", "banana", "cherry"
print(fruit1, fruit2, fruit3)
apple banana cherry

Ha az "apple", "banana", "cherry" oldalt egy változóba irányítjuk, akkor annak típusa tuple lesz:

fruits = "apple", "banana", "cherry"
print(fruits)
print(type(fruits))
('apple', 'banana', 'cherry')
<class 'tuple'>

Hasonlóan, korábban láttunk példát arra, hogy egy függvénye több visszatérési értékkel rendelkezik, például:

def circle_params(r):
  K = 2*r*3.14145
  T = r**2*3.14145
  return K, T

K, T = circle_params(10)

Ezekután nem meglepő, hogy a visszatérsi típus, ha nincs tagonként kifejtve, akkor tuplet kapunk:

def circle_params(r):
  K = 2*r*3.14145
  T = r**2*3.14145
  return K, T

result = circle_params(10)

print(result)
print(type(result))
(62.82899999999999, 314.145)
<class 'tuple'>

Figyelem

Ha a függvénynek több visszatérési értéke van, akkor a függvény hívásakor, vagy nem adunk meg visszatérési értéket, vagy egyet adunk meg, ami tuple típusú lesz, vagy mindegyiket meg kell adni. Nincs lehetőség csak részben megadni bal oldali változót, ezért az alábbi kód utolsó sorában hibát fogunk kapni.

def circle_params(r):
  K = 2*r*3.14145
  T = r**2*3.14145
  d = 2*r
  return K, T, d

circle_params(5) # ok
K, T, d = circle_params(5) # ok
result = circle_params(5) # ok
K, result = circle_params(5) # hiba: ValueError: too many values to unpack (expected 2)

Műveletek tuplen#

A tuple hosszát a szokásos módon tudjuk lekérdezni:

fruits = "apple", "banana", "cherry"

print(fruits)
print(len(fruits))
('apple', 'banana', 'cherry')
3

A tuple egy elemét a listához megszokott módon tudjuk elérni:

fruits = "apple", "banana", "cherry"

print(fruits[0])
print(fruits[-1])
apple
cherry

Figyelem

Ahogy korábban említettük a list elemei nem megváltoztathatóak, így egy elemének hivatkozása az értékadás bal oldalán nem szerepelhet. Például az alábbi kód futtatása esetén hibát kapunk:

fruits = "apple", "banana", "cherry"
fruits[0] = "apricot" # hiba: TypeError: 'tuple' object does not support item assignment

Lehetőség van slicing típusú elérésre is ugynolyan módon, ahogy a lista esetén már láttuk:

fun_str = ("Programming", "is", "fun", "!")
print('Típus:', type(fun_str))
print(fun_str[0:2])   # 0 és 2 indeszek közötti elemek (a 2-es indexű elem nem tartozik bele)
print(fun_str[1:3])   # 1 és 3 indeszek közötti elemek (a 2-es indexű elem nem tartozik bele)
print(fun_str[1:])    # elemek az egyes indexű elemtől az utolsó elemig
print(fun_str[0:-1])  # elemek 0-tól az utolsó indeszig (az utolsó elem nem tartozik bele)
print(fun_str[:-1])   # ha nem adunk meg kezdő ideszeket, akkor az automatikusan 0. eredmény ugyanaz mint az előző sorban`
Típus: <class 'tuple'>
('Programming', 'is')
('is', 'fun')
('is', 'fun', '!')
('Programming', 'is', 'fun')
('Programming', 'is', 'fun')

tupleket az összeadással tudjuk összefűzni, hasonlóan a listához:

fruits = "apple", "banana", "cherry"
veggies = "apple", "banana", "cherry"

healthy = fruits + veggies
print(healthy)
('apple', 'banana', 'cherry', 'apple', 'banana', 'cherry')

Figyelem

A list esetén használt extend vagy append nem elérhető, ezért a következő kód hibát fog adni. Ennek fő oka, hogy ezek a függvények a listát magát változtatják meg, és mint már említettük, a tuple megváltoztathatatlan.

fruits = "apple", "banana", "cherry"
veggies = "apple", "banana", "cherry"

fruits.append(veggies) # hiba: AttributeError: 'tuple' object has no attribute 'append'

Mutable és immutable típusok#

Nézzük meg a következő példát:

fun = 'Programming is fun!'
not_fun = fun

print(fun)
print(not_fun)

not_fun = 'Programming is not fun'

print(fun)
print(not_fun)
Programming is fun!
Programming is fun!
Programming is fun!
Programming is not fun

Minden a megszokottak szerint történik: lértehozunk a fun változót, majd annak értékét odaadjuk a not_fun változónak. Ezután a not_fun változót felülírjuk. Ezután nézzük meg ugyanezt a kódot listákkal:

fun = ['Programming', 'is', 'fun', '!']
not_fun = fun
print(fun)
print(not_fun)

not_fun[1] = 'is not' # változtatás

print(fun)
print(not_fun)
['Programming', 'is', 'fun', '!']
['Programming', 'is', 'fun', '!']
['Programming', 'is not', 'fun', '!']
['Programming', 'is not', 'fun', '!']

Meglepő módon azt tapasztaljuk, hogy a not_fun változón végzett változtatás, lásd not_fun[1] = 'is not', hatással van a fun változóra is.

Mi történik? A megoldás, hogy a fun változó nem magát a listát, hanem a listára mutató referenciát tartalmazza. Mit jelent ez? A lista valamilyen memóriaterületen foglal helyet. Ennek a memóriaterületnek van valamilyen címe. Amikor egy lista definíció az értékadás jobb oldalán szerepel, a lista a memóriában létrejön, és ennek a memóriának a címe kerül az értékadás bal oldalán található változóban értékadásra. Ez a cím nem látható: a Python elrejti előlünk, és a listát úgy használjuk, mintha a változó maga lenne a lista. Azonban a not_fun = fun értékadáskor a fun változóban található referencia kerül a not_fun változóba, így a fun és not_fun változók tulajdonképpen ugyanarra a listára mutatnak. Ezért az egyiken történő változtatás hatással van a másikra is!

Egyik következménye a fentieknek, hogy a not_fun változón végzett egyéb műveletek is hatással vannak a fun változóra, így például konkatencáiónál:

fun = ['Programming', 'is', 'fun', '!']
not_fun = fun
print(fun)
print(not_fun)

not_fun.append('Not really...')

print(fun)
print(not_fun)
['Programming', 'is', 'fun', '!']
['Programming', 'is', 'fun', '!']
['Programming', 'is', 'fun', '!', 'Not really...']
['Programming', 'is', 'fun', '!', 'Not really...']

Pythonban minden változó referencia. De az első példában láthattuk, hogy a szöveg esetén nem volt a fenti példához hasonló problémánk. Ez hogy lehetséges? Ha belegondolunk, a szám típusok, vagy például a szöveg típus esetén nem tudjuk a változó mögött lévő objektumot (memória tartalmat) olyan módon megváltoztatni, mint azt a lista append vagy extend függvényeinek segítségével tettük. Minden olyan esetben, amikor úgy tűnik egy szám, vagy szöveg változásra került, igazából a változó feltűnik egy értékadás jobb oldalán, így új objektum jön létre a memóriában. Az ilyen adattípusokat, melyek értékei nem megváltoztathatóak megváltoztathatatlan (immutable) típusoknak nevezzük; ezzel szemben az olyan típusokat amelyek megváltozthatóak (mutable) típusnak hívjuk. Az eddig tanul típusok esetén az osztályozás a kövekező:

  • Immutable típusok: int, float, boolean, string, tuple

  • Mutable típusok: list, dict

Sekély és mélymásolás#

mutable típusok esetén tehát problémába ütközünk, amikor egy másolatot szeretnénk készíteni. Ebben a példában két változót szeretnénk, amely két különböző szöveget tárolna:

fun = ['Programming', 'is', 'fun', '!']
not_fun = fun
not_fun[1] = 'is not'

print(fun)
print(not_fun)
['Programming', 'is not', 'fun', '!']
['Programming', 'is not', 'fun', '!']

Tehát a not_fun listát nem tudjuk megváltoztatni anélkül, hogy a fun változó ne változzon. Ezt úgy mondjuk, hogy a not_fun = fun egy sekély másolás, mivel csak referenciát másol. Ha egy teljes másolatot akarunk készíteni egy mutable típus mögött lévő objektumról, akkor a copy() függvényt kell meghívnunk a változón. Ezt hívjuk mély másolásnak:

fun = ['Programming', 'is', 'fun', '!']
not_fun = fun.copy()
not_fun[1] = 'is not'

print(fun)
print(not_fun)
['Programming', 'is', 'fun', '!']
['Programming', 'is not', 'fun', '!']

Feladat

A fentiek szótárakra is igazak. Mutassuk meg a fenti mutable tulajdonságokat szótárral!

Mutable típusok függvényekben#

Nézzük meg az alábbi kódot, amely egy nagy számra cseréli a bemeneti változót:

def make_big(x):
    x = 100000
    print(x)

a = 0
make_big(a)
print(a)
100000
0

Semmi meglepő nem történt, mivel az x változó hatásköre csak a függvényen belül érvényesül, ezért a globális névtérben létrehozott a változót nem írja felül.

Ezzel szemben nézzük meg az alábbi kódot:

def make_big(x):
    x[0] = 100000
    print(x)
    
a = [0]
make_big(a)
print(a)
[100000]
[100000]

Meglepő módon azt tapasztaljuk, hogy a globális változó értéke megváltozott.

Mi történik? Mindkét esetben úgy képzelhetjük, hogy a make_big függvény x paramétere, egy, a függvény látókörébe tartozó változó, amelynek értéke a függvény hívásakor felveszi az a változó értékét, oly módon hogy az a mögötti referencia bemásolásra kerül az x változóba. Az első esetben a x = 100000 sornál az x változó felvesz egy új referenciát ami az új 10000 értékre mutat. Ez egy új referencia, ezért a globális a nem változik meg. Ezzel szemben a lista esetén az a és x változók ugyanarra a listára mutató referenciák, ezért az x lista elemein történő változások hatással vannnak a globális névtérben létrehozott listára is.

Ez azt is jelenti, hogy nem csak az elemeken, hanem magán a listán végzett változtatás is hatással van a globális névtérben található a változóra:

def make_big(x):
    x.append(100000)
    print(x)
    
a = [0]
make_big(a)
print(a)
[0, 100000]
[0, 100000]

A fentiek további következménye, hogyha az x változót, amely egy listára mutató referenciát táról, egy értékadással felülírjuk, azaz egy új listára mutató referenciát hozunk létre, akkor az már nem fog változást okozni a globális névtérben található a változóra:

def make_big(x):
    x = [100000]
    print(x)
    
a = [0]
make_big(a)
print(a)
[100000]
[0]

A x = [100000] művelet ugyanaz, mint a x = 100000 sor az első példánkban.

Az is kulcsszó#

Az is kulcszóval ellenőrízhetjük, hogy két változó mögötti referenciák ugyanarra az objektumra mutat-e. Ez immutable típus esetén ugyan az mint a == összehasonlító operátor:

if (not False or True) is True:
  print('True')
True

mutable típusok esetén viszont különböző, nézzünk két példát. A két lista megegyezik az alábbi példában:

shopping_list_jane = ['bread', 'milk']
shopping_list_joe = ['bread', 'milk']

if shopping_list_jane == shopping_list_joe:
  print('It is true, so they are married!')
else:
  print('It is false, so they are not married!')
It is true, so they are married!

Azonban a két változó mögötti referencia különbözik:

shopping_list_jane = ['bread', 'milk']
shopping_list_joe = ['bread', 'milk']

if shopping_list_jane is shopping_list_joe:
  print('It is true, so they are married!')
else:
  print('It is false, so they are not married!')
It is false, so they are not married!