Funkcja id() w 6 kluczowych koncepcjach Pythona
Istnieje ponad 70 wbudowanych funkcji, które są dostępne dla każdego interpretera Pythona podczas jego uruchamiania. Każdy, kto uczy się Pythona, powinien znać przynajmniej kilka podstawowych. Możemy, na przykład, użyć len()
, aby uzyskać długość obiektu, taką jak liczba elementów na liście lub w słowniku. W innym przykładzie możemy użyć print()
do wydrukowania interesujących obiektów, by je debugować lub dowiedzieć się o nich więcej.
Co więcej, zakładam, że prawie wszyscy programiści Pythona widzieli w tutorialach, jak używa się wbudowanej funkcji id()
. Z tego, co mi jednak wiadomo, to raczej ciężko znaleźć te informacje w usystematyzowanej formie. Dlatego postaram się zapewnić usystematyzowane informacje dotyczące użycia funkcji id()
, co pozwoli na zrozumienie sześciu kluczowych koncepcji Pythona.
1. Wszystko jest obiektem w Pythonie
Będąc popularnym obiektowym językiem programowania, Python zawsze używa obiektów w swoich implementacjach. Na przykład, wbudowane typy danych, takie jak liczby całkowite, liczby zmiennoprzecinkowe, ciągi znaków, listy i słowniki są obiektami. Ponadto funkcje, klasy, a nawet moduły również są używane jako obiekty.
Z definicji wynika, że funkcja id()
pobiera obiekt i zwraca jego tożsamość, czyli adres pamięci wyrażony jako liczba całkowita. Ta funkcja doskonale pokazuje, że w Pythonie wszystko jest obiektem.
>>> import sys
>>> class Foo:
... pass
...
>>> def foo():
... pass
...
>>> a_tuple = ('Error', 404)
>>> a_dict = {'error_code': 404}
>>> a_list = [1, 2, 3]
>>> a_set = set([2, 3, 5])
>>> objects = [2, 2.2, 'hello', a_tuple, a_dict, a_list, a_set, Foo, foo, sys]
>>>
>>> for item in objects:
... print(f'{type(item)} with id: {id(item)}')
...
<class 'int'> with id: 4479354032
<class 'float'> with id: 4481286448
<class 'str'> with id: 4483233904
<class 'tuple'> with id: 4483061152
<class 'dict'> with id: 4483236000
<class 'list'> with id: 4483236720
<class 'set'> with id: 4483128688
<class 'type'> with id: 140235151304256
<class 'function'> with id: 4483031840
<class 'module'> with id: 4480703856
Obiekty Pythona i funkcja id()
W powyższym fragmencie kodu widać, że każdy element na liście obiektów może być używany w funkcji id()
, która ujawnia adres pamięci dla każdego obiektu. Następujący przykład polega na tym, że funkcja id()
też powinna mieć swój adres pamięci.
>>> print(f'{type(id)} with id: {id(id)}')
<class 'builtin_function_or_method'> with id: 4480774224
2. Przypisywanie zmiennej i aliasowanie
Kiedy tworzymy zmienną w Pythonie, to zwykle używamy następującej składni:
var_name = the_object
Przywiązuje to w zasadzie obiekt, który jest tworzony w pamięci, z nazwą konkretnej zmiennej. Co się stanie, jeśli przypiszemy zmienną inną zmienną, na przykład var_name1 = var_name
? Zwróć uwagę na następujący przykład. W poniższym fragmencie kodu najpierw stworzyliśmy zmienną o nazwie hello
, do której przypisaliśmy ciąg znaków. Następnie stworzyliśmy inną zmienną o nazwie world
, przypisując poprzednią zmienną hello
. Kiedy wydrukowaliśmy ich adresy pamięci, okazało się, że zarówno hello
, jak i world
mają ten sam adres, co sugeruje, że są one tym samym obiektem w pamięci.
>>> hello = 'Hello World!'
>>> print(f'{hello} from: {id(hello)}')
Hello World! from: 4341735856
>>> world = hello
>>> print(f'{world} from: {id(world)}')
Hello World! from: 4341735856
>>>
>>> bored = {'a': 0, 'b': 1}
>>> print(f'{bored} from: {id(bored)}')
{'a': 0, 'b': 1} from: 4341577200
>>> more_bored = bored
>>> print(f'{more_bored} from: {id(more_bored)}')
{'a': 0, 'b': 1} from: 4341577200
>>> more_bored['c'] = 2
>>> bored
{'a': 0, 'b': 1, 'c': 2}
>>> more_bored
{'a': 0, 'b': 1, 'c': 2}
Przypisywanie zmiennej i aliasowanie
W tym przypadku zmienna world
jest zwykle nazywana aliasem zmiennej hello
, a proces tworzenia nowej zmiennej przez przypisanie istniejącej można nazwać aliasowaniem. W innych językach programowania aliasy są bardzo podobne do wskaźników lub referencji w odniesieniu do podstawowych obiektów w pamięci. W powyższym kodzie możemy jeszcze zobaczyć, że kiedy utworzyliśmy alias dla słownika i zmodyfikowaliśmy jego dane, to modyfikacja ta dotyczyła również oryginalnej zmiennej. Dzieje się tak, ponieważ „pod maską” zmodyfikowaliśmy w pamięci ten sam obiekt słownika.
3.Operatory porównania: == oraz is
Czasem musimy porównać dwa obiekty, aby zastosować różne funkcje, gdy określony warunek jest spełniony lub nie. Jeśli chodzi o zbadanie równości, to możemy użyć dwóch operatorów porównania: ==
oraz is
. Ci, którzy Pythona się dopiero uczą, mogą błędnie uważać, że oznaczają one to samo, podczas gdy jest między nimi mała różnica.
Przyjrzyjmy się następującym przykładom. Stworzyliśmy dwie listy o tych samych elementach. Kiedy porównaliśmy te dwie listy za pomocą operatora ==
, wynik porównania był True
. Gdy porównaliśmy dwie listy za pomocą operatora is
, to wynik był False
. Dlaczego otrzymaliśmy różne wyniki? Jest tak, ponieważ operator ==
porównuje wartości, a is
porównuje tożsamości (czyli adresy pamięci).
Jak można się spodziewać po zmiennych, które odwołują się do tego samego obiektu w pamięci, mają one zarówno te same wartości, jak i tożsamości. Powoduje to, że wyniki oceny są takie same dla operatorów ==
oraz operatorów is
. Widać to na poniższym przykładzie dotyczącym str0
i str1
:
>>> list0 = [1, 2, 3, 4]
>>> list1 = [1, 2, 3, 4]
>>> print(f'list0 == list1: {list0 == list1}')
list0 == list1: True
>>> print(f'list0 is list1: {list0 is list1}')
list0 is list1: False
>>> print(f'list0 id: {id(list0)}')
list0 id: 4341753408
>>> print(f'list1 id: {id(list1)}')
list1 id: 4341884240
>>>
>>> str0 = 'Hello'
>>> str1 = str0
>>> print(f'str0 == str1: {str0 == str1}')
str0 == str1: True
>>> print(f'str0 is str1: {str0 is str1}')
str0 is str1: True
>>> print(f'str0 id: {id(str0)}')
str0 id: 4341981808
>>> print(f'str1 id: {id(str1)}')
str1 id: 4341981808
Operatory porównania
4. Cache’owanie liczb całkowitych
Liczby całkowite są często używane w programowaniu. Interpretery w Pythonie zazwyczaj zapisują w pamięci podręcznej małe liczby całkowite w zakresie od -5 do 256. Oznacza to, że po uruchomieniu interpretera w Pythonie liczby te zostaną utworzone i będą dostępne do późniejszego wykorzystania w pamięci. Poniższy fragment kodu pokazuje tę funkcję:
>>> number_range = range(-10, 265)
>>> id_counters = {x: 0 for x in number_range}
>>> id_records = {x: 0 for x in number_range}
>>>
>>> for _ in range(1000):
... for number in number_range:
... id_number = id(number)
... if id_records[number] != id_number:
... id_records[number] = id_number
... id_counters[number] += 1
...
>>> [x for x in id_counters.keys() if id_counters[x] > 1]
[-10, -9, -8, -7, -6, 257, 258, 259, 260, 261, 262, 263, 264]
Cache’owanie liczb całkowitych
W powyższym przykładzie utworzyłem dwa słowniki z id_counters
śledzącym liczbę unikalnych tożsamości dla każdej liczby całkowitej oraz id_records
śledzącym najnowszą tożsamość liczby całkowitej. W przypadku liczb całkowitych z zakresu od -10 do 265, jeśli tożsamość nowej liczby całkowitej różni się od istniejącej, to odpowiadający jej licznik zwiększa się o jeden. Powtórzyłem ten proces 1000 razy.
W ostatniej linijce kodu zastosowano wyrażenie listowe, aby pokazać liczby całkowite, które mają więcej niż jedną tożsamość. Najwyraźniej po tysiącu razach liczby całkowite od -5 do 256 miały tylko jedną tożsamość dla każdej listy, tak jak mówiłem w poprzednim paragrafie. Aby dowiedzieć się więcej o wyrażeniach listowych Pythona, zapoznaj się z moim poprzednim artykułem.
5. Płytkie i głębokie kopiowanie
Od czasu do czasu musimy robić kopie istniejących obiektów, abyśmy mogli zmienić jedną kopię bez zmiany drugiej. Wbudowany moduł kopiowania udostępnia w tym celu dwie metody: copy()
i deepcopy()
. Wykonują one odpowiednio płytkie i głębokie kopie. Skorzystaj z funkcji id()
, aby lepiej zrozumieć te dwa pojęcia.
>>> import copy
>>> original = [[0, 1], 2, 3]
>>> print(f'{original} id: {id(original)}, embeded list id: {id(original[0])}')
[[0, 1], 2, 3] id: 4342107584, embeded list id: 4342106784
>>> copy0 = copy.copy(original)
>>> print(f'{copy0} id: {id(copy0)}, embeded list id: {id(copy0[0])}')
[[0, 1], 2, 3] id: 4341939968, embeded list id: 4342106784
>>> copy1 = copy.deepcopy(original)
>>> print(f'{copy1} id: {id(copy1)}, embeded list id: {id(copy1[0])}')
[[0, 1], 2, 3] id: 4341948160, embeded list id: 4342107664
Płytkie i głębokie kopiowanie
Najpierw stworzyliśmy listę o nazwie original, która składała się z zagnieżdżonej listy i dwóch liczb całkowitych. Następnie wykonaliśmy dwie kopie (copy0
i copy1
), używając odpowiednio metod copy()
i deepcopy()
. Jak możemy się spodziewać, oryginał, copy0
i copy1
mają te same wartości (tj. [[0, 1], 2, 3]
). Mają one jednak różne tożsamości, ponieważ, w przeciwieństwie do aliasowania, zarówno metody copy()
, jak i deepcopy()
tworzą nowe obiekty w pamięci, dzięki czemu nowe kopie mają różne tożsamości.
Najistotniejsza różnica między płytkim i głębokim kopiowaniem polega na tym, że głębokie kopiowanie utworzy kopie rekurencyjnie dla oryginalnych obiektów złożonych, natomiast płytkie kopiowanie zachowa odniesienia do istniejących obiektów, jeśli zajdzie taka potrzeba. W powyższym przykładzie zmienna original jest w rzeczywistości obiektem złożonym (tzn. lista jest zagnieżdżona na innej liście).
W tym przypadku użycie metody copy()
sprawia, że pierwszy element zmiennej copy0
ma taką samą tożsamość (tj. ten sam obiekt), jak pierwszy element oryginału. Natomiast metoda deepcopy()
tworzy kopię zagnieżdżonej listy w pamięci, dzięki czemu pierwszy element w copy1
ma inną tożsamość niż oryginał.
„Rekurencyjnie” w przypadku głębokiego kopiowania oznacza tyle, że jeśli istnieje wiele poziomów zagnieżdżenia się (np. lista zagnieżdżona w liście, która jest z kolei zagnieżdżona w innej liście), metoda deepcopy()
utworzy nowe obiekty dla każdego poziomu. Oto przykład:
>>> mul_nested = [[[0, 1], 2], 3]
>>> print(f'{mul_nested} id: {id(mul_nested)}, inner id: {id(mul_nested[0])}, innermost id: {id(mul_nested[0][0])}')
[[[0, 1], 2], 3] id: 4342107824, inner id: 4342106944, innermost id: 4342107424
>>> mul_nested_dc = copy.deepcopy(mul_nested)
>>> print(f'{mul_nested_dc} id: {id(mul_nested_dc)}, inner id: {id(mul_nested_dc[0])}, innermost id: {id(mul_nested_dc[0][0])}')
[[[0, 1], 2], 3] id: 4342107264, inner id: 4342107984, innermost id: 4342107904
Rekurencja w deepcopy()
6. Mutowalność danych
Mutowalność danych to zaawansowany temat, jeżeli chodzi o Pythona. Ogólnie rzecz biorąc, dane niemutowalne to te obiekty, których wartości nie można zmienić po ich utworzeniu, takie jak liczby całkowite, ciągi i krotki. Dane mutowalne odnoszą się natomiast do tych obiektów, których wartość można po utworzeniu zmienić, np. listy, słowniki oraz zbiory. Jedną z rzeczy, na które należy zwrócić uwagę, jest to, że „zmieniając wartości” mamy na myśli to, czy obiekt w pamięci można zmienić, czy nie. Dokładne omówienie mutowalności danych można znaleźć w moim poprzednim artykule.
Przyjrzymy się następującemu przykładowi, jeżeli chodzi o id()
. Dla danych niemutowalnych (zmienna liczba całkowita thousand we fragmencie kodu), kiedy próbowaliśmy zmienić ich wartość, to w pamięci utworzyła się nowa liczba całkowita, co odzwierciedla nowa tożsamość zmiennej thousand. Innymi słowy, pierwotny podstawowy obiekt liczby całkowitej nie mógł zostać zmieniony. Próba zmiany liczby całkowitej spowodowała właśnie utworzenie nowego obiektu w pamięci.
>>> thousand = 1000
>>> print(f'{thousand} id: {id(thousand)}')
1000 id: 4342004944
>>> thousand += 1
>>> print(f'{thousand} id: {id(thousand)}')
1001 id: 4342004912
>>> numbers = [4, 3, 2]
>>> print(f'{numbers} id: {id(numbers)}')
[4, 3, 2] id: 4342124624
>>> numbers += [1]
>>> print(f'{numbers} id: {id(numbers)}')
[4, 3, 2, 1] id: 4342124624
Mutowalność danych, a id()
Jeśli coś jest niejasne, zobaczmy, co się stało z mutowalnym typem danych — w naszym przypadku z listą numbers
. Jak widać w powyższym kodzie, kiedy próbowaliśmy zmienić wartości numbers
, zmienna ta została zaktualizowana, a zaktualizowana lista wciąż miała tę samą tożsamość. Potwierdza to mutowalność obiektów takich jak listy.
Podsumowanie
Dzięki funkcji id()
zrozumieliśmy 6 kluczowych koncepcji w Pythonie. Oto krótkie podsumowanie:
- W Pythonie wszystko jest obiektem
- Tworzymy zmienne przez przypisywanie, a aliasy wskazują na ten sam obiekt w pamięci
- Operator porównania
==
zestawia ze sobą wartości, podczas gdy operatoris
porównuje tożsamości. - Interpretery Pythona tworzą obiekty liczb całkowitych od -5 do 256 w momencie uruchomienia.
- Zarówno płytkie, jak i głębokie kopie mają takie same wartości jak ich oryginalne obiekty, ale płytkie kopiowanie ogranicza się do skopiowania referencji do zagnieżdżonych obiektów oryginalnego obiektu.
- Wartości mutowalnych obiektów można zmieniać w pamięci, podczas gdy obiekty niezmienne nie obsługują zmian wartości.
Oryginał w języku angielskim przeczytasz tutaj.