Cum să utilizați operatorii de despachetare (*, **) în Python?

Python este un limbaj de programare extrem de popular și larg utilizat. În acest articol, vom explora o caracteristică esențială, dar adesea subestimată: despachetarea.

Este posibil să fi întâlnit simbolurile * și ** în codul altora sau chiar să le fi folosit fără a înțelege pe deplin rolul lor. Vom analiza conceptul de despachetare și vom vedea cum îl putem aplica pentru a scrie un cod Python mai eficient și mai clar.

Pentru o înțelegere mai bună a acestui tutorial, iată câteva noțiuni de bază:

  • Iterabil: Orice secvență (listă, set, tuplu, dicționar) care poate fi parcursă printr-o buclă „for”.
  • Apelabil: Un obiect care poate fi executat folosind paranteze (ex: funcția mea()).
  • Shell: Un mediu interactiv în care putem rula cod Python (accesibil prin comanda „python” în terminal).
  • Variabilă: Un nume simbolic care stochează un obiect și îi alocă un spațiu în memorie.

Începem cu o confuzie frecventă: asteriscul în Python are și rol de operator aritmetic. Un asterisc (*) se utilizează pentru înmulțire, iar două asteriscuri (**) semnifică ridicarea la putere.

>>> 3*3
9
>>> 3**3
27

Putem demonstra acest lucru deschizând un shell Python și introducând expresiile de mai sus:

Notă: Pentru a urma acest tutorial, este necesar să aveți instalat Python 3. Dacă nu îl aveți, consultați ghidul nostru de instalare.

După cum vedeți, folosim asteriscul între două numere. În acest context, el este un operator aritmetic.

>>> *range(1, 6),
(1, 2, 3, 4, 5)
>>> {**{'vanilla':3, 'chocolate':2}, 'strawberry':2}
{'vanilla': 3, 'chocolate': 2, 'strawberry': 2}

Pe de altă parte, asteriscul (*) și dublu asteriscul (**) plasate înaintea unui obiect iterabil indică despachetarea acestuia, așa cum vedem în exemplul de mai sus.

Nu vă faceți griji dacă conceptul nu este încă clar; acesta este doar un preludiu la despachetarea în Python. Vă invităm să citiți tutorialul complet pentru o înțelegere aprofundată!

Ce este despachetarea?

Despachetarea reprezintă acțiunea de a extrage elemente dintr-un obiect iterabil, precum liste, tupluri și dicționare. Imaginați-vă că deschideți o cutie și scoateți obiectele din interior (cabluri, căști, un stick USB).

Despachetarea în Python este similară cu despachetarea unei cutii în viața reală.

>>> mybox = ['cables', 'headphones', 'USB']
>>> item1, item2, item3 = mybox

Să ilustrăm același exemplu în cod pentru o înțelegere mai facilă:

Observăm că atribuim cele trei elemente din lista ‘mybox’ la trei variabile separate: item1, item2 și item3. Această atribuire de valori variabilelor este conceptul de bază al despachetării în Python.

>>> item1
'cables'
>>> item2
'headphones'
>>> item3
'USB'

Verificând valoarea fiecărei variabile, vom observa că item1 conține ‘cables’, item2 conține ‘headphones’ și item3 conține ‘USB’.

>>> newbox = ['cables', 'headphones', 'USB', 'mouse']
>>> item1, item2, item3 = newbox
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 3)

Până în acest moment, codul funcționează corect. Dar ce se întâmplă dacă dorim să despachetăm o listă cu mai multe elemente, păstrând același număr de variabile?

Probabil vă așteptați la această eroare. Încercăm să atribuim 4 elemente la 3 variabile. Cum ar putea Python să gestioneze această situație? Așa este, obținem o eroare de tip ValueError, cu mesajul „too many values to unpack” (prea multe valori pentru despachetare), deoarece avem 3 variabile în stânga și 4 valori în dreapta.

>>> lastbox = ['cables', 'headphones']
>>> item1, item2, item3 = lastbox
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)

În cazul în care încercăm să facem același lucru cu mai multe variabile decât elemente, vom obține tot o eroare ValueError, dar cu un mesaj ușor diferit:

Notă: Am lucrat cu liste, dar această formă de despachetare poate fi utilizată cu orice obiect iterabil (liste, seturi, tupluri, dicționare).

