4. Mai multe unelte de control al fluxului

Pe lângă instrucțiunea de control while introdusă anterior, Python-ul utilizează alte câteva asemenea instrucțiuni cu care ne vom întâlni în acest capitol.

4.1. Instrucțiuni if

Probabil că cel mai cunoscut tip de instrucțiune de control este instrucțiunea if. Ca, de exemplu:

>>> x = int(input("Vă rugăm să introduceți un număr întreg: "))
Vă rugăm să introduceți un număr întreg: 42
>>> if x < 0:
...     x = 0
...     print('Număr negativ, pe care îl transformăm în zero')
... elif x == 0:
...     print('Numărul zero')
... elif x == 1:
...     print('Numărul unu')
... else:
...     print('Număr mai mare ca unu')
...
Număr mai mare ca unu

Putem avea zero sau mai multe clauze elif, în timp ce clauza else este opțională. Cuvântul-cheie «elif» este prescurtarea lui «else if» și ne permite să evităm indentarea excesivă. O secvență ifelifelif … servește de substitut pentru instrucțiuni precum switch sau case disponibile în alte limbaje de programare.

Dacă aveți de comparat o valoare cu mai multe constante, ori dacă verificați anumite tipuri de date sau atribute, s-ar putea să vă fie utilă instrucțiunea de control match. Pentru mai multe detalii, vedeți Instrucțiuni match.

4.2. Instrucțiuni for

Instrucțiunea for din Python diferă puțin de cele cu care poate că sunteți obișnuit din C ori din Pascal. În loc să itereze mereu după elementele numerice ale unei progresii aritmetice (precum în Pascal), ori să-i dea utilizatorului posibilitatea de a defini atât un pas de iterație cât și o condiție de oprire (ca în C), instrucțiunea for a Python-ului iterează după itemii oricărei secvențe (fie ea listă sau șir de caractere), în ordinea în care acești itemi apar în secvență. De exemplu (fără să facem vreun joc de cuvinte) :

>>> # Să evaluăm câteva șiruri de caractere:
>>> cuvinte = ['pisică', 'fereastră', 'defenestrare']
>>> for c in cuvinte:
...     print(c, len(c))
...
pisică 6
fereastră 9
defenestrare 12

Codul care să modifice o colecție în timp ce iterează după itemii acelei colecții este greu de scris din prima încercare. În schimb, se dovedesc mai ușor de realizat atât o ciclare după itemii unei copii a colecției în cauză cât și crearea unei colecții noi:

# Crearea unei colecții eșantion
utilizatori = {'Hans': 'activ', 'Éléonore': 'inactiv', '景太郎': 'activ'}

# Strategie:  să iterăm după itemii unei copii
for utilizatorul, starea in utilizatori.copy().items():
    if starea == 'inactiv':
        del utilizatori[utilizatorul]

# Strategie:  să creăm o nouă colecție
utilizatori_activi = {}
for utilizatorul, starea in utilizatori.items():
    if starea == 'activ':
        utilizatori_activi[utilizatorul] = starea

4.3. Funcția range()

Dacă aveți de iterat după termenii unui șir de numere, atunci funcția predefinită range() vă este la îndemână. Ea generează progresii aritmetice:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

Valoarea de final precizată nu va face niciodată parte din secvența generată; range(10) generează 10 valori, mai precis indecșii permiși ai itemilor unei secvențe de lungime 10. Este posibil să începem plaja de valori și de la alt număr, ori să specificăm un increment diferit (inclusiv negativ; uneori, el este numit «pasul»):

>>> list(range(5, 10))
[5, 6, 7, 8, 9]

>>> list(range(0, 10, 3))
[0, 3, 6, 9]

>>> list(range(-10, -100, -30))
[-10, -40, -70]

Pentru a itera după indicii unei secvențe, puteți combina range() și len() după cum urmează. Exemplul face referire la un cântec de grădiniță american:

>>> a = ['Maria', 'avea', 'un', 'mieluț']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 Maria
1 avea
2 un
3 mieluț

Pe de altă parte, în majoritatea unor asemenea cazuri, este convenabil să utilizați funcția enumerate(), a se vedea Tehnici de iterare.

Ceva neobișnuit se va petrece atunci când printați o plajă de valori de-a dreptul:

>>> range(10)
range(0, 10)

În multe privințe, obiectul returnat de range() se comportă de parcă ar fi o listă, însă el nu este o listă. Este doar un obiect care întoarce itemii consecutivi ai secvenței care vă interesează atunci când iterați după itemii acesteia, însă el nu construiește cu adevărat o listă și prin aceasta economisește spațiu.

Spunem despre un asemenea obiect că este iterable, adică potrivit ca țintă pentru funcțiile și constructele care se așteaptă la ceva din care să extragă itemi succesivi până ce rezervorul acestor itemi se va goli. Am văzut deja că instrucțiunea for este un asemenea construct, în timp ce o funcție care poate avea un iterabil drept argument este sum():

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

Vom vedea ulterior și alte funcții care întorc iterabili și care primesc iterabili ca argumente. În capitolul Structuri de date vom discuta mai în detaliu despre list().

4.4. Instrucțiunile break și continue

Instrucțiunea break stopează cea mai din interior instrucțiune de ciclare for sau while care o include:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(f"{n} este egal cu {x} * {n//x}")
...             break
...
4 este egal cu 2 * 2
6 este egal cu 2 * 3
8 este egal cu 2 * 4
9 este egal cu 3 * 3

