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ță "if" ...
"elif" ... "elif" ... 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 *iterabil*, 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!
   ...

În acest din urmă caz, mulți programatori utilizează literalul
*elipsă*, "...", în locul lui "pass". Acest literal nu dispune de
nicio semnificație aparte în Python și nici nu se regăsește în
definiția limbajului (puteți folosi orice expresie constantă în locul
său), doar că "..." se întrebuințează în mod convențional drept
înlocuitorul unui corp de funcție. Vedeți The Ellipsis Object.


4.7. Instrucțiuni "match"
=========================

A "match" statement takes an expression and compares its value to
successive patterns given as one or more case blocks.  This is
superficially similar to a switch statement in C, Java or JavaScript
(and many other languages), but it's more similar to pattern matching
in languages like Rust or Haskell. Only the first pattern that matches
gets executed and it can also extract components (sequence elements or
object attributes) from the value into variables. If no case matches,
none of the branches is executed.

Î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"

Note the last block: the "variable name" "_" acts as a *wildcard* and
never fails to match.

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".

* The statement "result.append(a)" calls a *method* of the list object
  "result".  A method is a function that 'belongs' to an object and is
  named "obj.methodname", where "obj" is some object (this may be an
  expression), and "methodname" is the name of a method that is
  defined by the object's type. Different types define different
  methods.  Methods of different types may have the same name without
  causing ambiguity.  (It is possible to define your own object types
  and methods, using *classes*, see Clase) The method "append()" shown
  in the example is defined for list objects; it adds a new element at
  the end of the list.  In this example it is equivalent to "result =
  result + [a]", but more efficient.


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 să 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, vă 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 Tipuri de asociere -- 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).

*Annotations* are stored in the "__annotations__" attribute of the
function as a dictionary and have no effect on any other part of the
function.  Parameter annotations are defined by a colon after the
parameter name, followed by an expression evaluating to the value of
the annotation.  Return annotations are defined by a literal "->",
followed by an expression, between the parameter list and the colon
denoting the end of the "def" statement.  The following example has a
required argument, an optional argument, and the return value
annotated:

   >>> 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 Prima privire aruncată asupra claselor
  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 subsol ]-

[1] De fapt, *apel prin referință la obiect* ar fi o descriere mai
    potrivită, dat fiind că, dacă transmitem un obiect mutabil, atunci
    apelantul va vedea orice modificare făcută de apelat (cum ar fi
    niște itemi noi inserați într-o listă).