Cum depășim această problemă? Există o modalitate de a despacheta toate elementele unui obiect iterabil într-un număr mai mic de variabile, fără a genera erori?

Desigur că există, și se numește operator de despachetare (sau operator asterisc: * și **). Să vedem cum îl folosim în Python.

Despachetarea listelor cu operatorul *

Operatorul asterisc (*)

>>> first, *unused, last = [1, 2, 3, 5, 7]
>>> first
1
>>> last
7
>>> unused
[2, 3, 5]

este folosit pentru a despacheta toate valorile unui obiect iterabil care nu au fost încă atribuite variabilelor.

>>> first, *_, last = [1, 2, 3, 5, 7]
>>> _
[2, 3, 5]

Să presupunem că dorim să obținem primul și ultimul element dintr-o listă, fără a utiliza indici. Am putea face acest lucru folosind operatorul asterisc:

>>> first, *_, last = [1, 2]
>>> first
1
>>> last
2
>>> _
[]

După cum vedem, obținem toate valorile neutilizate cu ajutorul operatorului asterisc. De obicei, când nu avem nevoie de anumite valori, folosim o variabilă cu numele „_” (underscore), care indică o „variabilă ignorată”.

Putem folosi această metodă chiar dacă lista are doar două elemente:

În acest caz, variabila underscore („_”) stochează o listă goală, iar celelalte două variabile primesc valorile disponibile din listă.

>>> *string = 'PythonIsTheBest'

O greșeală frecventă

>>> *string = 'PythonIsTheBest'
  File "<stdin>", line 1
SyntaxError: starred assignment target must be in a list or tuple

Putem încerca să despachetăm un singur element dintr-un iterabil. De exemplu, am putea scrie un cod asemănător cu următorul: Codul de mai sus va genera o eroare SyntaxError: Acest lucru se întâmplă pentru că, conform

Specificatiei PEP

>>> *string, = 'PythonIsTheBest'
>>> string
['P', 'y', 't', 'h', 'o', 'n', 'I', 's', 'T', 'h', 'e', 'B', 'e', 's', 't']

o atribuire simplă cu un tuplu (sau o listă) în partea stângă.

>>> *numbers, = range(5)
>>> numbers
[0, 1, 2, 3, 4]

Dacă dorim să despachetăm toate valorile unui iterabil într-o singură variabilă, trebuie să folosim un tuplu, adăugând o simplă virgulă la final:

Un alt exemplu este folosirea funcției ‘range’, care returnează o secvență de numere.

Acum că știm cum să despachetăm liste și tupluri cu operatorul *, să vedem cum despachetăm dicționarele.

Despachetarea dicționarelor cu operatorul **

>>> **greetings, = {'hello': 'HELLO', 'bye':'BYE'} 
...
SyntaxError: invalid syntax

În timp ce un singur asterisc se folosește pentru a despacheta liste și tupluri, dublu asterisc (**) este folosit pentru a despacheta dicționare.

>>> food = {'fish':3, 'meat':5, 'pasta':9} 
>>> colors = {'red': 'intensity', 'yellow':'happiness'}
>>> merged_dict = {**food, **colors}
>>> merged_dict
{'fish': 3, 'meat': 5, 'pasta': 9, 'red': 'intensity', 'yellow': 'happiness'}

Din păcate, nu putem despacheta un dicționar într-o singură variabilă, așa cum am făcut cu tuplurile și listele. Următorul cod va genera o eroare:

Cu toate acestea, putem folosi operatorul ** în interiorul apelabilelor și al altor dicționare. De exemplu, dacă dorim să creăm un dicționar nou, format din alte dicționare, am putea folosi codul de mai jos:

Aceasta este o modalitate eficientă de a crea dicționare compuse, deși nu este abordarea principală pentru despachetarea în Python.

Să vedem cum folosim despachetarea cu obiecte apelabile.

Ambalarea în Funcții: args și kwargs

Probabil ați văzut parametrii ‘args’ și ‘kwargs’ implementați în clase sau funcții. Să vedem de ce îi folosim împreună cu apelabilele.

>>> def product(n1, n2):
...     return n1 * n2
... 
>>> numbers = [12, 1]
>>> product(*numbers)
12

Împachetarea cu operatorul * (args)

>>> product(12, 1)
12

Să presupunem că avem o funcție care calculează produsul a două numere.