Instrucțiunea continue trece la următoarea iterație a ciclului:

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print(f"Găsit un număr par {num}")
...         continue
...     print(f"Găsit un număr impar {num}")
...
Găsit un număr par 2
Găsit un număr impar 3
Găsit un număr par 4
Găsit un număr impar 5
Găsit un număr par 6
Găsit un număr impar 7
Găsit un număr par 8
Găsit un număr impar 9

4.5. Clauzele else ale ciclurilor

Într-un ciclu for sau while, instrucțiunea break poate fi utilizată în tandem cu clauza else. Astfel, dacă ciclul se încheie fără să fi fost executat break-ul, atunci va fi executată clauza else.

Într-un ciclu for, clauza else se execută după ce bucla își încheie iterația finală, adică dacă nu a intervenit nicio stopare pe parcurs.

Într-un ciclu while, clauza se va executa după ce condiția buclei va deveni falsă.

Pentru ambele tipuri de ciclare, clauza else nu se va executa dacă ciclul în cauză s-a terminat cu un break. Desigur, și alte cazuri de oprire timpurie a ciclării, precum un return sau lansarea (de la englezescul to raise) unei excepții, vor anula execuția clauzei else.

Toate acestea se exemplifică în următorul ciclu for, cu care căutăm numere prime:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'este egal cu', x, '*', n//x)
...             break
...     else:
...         # cade până aici fără să fi găsit vreun divizor
...         print(n, 'este număr prim')
...
2 este număr prim
3 este număr prim
4 este egal cu 2 * 2
5 este număr prim
6 este egal cu 2 * 3
7 este număr prim
8 este egal cu 2 * 4
9 este egal cu 3 * 3

(Da, acesta este codul corect. Uitați-vă îndeaproape: clauza else ține de bucla for și nu de instrucțiunea if.)

Este bine, atunci când vă gândiți la clauza else, să o socotiți ca făcând pereche cu if-ul din buclă. Pe măsură ce bucla este executată, ea va rula o secvență de tipul if/if/if/else. Acest if este în interiorul buclei și va fi întâlnit de un număr oarecare de ori. Dacă condiția asociată lui va avea valoarea logică adevărat la un moment dat, atunci se va produce un break. În schimb, dacă valoarea logică a condiției nu va fi niciodată adevărat, atunci se va executa clauza else din afara buclei.

Atunci când este folosită cu o instrucțiune de ciclare, clauza else seamănă mai degrabă cu clauza else a unei instrucțiuni try decât cu cea a unor instrucțiuni if: clauza else a unei instrucțiuni try va rula numai atunci când nu survine nicio excepție, pe când clauza else a unei bucle va rula atunci când nu intervine niciun break. Pentru detalii privind instrucțiunea try și excepțiile, vedeți Tratarea excepțiilor.

4.6. Instrucțiuni pass

Instrucțiunea pass nu realizează nimic. Ea poate fi folosită atunci când sintaxa limbajului cere introducerea unei instrucțiuni dar programul propriu-zis nu are nimic de executat. De exemplu:

>>> while True:
...     pass  # Ocupat-se așteaptă o întrerupere de la tastatură (Ctrl+C)
...

Se folosește în mod uzual la crearea unor clase minimale:

>>> class ClasaMeaVidă:
...     pass
...

Altă întrebuințare pentru pass este ca înlocuitor al corpului unei funcții sau al celui al unei instrucțiuni de selecție atunci când lucrați la un program nou, permițându-vă să gândiți programul la nivel abstract. Instrucțiunea pass este ignorată în mod silențios:

>>> def initlog(*args):
...     pass   # Nu uitați să o implementați!
...

4.7. Instrucțiuni match

O instrucțiune match preia o expresie și îi compară valoarea cu modelele date de blocuri case de cod succesive. Aceasta o face să semene, la prima vedere, cu o instrucțiune switch din C, Java sau JavaScript (și din multe alte limbaje), doar că ea este mai degrabă similară cu potrivirea după model (de la englezescul pattern matching) din limbaje precum Rust ori Haskell. Va fi executat numai primul model care se potrivește și tot atunci vor putea fi salvate în variabile diverse componente (cum ar fi itemi de secvență ori atribute de obiect) ale valorii expresiei.

În cea mai simplă formă, ea compară valoarea unei mărimi cu valorile uneia sau mai multor date literale. În exemplul care urmează este introdus un cod de răspuns HTTP (418) folosit ca păcăleală de întâi aprilie:

def http_error(stare):
    match stare:
        case 400:
            return "Cerere incorectă"
        case 404:
            return "Nu a fost găsită"
        case 418:
            return "Eu sunt un ceainic"
        case _:
            return "S-a întâmplat ceva cu conexiunea internet"

Observați ultimul bloc: „numele de variabilă” _ acționează ca un caracter de înlocuire (de la englezescul wildcard) care se va potrivi cu toate valorile. Dacă niciunul dintre case-uri nu se potrivește, niciuna din ramurile de cod nu se va executa.

Puteți combina mai mulți literali într-un singur model folosind | („sau”):

case 401 | 403 | 404:
    return "Nepermis"

Modelele pot semăna cu atribuirile cu despachetare (de la englezescul unpacking assignment), putând fi folosite la legarea variabilelor:

# punctul este un tuplu (x, y)
match punctul:
    case (0, 0):
        print("Originea")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Nu este punct")

Studiați exemplul cu atenție! Primul model are doi literali, și poate fi considerat ca o extensie a modelului literal prezentat înainte. Următoarele două modele, în schimb, combină o dată literală cu o variabilă, iar de variabilă se leagă o valoare din mărime (punctul). Cel de-al patrulea model captează două valori, ceea ce îl face conceptual echivalent atribuirii cu despachetare (x, y) = punctul.