>>> numbers = [12, 1, 3, 4]
>>> product(*numbers)
...
TypeError: product() takes 2 positional arguments but 4 were given

După cum vedem, despachetăm lista ‘numbers’ și o pasăm funcției. Practic, rulăm codul:

>>> def product(*args):
...     result = 1
...     for i in args:
...             result *= i
...     return result
...
>>> product(*numbers)
144

Până acum, totul funcționează, dar ce se întâmplă dacă vrem să pasăm o listă mai lungă? Va genera o eroare, deoarece funcția primește mai multe argumente decât poate gestiona.

Putem rezolva această problemă prin împachetarea listei direct în funcție, creând un iterabil în interiorul acesteia, care ne permite să transmitem oricâte argumente dorim.

Aici tratăm parametrul ‘args’ ca pe un obiect iterabil, parcurgând elementele sale și returnând produsul tuturor numerelor. Observăm că valoarea inițială a lui ‘result’ trebuie să fie 1, deoarece dacă ar fi 0, funcția ar returna mereu 0. Notă: ‘args’ este doar o convenție, puteți folosi orice alt nume de parametru.De asemenea, putem pasa numere direct funcției, fără a folosi o listă, la fel cum facem cu funcția încorporată

>>> product(5, 5, 5)
125
>>> print(5, 5, 5)
5 5 5

print()

>>> def test_type(*args):
...     print(type(args))
...     print(args)
... 
>>> test_type(1, 2, 4, 'a string')
<class 'tuple'>
(1, 2, 4, 'a string')

.

În final, vom obține tipul de obiect al argumentelor unei funcții.

După cum vedem în codul de mai sus, tipul argumentelor va fi mereu un tuplu, iar conținutul acestuia va fi toate argumentele non-cheie pasate funcției.

Împachetarea cu operatorul ** (kwargs)

>>> def make_person(name, **kwargs):
...     result = name + ': '
...     for key, value in kwargs.items():
...             result += f'{key} = {value}, '
...     return result
... 
>>> make_person('Melissa', id=12112, location='london', net_worth=12000)
'Melissa: id = 12112, location = london, net_worth = 12000, '

După cum am văzut, operatorul ** este folosit exclusiv pentru dicționare. Aceasta înseamnă că, prin acest operator, putem trimite funcției perechi cheie-valoare ca parametru.

Să creăm o funcție ‘make_person’, care primește un argument pozițional ‘name’ și o cantitate nedefinită de argumente cu cuvinte cheie.

Observăm că expresia ‘**kwargs’ transformă toate argumentele cu cuvinte cheie într-un dicționar, pe care îl putem itera în interiorul funcției.

>>> def test_kwargs(**kwargs):
...     print(type(kwargs))
...     print(kwargs)
... 
>>> test_kwargs(random=12, parameters=21)
<class 'dict'>
{'random': 12, 'parameters': 21}

Notă: ‘kwargs’ este doar o convenție, puteți folosi orice alt nume de parametru.

Putem verifica tipul ‘kwargs’ în același fel cum am făcut cu ‘args’:

>>> def my_final_function(*args, **kwargs):
...     print('Type args: ', type(args))
...     print('args: ', args)
...     print('Type kwargs: ', type(kwargs))
...     print('kwargs: ', kwargs)
... 
>>> my_final_function('Python', 'The', 'Best', language="Python", users="A lot")
Type args:  <class 'tuple'>
args:  ('Python', 'The', 'Best')
Type kwargs:  <class 'dict'>
kwargs:  {'language': 'Python', 'users': 'A lot'}

Variabila internă ‘kwargs’ se transformă întotdeauna într-un dicționar, care stochează perechile cheie-valoare transmise funcției.

În final, folosim atât ‘args’ cât și ‘kwargs’ în aceeași funcție:

Concluzie

  • Operatorii de despachetare sunt extrem de utili în sarcinile de zi cu zi. Acum știți cum să îi folosiți, atât în declarații simple, cât și în parametrii funcțiilor.
  • În acest tutorial ați învățat:
    • Folosirea operatorului * pentru tupluri și liste și ** pentru dicționare.
    • Utilizarea operatorilor de despachetare în funcții și clase.

Parametrul ‘args’ este folosit pentru a transmite funcțiilor parametrii care nu au o cheie, iar ‘kwargs’ este folosit pentru a transmite parametrii cu cuvinte cheie către funcții.