Dacă folosiți clase pentru a vă structura datele în program, atunci veți putea utiliza numele clasei urmat de o listă de argumente asemănătoare celei a unui constructor, doar că având proprietatea de a salva atribute în variabile:

class Punctul:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def unde_este(punctul):
    match punctul:
        case Punctul(x=0, y=0):
            print("Originea")
        case Punctul(x=0, y=y):
            print(f"Y={y}")
        case Punctul(x=x, y=0):
            print(f"X={x}")
        case Punctul():
            print("Altundeva")
        case _:
            print("Nu e punct")

Puteți utiliza argumente poziționale la unele din clasele predefinite, cele care oferă o ordonare a atributelor lor (precum clasele-de-date). De asemenea, puteți defini poziții specifice pentru atributele din modele prin setarea atributului special __match_args__ pentru clasele dumneavoastră. Dacă acesta este setat la („x”, „y”), atunci următoarele modele sunt cu toatele echivalente (și toate leagă atributul y de variabila var):

Punctul(1, var)
Punctul(1, y=var)
Punctul(x=1, y=var)
Punctul(y=var, x=1)

Atunci când citim un model este recomandat să-l privim ca pe forma extinsă a ceea ce se pune la stânga unei atribuiri, respectiv să înțelegem ce variabilă va fi setată la ce valoare. O instrucțiune match poate face atribuiri numai pentru numele de sine stătătoare (precum var-ul de deasupra). Numelor-cu-punct (de la englezescul dotted name; ca, de exemplu, foo.bar, o expresie intraductibilă, vedeți foobar), numelor de atribute (x= și y= de mai sus) ori numelor de clase (pe care le recunoaștem după „(…)”-ul așezat după ele, ca în cazul lui Punctul de deasupra) nu li se fac niciodată atribuiri.

Modelele pot fi imbricate de câte ori dorim. De exemplu, dacă avem o listă scurtă de Puncte, căreia i-am adăugat __match_args__, îi putem aplica un match astfel:

class Punctul:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match puncte:
    case []:
        print("Niciun punct")
    case [Punctul(0, 0)]:
        print("Originea")
    case [Punctul(x, y)]:
        print(f"Un singur punct {x}, {y}")
    case [Punctul(0, y1), Punctul(0, y2)]:
        print(f"Două puncte pe axa Y la {y1}, {y2}")
    case _:
        print("Altceva")

Îi putem adăuga o clauză if, numită „gardă” (de la englezescul guard), unui model. Dacă valoarea logică a gărzii este fals, atunci match-ul trece la următorul bloc case. Captarea valorilor, trebuie remarcat, se va petrece înainte de evaluarea gărzii:

match punct:
    case Punctul(x, y) if x == y:
        print(f"Y=X la {x}")
    case Punctul(x, y):
        print(f"Nu e pe diagonală")

Alte caracteristici-cheie ale acestei instrucțiuni:

  • În calitate de atribuiri cu despachetare, tuplurile și modelele de liste au aceeași semnificație și reușesc să se potrivească unor secvențe arbitrare. O excepție importantă este aceea că ele nu fac potriviri pentru iteratori sau șiruri de caractere.

  • Modelele de secvențe suportă despachetări extinse: [x, y, *restul] și (x, y, *restul) se comportă similar cu atribuirile cu despachetare. Numele de după * poate fi inclusiv _, astfel că (x, y, *_) se va potrivi unei secvențe de cel puțin doi itemi fără a mai lega itemii rămași.

  • Modele de mapare: {"lățime_de_bandă": b, "latență": l} capturează valorile lui "lățime_de_bandă" și "latență" dintr-un dicționar. Spre deosebire de cazul modelelor de secvențe, cheile suplimentare vor fi ignorate. Este suportată și o despachetare de tipul **restul. (Dat fiind că despachetarea **_ ar fi redundantă, ea nu este permisă.)

  • Submodelele pot fi capturate folosind cuvântul-cheie as:

    case (Punctul(x1, y1), Punctul(x2, y2) as p2): ...
    

    va captura cel de-al doilea element al inputului ca p2 (numai dacă inputul este o secvență de două puncte)

  • Majoritatea datelor literale sunt comparate folosind egalitatea, totuși valorile unice (de la englezescul singleton) True, False și None se compară folosind identitatea.

  • Modelele pot folosi constante cu nume. Acestea trebuie să fie nume-cu-punct pentru a se evita interpretarea lor drept variabile de captare:

    from enum import Enum
    class Culoare(Enum):
        ROȘU = 'roșu'
        VERDE = 'verde'
        ALBASTRU = 'albastru'
    
    culoare = Culoare( \
    input("Alegeți între 'roșu', 'albastru' sau 'verde': "))
    
    match culoare:
        case Culoare.ROȘU:
            print("Văd roșu!")
        case Culoare.VERDE:
            print("Iarba este verde")
        case Culoare.ALBASTRU:
            print("Cântă-mi de inimă albastră :(")
    

Pentru explicații detaliate și alte exemple, puteți răsfoi PEP 636 care este redactat sub formă de tutorial.

4.8. Definirea funcțiilor

Puteți construi o funcție care să scrie toți termenii șirului lui Fibonacci care sunt mici decât o valoare dată:

>>> def fib(n):    # scriem termenii șirului lui Fibonacci mai mici ca n
...     """Afișez termenii șirului lui Fibonacci mai mici ca n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Acum apelăm funcția proaspăt definită:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

Cuvântul-cheie def introduce definiția unei funcții. El trebuie urmat de numele funcției și de lista cuprinsă între paranteze a parametrilor formali ai funcției. Instrucțiunile care alcătuiesc corpul funcției vor începe de pe linia următoare, și vor trebui scrise ca alineate.

Prima instrucțiune din corpul funcției poate fi o dată literală, opțională, sub forma unui șir de caractere; acest literal de tip șir de caractere este șirul de caractere al documentației funcției respective, așa-numitul docstring. (Mai multe despre docstring-uri se pot afla în secțiunea Șiruri de documentație.) Există unelte informatice care folosesc docstring-urile pentru a produce în mod automat documentație, atât online cât și tipărită, ori pentru a-i permite utilizatorului să răsfoiască, interactiv, codul-sursă; este o bună practică să inserați docstring-uri în codul pe care îl construiți, așadar, făceți-vă un obicei din aceasta.

La execuția unei funcții se construiește o nouă tabelă de simboluri ce va fi folosită pentru variabilele locale ale funcției. Mai precis, toate atribuirile făcute unor variabile în corpul unei funcții stochează valorile respective în tabela de simboluri locală; în ce le privește, referințele la variabile vor căuta mai întâi în tabela de simboluri locală, apoi în tabelele de simboluri locale ale funcțiilor în care este imbricată funcția, după care în tabela de simboluri globală și, în final, în tabela de nume predefinite. Astfel, nici variabilelor globale și nici variabilelor din funcțiile în care este imbricată funcția în cauză nu li se pot atribui în mod direct valori prin codul funcției (cu excepția, pentru variabilele globale, celor numite într-o instrucțiune global ori, pentru variabilele din funcțiile în care este imbricată funcția, a celor numite într-o instrucțiune nonlocal), chiar dacă pot exista referințe la ele.

Parametrii actuali (argumentele) dintr-un apel de funcție sunt introduși în tabela de simboluri locală a funcției apelate la apelul acesteia; astfel, argumentele sunt transmise prin valoare (unde valoarea este întotdeauna referința la un obiect, niciodată valoarea obiectului). [1] Când o funcție apelează altă funcție, ori se apelează recursiv pe ea-însăși, o tabelă de simboluri locale nouă va fi creată la apel.

Definiția unei funcții asociază numele funcției cu obiectul-funcție în tabela de simboluri curentă. Interpretorul recunoaște obiectul către care țintește numele ca fiind o funcție definită-de-utilizator. Și alte nume pot ținti către același obiect-funcție și pot fi, de aceea, utilizate la accesarea funcției:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

Dacă sunteți obișnuit cu alte limbaje de programare, atunci este posibil să considerați că fib nu poate fi funcție, ci procedură, deoarece ea nu returnează nicio valoare. În fapt, chiar și funcțiile care nu au nicio instrucțiune return returnează o valoare, numai că valoarea respectivă nu înseamnă mai nimic. Această valoare se numește None (adică Nimic, un nume predefinit). Afișarea valorii None este suprimată, în mod obișnuit, de către interpretor în cazul în care ea ar fi singura valoare afișată. O puteți vedea, dacă țineți neapărat, folosind print():

>>> fib(0)
>>> print(fib(0))
None

Este mai ușor să scrieți o funcție care returnează o listă de numere din șirul lui Fibonaci în loc de una care să afișeze aceste numere:

>>> def fib2(n):  # întoarce șirul lui Fibonacci de până la n
...     """Întoarce o listă conținând termenii din șirul lui
...     Fibonacci mai mici decât n."""
...     rezultat = []
...     a, b = 0, 1
...     while a < n:
...         rezultat.append(a)    # vezi mai jos
...         a, b = b, a+b
...     return rezultat
...
>>> f100 = fib2(100)    # apelul funcției
>>> f100                # afișarea rezultatului
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Acest exemplu, ca și până acum, probează câteva caracteristici noi ale Python-ului:

  • Instrucțiunea return întoarce o valoare a funcției. return neurmat de vreo expresie ca argument va întoarce None. De asemeni, la terminarea corpului de instrucțiuni al unei funcții se întoarce None.

  • Intrucțiunea rezultat.append(a) apelează o metodă a obiectului listă rezultat. Metoda este acea funcție care îi «aparține» unui obiect și care poartă numele de obiect.nume_de_metodă, în care obiect desemnează un obiect oarecare (poate fi inclusiv o expresie) iar nume_de_metodă este numele unei metode definite de către tipul obiectului. Unor tipuri diferite le corespund metode diferite. Metodele unor tipuri diferite pot avea același nume fără a da naștere la ambiguități. (Aveți posibilitatea să vă definiți propriile tipuri de obiecte și propriile metode, cu ajutorul claselor, vezi Classes.) Metoda append(), folosită în exemplu, se definește pentru obiectele listă; ea adaugă un element nou la sfârșitul listei. În exemplul de față, această adăugare este echivalentă cu rezultat = rezultat + [a], doar că se realizează mai eficient.

4.9. Mai multe despre definirea funcțiilor

Este posibil să definim funcții care să aibă număr variabil de argumente. Aceasta poate fi realizată în trei feluri, care pot fi combinate.

4.9.1. Valori implicite pentru argumente

Modalitatea cea mai utilă este să specificăm valori prestabilite pentru unul sau mai multe argumente. Se creează o funcție ce va putea fi apelată cu mai puține argumente decât au fost folosite la definirea ei. Cum ar fi:

def cere_permisiunea(promptul, încercări=4, \
    atenționare='Încercați din nou, vă rog!'):
    while True:
        răspuns = input(promptul)
        if răspuns in {'d', 'mda', 'da'}:
            return True
        if răspuns in {'n', 'nu', 'nț', 'nici pomeneală'}:
            return False
        încercări = încercări - 1
        if încercări < 0:
            raise ValueError('utilizatorul răspunde greșit')
        print(atenționare)

Această funcție poate fi apelată în mai multe moduri:

  • precizând numai argumentul obligatoriu: cere_permisiunea('Chiar vreți ieșiți?')

  • oferind și unul dintre argumentele opționale: cere_permisiunea('De acord cu suprascrierea fișierului?', 2)

  • ori introducând absolut toate argumentele: cere_permisiunea('De acord cu suprascrierea fișierului?', 2, 'Haideți, rog, numai da sau nu!')

Exemplul de față introduce și cuvântul-cheie in. El verifică dacă o secvență conține sau nu o anumită valoare.

Valorile prestabilite sunt evaluate chiar acolo unde se definește funcția în domeniul de valabilitate (de la englezescul scope) al definiției, așa că:

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

va afișa 5.

Avertizare importantă: Valoarea prestabilită se evaluează o singură dată. Acest fapt contează atunci când valoarea prestabilită este un obiect mutabil, precum o listă, un dicționar ori instanțe ale majorității claselor. De exemplu, funcția următoare pune laolaltă argumentele transmise ei în apeluri succesive:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

Se va afișa

[1]
[1, 2]
[1, 2, 3]

Dacă nu doriți ca valoarea prestabilită să se păstreze între apeluri succesive, atunci puteți rescrie funcția după cum urmează:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

4.9.2. Argumentele cuvânt-cheie

Funcțiile mai pot fi apelate și folosind argumente cuvânt-cheie de forma arg_cvc=valoare. De exemplu, funcția următoare, care face referire la un scheci Monty Python:

def papagal(voltaj, stare='țeapăn', acțiune='fâl-fâl', \
    tip='Albastrul de Norvegia'):
    print("-- Papagalul acesta n-are cum să", acțiune, end=' ')
    print("dacă băgați", voltaj, "de volți în el.")
    print("-- Ce mai penaj, ", tip)
    print("-- Doar că-i", stare, "!")

are un argument obligatoriu (voltaj) și trei argumente opționale (stare, acțiune și tip). Funcția poate fi apelată în oricare din modurile care urmează:

papagal(1000)                                         # 1 argument pozițional
papagal(voltaj=1000)                                  # 1 argument cuvânt-cheie
papagal(voltaj=1000000, acțiune='FÂÂÂL-FÂÂÂL')        # 2 argumente cuvânt-cheie
papagal(acțiune='FÂÂÂL-FÂÂÂL', voltaj=1000000)        # 2 argumente cuvânt-cheie
papagal('un milion', 'lipsit de viață', 'sară')       # 3 argumente poziționale
papagal('o mie', stare='pământ de flori')             # 1 pozițional, 1 cuvânt-cheie

în schimb, următoarele apeluri vor fi invalidate:

papagal()                     # lipsește argumentul obligatoriu
papagal(voltaj=5.0, 'mort')   # argument care nu e cuvânt-cheie
                              # după un argument cuvânt-cheie
papagal(110, voltaj=220)      # valoare duplicată a unui argument
papagal(actor='John Cleese')  # argument cuvânt-cheie necunoscut

Într-un apel de funcție, argumentele cuvânt-cheie le urmează argumentelor poziționale. Orice argument cuvânt-cheie trebuie să-i corespundă unuia din argumentele acceptate de funcție (de exemplu, actor nu este un argument valid pentru funcția papagal), ordinea acestor argumente nefiind importantă. Cerința privește și argumentele neopționale (astfel, papagal(voltaj=1000) este o exprimare validă). Niciunui argument nu îi va fi atribuită valoarea de mai multe ori. Iată un exemplu de formulare neacceptată din cauza acestei restricții:

>>> def funcție(a):
...     pass
...
>>> funcție(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: funcție() got multiple values for argument 'a'

Atunci când parametrul formal final este de forma **nume, lui i se va atribui un dicționar (vezi Mapping Types — dict) care să cuprindă toate argumentele cuvânt-cheie cu excepția celor corespunzând unui parametru formal. Acest fapt poate fi combinat cu un parametru formal exprimat ca *nume (care va fi descris în subsecțiunea următoare) și căruia i se atribuie un tuplu ce conține toate argumentele poziționale de după lista parametrilor formali (*nume trebuie să apară înaintea lui **nume.) De exemplu, să definim o funcție astfel:

def magazin_de_brânzeturi(fel, *argumente, **cuvinte_cheie):
    print("-- Aveți cumva", fel, "?")
    print("-- Scuze, am rămas fără", fel)
    for arg in argumente:
        print(arg)
    print("-" * 40)
    for cvc in cuvinte_cheie:
        print(cvc, ":", cuvinte_cheie[cvc])

Funcția poate fi apelată astfel:

magazin_de_brânzeturi("Limburger", "E tare apoasă, domnule.",
                      "Realmente, e tare, TARE apoasă, domnule.",
                      proprietar="Michael Palin",
                      client="John Cleese",
                      scheci="Cheese Shop Sketch")

și, firește, va afișa:

-- Aveți cumva Limburger ?
-- Scuze, am rămas fără Limburger
E tare apoasă, domnule.
Realmente, e tare, TARE apoasă, domnule.
----------------------------------------
proprietar : Michael Palin
client : John Cleese
scheci : Cheese Shop Sketch

Remarcați că ordinea în care argumentele cuvânt-cheie sunt afișate este garantată ca urmând ordinea în care aceste argumente cuvânt-cheie au fost inserate în apelul funcției.

4.9.3. Parametri speciali

Tipic, argumentele i se transmit unei funcții în Python fie prin poziție fie prin cuvinte-cheie date în mod explicit. Din motive de lizibilitate și performanță, modurile de transmitere a argumentelor au fost restrânse astfel încât unui dezvoltator de cod să-i fie suficient să se uite la definiția unei funcții pentru a ști dacă un argument trebuie transmis numai prin poziție, prin poziție sau prin cuvânt-cheie ori numai prin cuvânt-cheie.

Definiția unei funcții poate arăta ca aici:

def f(poz1, poz2, /, poz_sau_cvc, *, cvc1, cvc2):
      ----------     -----------     ----------
        |             |                  |
        |         Pozițional             |
        |            sau                 |
        |         cuvânt-cheie           |
        |                                - Numai cuvânt-cheie
         -- Numai pozițional

unde / și * sunt opționale. Dacă le folosiți, atunci aceste simboluri vor indica tipul de parametru prin chiar modul cum le sunt transmise argumentele funcției: numai pozițional, pozițional-sau-cuvânt-cheie ori numai cuvânt-cheie. Despre parametrii cuvânt-cheie spunem că sunt și parametri-cu-nume.

4.9.3.1. Argumente pozițional-sau-cuvânt-cheie

Dacă nici /, nici * nu sunt prezente în definiția funcției, atunci argumentele îi pot fi transmise acesteia atât prin poziție cât și prin cuvinte-cheie.

4.9.3.2. Parametri numai poziționali

Intrând puțin în detalii, să observăm că este posibil să marcăm anumiți parametri cu numai pozițional. Atunci când aceștia sunt numai poziționali, ordinea parametrilor contează și parametrii nu pot fi transmiși prin cuvinte-cheie. Parametrii numai poziționali sunt plasați înaintea lui / (bara oblică). /-ul este folosit pentru a separa din punct de vedere logic parametrii numai poziționali de restul parametrilor. Dacă nu există niciun / în definiția funcției, atunci nu există nici parametri numai poziționali ai acesteia.

Parametrii care îi urmează lui / pot fi poziționali-sau-cuvinte-cheie ori numai cuvinte-cheie.

4.9.3.3. Argumente numai cuvinte-cheie

Ca să marcăm parametrii cu numai cuvânt-cheie, adică să indicăm că acești parametri trebuie transmiși prin argumente cuvinte-cheie, vom plasa un * în lista argumentelor, chiar înainte de primul parametru numai cuvânt-cheie.

4.9.3.4. Exemple de funcții

Să luăm în considerare următoarele definiții de funcții demonstrative, cu grijă la marcajele / și *:

>>> def argumente_uzuale(arg):
...     print(arg)
...
>>> def argumente_numai_poziționale(arg, /):
...     print(arg)
...
>>> def argumente_numai_cuvinte_cheie(*, arg):
...     print(arg)
...
>>> def exemplu_de_combinații(numai_poz, /, tipice, *, numai_cvc):
...     print(numai_poz, tipice, numai_cvc)

Prima definiție de funcție, argumente_uzuale, cu forma cea mai des întâlnită, nu impune nicio restricție asupra felului în care o apelăm pe aceasta iar argumentele funcției pot fi transmise atât prin poziție cât și prin cuvânte-cheie:

>>> argumente_uzuale(2)
2

>>> argumente_uzuale(arg=2)
2

Cea de-a doua funcție, argumente_numai_poziționale, este restrânsă la utilizarea doar a parametrilor numai poziționali deoarece în definiția funcției apare un /:

>>> argumente_numai_poziționale(1)
1

>>> argumente_numai_poziționale(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: argumente_numai_poziționale() got some positional-only arguments passed as keyword arguments: 'arg'

A treia funcție, argumente_numai_cuvinte_cheie, permite doar argumente cuvinte-cheie, după cum ne indică acel * din definiția funcției:

>>> argumente_numai_cuvinte_cheie(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: argumente_numai_cuvinte_cheie() takes 0 positional arguments but 1 was given

>>> argumente_numai_cuvinte_cheie(arg=3)
3

Iar ultimul exemplu întrebuințează toate cele trei convenții de apel în aceeași definiție de funcție:

>>> exemplu_de_combinații(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exemplu_de_combinații() takes 2 positional arguments but 3 were given

>>> exemplu_de_combinații(1, 2, numai_cvc=3)
1 2 3

>>> exemplu_de_combinații(1, tipice=2, numai_cvc=3)
1 2 3

>>> exemplu_de_combinații(numai_poz=1, tipice=2, numai_cvc=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exemplu_de_combinații() got some positional-only arguments passed as keyword arguments: 'pos_only'

În sfârșit, să considerăm o definiție de funcție care poate provoca o coliziune de nume între argumentul pozițional nume și argumentul **cuvinte-cheie atunci când acesta din urmă va include nume pe post de cheie:

def foo(nume, **cuvinte_cheie):
    return 'nume' in cuvinte_cheie

Nu putem realiza niciun apel al funcției care să întoarcă True deoarece cuvântul-cheie 'nume' se va lega întotdeauna la primul parametru. De exemplu:

>>> foo(1, **{'nume': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'nume'
>>>

Însă, folosind / (plasat după argumentele numai poziționale), un asemenea apel este posibil deoarece acum putem distinge între nume ca parametru numai pozițional și cheia 'nume' a unui argument cuvânt-cheie:

>>> def foo(nume, /, **cuvinte_cheie):
...     return 'nume' in cuvinte_cheie
...
>>> foo(1, **{'nume': 2})
True

Altfel spus, numele parametrilor numai poziționali vor putea fi folosite în **cuvinte_cheie fără nicio ambiguitate.

4.9.3.5. De reținut

Modul de întrebuințare determină ce parametri să folosim la definiția unei funcții:

def f(poz1, poz2, /, poz_sau_cvc, *, cvc1, cvc2):

Recomandări:

  • Folosiți argumente numai poziționale dacă doriți ca numele parametrilor să nu le fie accesibile utilizatorului. Această întrebuințare ne este de folos atunci când numele parametrilor nu au nicio semnificație pentru program, atunci când dorim să impunem o anumită ordine a argumentelor la apelul funcției sau atunci când este nevoie să utilizăm câțiva parametri numai poziționali alături de niște cuvinte cheie arbitrare.

  • Folosiți argumente numai cuvinte-cheie atunci când numele au semnificație pentru program iar definiția funcției se va înțelege mai ușor dacă conține nume ori atunci când doriți să-i împiedicați pe utilizatori să se bazeze pe pozițiile argumentelor ce trebuie transmise.

  • În cazul unui API, folosiți argumente numai poziționale pentru a evita funcționarea defectuoasă a viitoarelor transformări ale API-ului atunci când numele vreunui parametru va fi modificat.

4.9.4. Liste arbitrare de argumente

La final, cea mai puțin utilizată variantă de definiție este cea în care precizăm că o funcție poate fi apelată cu un număr variabil de argumente. Aceste argumente vor fi încadrate într-un tuplu (vezi Tupluri și secvențe).

def înscrie_mai_mulți_itemi(fișier, despărțitor, *argumente):
    fișier.write(despărțitor.join(argumente))

După cum e de așteptat, aceste argumente variadice vor fi așezate la sfârșitul listei de parametri formali, dat fiind că ele vor cuprinde tot restul datelor de intrare care trebuie transmise funcției. Acei parametri formali care sunt poziționați după parametrul *argumente sunt argumente «numai cuvânt-cheie», ceea ce înseamnă că vor putea fi folosiți doar pe post de cuvinte-cheie și nu ca argumente poziționale.

>>> def concatenează(*argumente, despărțitor="/"):
...     return despărțitor.join(argumente)
...
>>> concatenează("Pământ", "Marte", "Venus")
'Pământ/Marte/Venus'
>>> concatenează("Pământ", "Marte", "Venus", despărțitor=".")
'Pământ.Marte.Venus'

4.9.5. Despachetarea listelor de argumente

Situația inversă va fi întâlnită atunci când argumentele care ne interesează ne vor fi date sub formă fie de listă, fie de tuplu și va trebui să le despachetăm pentru a fi folosite într-un apel de funcție care necesită argumente poziționale introduse separat. De exemplu, funcția predefinită range() necesită argumente start și stop transmise separat. Dacă acestea nu sunt disponibile separat, atunci vom înscrie în apelul funcției operatorul * pentru a despacheta argumentele fie din listă fie din tuplu:

>>> list(range(3, 6))            # apel tipic, cu argumente date separat
[3, 4, 5]
>>> argumente = [3, 6]
>>> list(range(*argumente))      # apel cu argumente despachetate dintr-o listă
[3, 4, 5]

La fel, dicționarele de date pot transmite argumente cuvinte-cheie cu ajutorul operatorului **:

>>> def papagal(voltaj, stare='țeapăn', acțiune='fâl-fâl'):
...     print("-- Papagalul acesta n-are cum să", acțiune, end=' ')
...     print("dacă băgați", voltaj, "de volți în el.", end=' ')
...     print("Zău că-i", stare, "!")
...
>>> d = {"voltaj": "patru milioane", "stare": "al naibii de dus", "acțiune": "FÂL-FÂL"}
>>> papagal(**d)
-- Papagalul acesta n-are cum să FÂL-FÂL dacă băgați patru milioane de volți în el. Zău că-i al naibii de dus !

4.9.6. Expresii lambda

Funcții anonime, cu cod de dimensiuni reduse, pot fi create folosind cuvântul-cheie lambda. Funcția care urmează va returna suma celor două argumente ale ei: lambda a, b: a+b. Funcțiile lambda pot fi utilizate oriunde se cer obiecte funcție. Ele sunt restrânse dpdv. sintactic la o singură expresie. Dpdv. semantic, ele sunt doar zahăr sintactic pudrat peste o definiție obișnuită de funcție. Aidoma definițiilor imbricate de funcții, funcțiile lambda pot face referire la variabile dintr-un domeniu de valabilitate care include definițiile funcțiilor lambda în cauză:

>>> def construiește_un_incrementor(n):
...     return lambda x: x + n
...
>>> f = construiește_un_incrementor(42)
>>> f(0)
42
>>> f(1)
43

Exemplul de deasupra întrebuințează o expresie lambda pentru a returna o funcție. Altă utilizare este dată de transmiterea unei funcții cu cod redus pe post de argument. Astfel, list.sort() folosește ca valoare a cheii key o funcție de sortare iar aceasta poate fi inclusiv o funcție lambda:

>>> perechi = [(1, 'unu'), (2, 'doi'), (3, 'trei'), (4, 'patru')]
>>> perechi.sort(key=lambda pereche: pereche[1])
>>> perechi
[(2, 'doi'), (4, 'patru'), (3, 'trei'), (1, 'unu')]

4.9.7. Șiruri de documentație

Iată câteva convenții privind conținutul și formatul șirurilor de documentație (în englezește, ca jargon, docstring).

Primul rând trebuie să fie întotdeauna un sumar restrâns, concis al scopului pe care îl are obiectul în cauză. Pentru concizie, el nu trebuie să menționeze nici numele și nici tipul obiectului, dat fiind că acestea vor putea fi aflate prin alte mijloace (excepție face cazul când numele este chiar verbul care descrie modul de operare al funcției). Ar fi bine ca acest rând să înceapă cu o majusculă și să se încheie cu punct.

Dacă șirul de documentație conține mai multe rânduri, atunci cel de-al doilea rând este bine să fie gol, separând vizual sumarul de restul descrierii. Rândurile care urmează, în șirul de documentație, ar trebui grupate într-unul sau mai multe paragrafe referitoare la convențiile de apel ale obiectului, la diversele complicații date de utilizarea sa șamd.

Parserul Python-ului nu elimină indentația dintr-o dată literală de tip șir de caractere care se întinde pe mai multe rânduri de program Python, așa că uneltele informatice care vor procesa documentația trebuie să elimine ele-însele indentația, dacă acest lucru este dorit. Eliminarea se pregătește folosind următoarea convenție. Primul rând care nu este gol de după primul rând al datei literale va determina lățimea indentației pentru întregul șir de documentație. (Nu putem întrebuința în această privință primul rând deoarece el este adiacent, în general, ghilimelor din deschiderea șirului astfel că indentația acestui rând nu se observă în cuprinsul șirului literal.) Un spațiu gol „echivalent” cu lățimea acestei indentații va fi eliminat de la începutul tuturor rândurilor datei literale. Rânduri care să fie indentate cu o indentație mai scurtă nu ar trebui să existe în șirul de documentație, dar dacă există, atunci întregul spațiu gol de la începutul lor este bine să fie eliminat. Lățimea spațiului gol trebuie testată folosind expansiunea tabulatorului (de 8 spații, în mod normal).

Iată un exemplu de docstring pe mai multe rânduri:

>>> def funcția_mea():
...     """Nu face nimica, dar precizează asta.
...
...     Zău, realmente, nu face nimica.
...     """
...     pass
...
>>> print(funcția_mea.__doc__)
Nu face nimica, dar precizează asta.

Zău, realmente, nu face nimica.

4.9.8. Adnotarea funcțiilor

Adnotarea funcțiilor reprezintă informația opțională, de meta-date, care se referă la tipurile datelor întrebuințate de către funcțiile definite de utilizator (vezi PEP 3107 și PEP 484 pentru mai multe detalii).

Adnotările sunt memorate în atributul __annotations__ al funcției sub formă de dicționar de date și nu au niciun efect asupra celorlalte componente ale funcției. Adnotările parametrilor se marchează prin două puncte așezate după numele parametrului, semnul de punctuație fiind urmat de o expresie a cărei evaluare va da valoarea adnotării. Adnotarea mărimilor returnate de funcție se definește prin ansamblul dat de constanta literală -> urmată de o expresie de evaluat, ansamblu ce va fi poziționat între lista parametrilor funcției și semnul de punctuație două puncte ce denotă finalul instrucțiunii def.

>>> def f(șuncă: str, ouă: str = 'ouă') -> str:
...     print("Adnotări:", f.__annotations__)
...     print("Argumente:", șuncă, ouă)
...     return șuncă + ' și ' + ouă
...
>>> f('carne presată')
Adnotări: {'șuncă': <class 'str'>, 'return': <class 'str'>, 'ouă': <class 'str'>}
Argumente: carne presată ouă
'carne presată și ouă'

4.10. Intermezzo: stilul în care scriem cod

Acum, când puteți începe să scrieți fragmente mai lungi și mai complexe de programe în Python, este momentul potrivit să discutăm despre stilul de scris cod. Majoritatea limbajelor permite redactarea (ori, mai pe scurt, formatarea) în stiluri diferite; unele sunt mai lizibile ca altele. A ușura citirea de către alții a codului dumneavoastră este întotdeauna o idee bună, iar adoptarea unui stil de scriere prezentabil se dovedește de mare ajutor în acest scop.

În cazul Python-ului, PEP 8 s-a impus ca ghidul stilistic la care a aderat majoritatea proiectelor; acesta promovează un stil de scriere a codului ușor citibil și plăcut ochiului. Orice programator în Python ar trebui să îl citească la un moment dat; iată un extras al celor mai importante elemente:

  • Folosiți indentări de 4 spații goale, nu apelați la tabulator.

    4 spații goale constituie un compromis bun între indentarea îngustă (care permite imbricarea pe mai multe niveluri) și indentarea lată (mai ușor de citit).

  • Desfășurați rândurile de cod astfel încât ele să nu depășească 79 de caractere.

    Aceasta le servește utilizatorilor cu terminale de dimensiuni mici și permite așezarea unul lângă celălalt a mai multor fișiere cu cod pe un ecran mai lat.

  • Folosiți rânduri goale pentru a separa funcțiile și clasele, precum și blocurile mai mari de cod din corpul funcțiilor.

  • Atunci când este posibil, plasați comentariile pe rânduri dedicate lor.

  • Utilizați docstring-uri.

  • Inserați spații goale în jurul operatorilor și după virgule, dar nu și direct în constructele cu paranteze: a = f(1, 2) + g(3, 4).

  • Denumiți-vă clasele și funcțiile într-un mod sistematic; convenția este de a folosi ScriereaCămilăCareÎncepeCuMajusculă pentru clase și litere_mici_despărțite_de_bara_jos pentru funcții și metode. Întrebuințați întotdeauna self pe post de nume pentru cel dintâi argument al unei metode (vezi A First Look at Classes pentru mai multe informații despre clase și metode).

  • Nu utilizați codificări de caractere sofisticate dacă programul pe care îl scrieți este destinat utilizării în medii internaționale. Codificarea prestabilită a Python-ului, UTF-8, ori chiar și simpla codificare ASCII, funcționează de minune în aproape orice caz.

  • De asemeni, nu întrebuințați caractere non-ASCII în identificatori dacă există o oricât de neînsemnată șansă ca persoane care nu vă vorbesc limba să aibă de citit ori de întreținut codul pe care îl scrieți.

Note de sunsol