Sala de studiu Karinoya

Calificări · Laboratorul de promovare Cloud / AI / Python

Funcții, module și intrare/ieșire

Puteți citi întrebările și explicațiile în Română. Lecțiile (articolele explicative) sunt disponibile doar în japoneză.

Vedeți versiunea în japoneză (cu lecții) →

Î1 | Primirea argumentelor

Următoarea funcție, cu un parametru precedat de asterisc, este apelată doar cu argumente poziționale. Ce se afișează?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, 3, 4, 5))
  1. (1, 3, (4, 5), None)
  2. (1, 3, (4, 5), {})
  3. (1, 2, (3, 4, 5), {})
  4. (1, 3, [4, 5], {})
Răspuns corectB. (1, 3, (4, 5), {})

Argumentele poziționale se asociază în ordine, de la stânga, deci a primește 1, iar b primește 3. b are o valoare implicită de 2, dar cum i s-a pasat o valoare, aceasta are prioritate. Valorile care nu au mai putut fi asociate, 4 și 5, sunt grupate ca tuplu în args, cel cu un singur asterisc, devenind (4, 5). Nu s-a pasat niciun argument cu nume, deci kwargs este dicționarul gol {}, nu None. Dacă s-ar crede că b păstrează valoarea implicită 2, s-ar ajunge la (1, 2, (3, 4, 5), {}), dar argumentele poziționale au prioritate față de valorile implicite. În args ajunge un tuplu, nu o listă, deci nu apare afișat cu paranteze pătrate.

Î2 | Specificare prin nume

Un nume care nu apare în definiție este pasat prin cuvânt-cheie în următorul apel. Ce se afișează?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, x=9))
  1. (1, 9, (), {})
  2. (1, 2, (), {'x': 9})
  3. (1, 2, (9,), {})
  4. (1, 2, {'x': 9}, {})
Răspuns corectB. (1, 2, (), {'x': 9})

Există un singur argument pozițional, deci a primește 1, iar cum b nu a fost pasat, rămâne la valoarea implicită 2. Parametrul cu numele x nu există în definiție, deci argumentul cu cuvânt-cheie x=9 ajunge în kwargs, cel cu doi asteriscuri, ca {'x': 9}. Nu au rămas argumente poziționale în plus, deci args este tuplul gol (). Dacă s-ar crede că x=9 ajunge în b, s-ar obține (1, 9, (), {}), dar un argument cu nume ajunge doar la parametrul al cărui nume se potrivește. Dacă s-ar crede că 9 ajunge în args, s-ar obține (1, 2, (9,), {}), dar o valoare pasată cu nume nu se transformă în argument pozițional în exces. Dacă se confundă rolul celor doi asteriscuri, crezând că unul primește dicționarul și celălalt tuplul, s-ar putea vedea eronat {'x': 9} în args și kwargs gol.

Î3 | Lista implicită vidă

Următoarea funcție, cu o listă vidă ca valoare implicită, este apelată de 3 ori. Care sunt cele 3 linii afișate?

def add(x, lst=[]):    lst.append(x)    return lstprint(add(1))print(add(2))print(add(3))
  1. Se afișează, de sus în jos, None / None / None
  2. Se afișează, de sus în jos, [1] / [1, 2] / [1, 2, 3]
  3. Se afișează, de sus în jos, [1] / [2] / [3]
  4. Se afișează, de sus în jos, [1] / [1, 2] / [2, 3]
Răspuns corectB. Se afișează, de sus în jos, [1] / [1, 2] / [1, 2, 3]

Valoarea implicită [] este creată o singură dată, la execuția liniei def, iar acea aceeași listă rămâne salvată în obiectul funcției. Nu se creează o listă vidă nouă la fiecare apel, deci rezultatele lui append rămân de la un apel la altul, iar lista crește: [1], [1, 2], [1, 2, 3]. Dacă s-ar crede că fiecare apel primește o listă nouă, s-ar obține [1] / [2] / [3], dar acesta ar fi comportamentul dacă lst=None ar fi valoarea implicită, iar lista s-ar recrea în corpul funcției. Dacă corpul ar fi scris greșit ca return lst.append(x), ar apărea de trei ori None, valoarea returnată de append. De notat că, scris pe o singură linie, print(add(1), add(2), add(3)), toate cele trei apeluri indică aceeași listă, deci se afișează de trei ori [1, 2, 3], forma finală după ce toate evaluările s-au încheiat.

Î4 | Partajarea valorii implicite

După 3 apeluri, se afișează un atribut al obiectului funcției. Ce se afișează?

def add(x, lst=[]):    lst.append(x)    return lstadd(1)add(2)add(3)print(add.__defaults__)
  1. Rezultatul este de forma ((1, 2, 3),)
  2. Rezultatul este de forma ([],)
  3. Rezultatul este de forma [[1, 2, 3]]
  4. Rezultatul este de forma ([1, 2, 3],)
Răspuns corectD. Rezultatul este de forma ([1, 2, 3],)

Obiectul funcției păstrează valorile implicite ca tuplu, în atributul __defaults__. Dintre parametri, doar lst are valoare implicită, deci devine un tuplu cu un singur element, afișat cu virgulă la sfârșit. Conținutul acelui element este chiar lista creată o singură dată la linia def, iar rezultatul celor 3 append-uri din apeluri rămâne acolo, deci devine ([1, 2, 3],). Dacă nu s-ar fi apelat funcția deloc, ar fi ([],), dar faptul că apelurile își lasă urma aici este exact pericolul unui argument implicit mutabil. __defaults__ însuși este tuplu, deci nu se afișează cu paranteze pătrate, iar lista din interior nu devine tuplu.

Î5 | Evaluarea valorii implicite

O variabilă este folosită ca valoare implicită, iar apoi acea variabilă este modificată. Care sunt cele 2 linii afișate?

i = 5def h(x=i):    return xi = 10print(h())print(h(i))
  1. 5, apoi 5
  2. 5, apoi 10
  3. 10, apoi 10
  4. 10, apoi 5
Răspuns corectB. 5, apoi 10

Expresia unei valori implicite nu se evaluează la fiecare apel al funcției, ci o singură dată, în momentul în care se execută linia def. La acel moment, i este 5, deci 5 este salvat în obiectul funcției. Chiar dacă i este ulterior schimbat în 10, valoarea implicită deja salvată nu se schimbă, deci h() returnează 5. Pe de altă parte, h(i) citește i chiar în momentul apelului și îl pasează, deci returnează valoarea curentă, 10. Dacă s-ar crede că valoarea implicită se evaluează la fiecare apel, ambele linii ar deveni 10; dacă s-ar crede că și valoarea pasată se înlocuiește cu cea implicită, ambele linii ar deveni 5. Și capcana argumentului implicit mutabil provine din aceeași regulă.

Î6 | Ordinea parametrilor

Următorul cod este salvat într-un fișier și rulat cu python3. Ce se întâmplă?

def g(a=1, b):    return a + bprint(g(2, 3))
  1. Se produce SyntaxError la momentul definirii, fără să se execute nicio linie
  2. Se afișează 4, iar execuția se încheie normal, până la capăt
  3. Se produce TypeError la momentul apelării, iar execuția se oprește acolo
  4. Se afișează 5, iar execuția se încheie normal, până la capăt
Răspuns corectA. Se produce SyntaxError la momentul definirii, fără să se execute nicio linie

Nu este permis ca, după un parametru cu valoare implicită, să urmeze un parametru fără valoare implicită. Această regulă este verificată la nivel sintactic, deci, la rularea fișierului, apare mai întâi SyntaxError, cu mesajul non-default argument follows default argument. Analiza sintactică se face pentru întregul fișier înainte de execuție, deci nici măcar linia cu print nu se execută. Este un moment de detecție complet diferit de un TypeError cauzat de un apel greșit. Apelul g(2, 3) ar corespunde ca număr de parametri, deci dacă definiția ar fi corectă, ar apărea 5, dar execuția nu ajunge până acolo. Forma corectă este să se pună parametrul cu valoare implicită la urmă, ca în def g(b, a=1).

Î7 | Valoarea returnată de o funcție

Se apelează, ca mai jos, două funcții care diferă prin prezența sau absența unui return. Ce se afișează?

def one():    passdef two():    return 1, 2print(one(), type(two()).__name__)
  1. None tuple
  2. None list
  3. () list
  4. () tuple
Răspuns corectA. None tuple

one se termină fără să treacă niciodată printr-un return, deci valoarea returnată este None. Nu se returnează nici un tuplu gol, nici un șir vid. two are după return valorile 1 și 2, separate prin virgulă, dar acestea nu reprezintă returnarea a două valori, ci returnarea unui singur tuplu, (1, 2). Prin urmare, type(two()).__name__ este tuple. Dacă s-ar crede că valorile multiple se strâng într-o listă, ar rezulta list, dar o expresie cu virgule formează un tuplu. Blocajele apărute la folosirea valorii returnate de o funcție care doar afișează sunt, de multe ori, cauzate exact de acest None.

Î8 | Apel cu despachetare

O listă precedată de asterisc este folosită la apel, în codul următor. Ce se afișează?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)vals = [1, 2, 3]print(f(*vals))
  1. ([1, 2, 3], 2, (), {})
  2. (1, 2, (3,), None)
  3. (1, 2, [3], {})
  4. (1, 2, (3,), {})
Răspuns corectD. (1, 2, (3,), {})

Dacă la apel se pune un singur asterisc înaintea unei liste, conținutul ei este distribuit ca argumente poziționale, unul câte unul. Astfel, a primește 1, b primește 2, iar 3, rămas în plus, este grupat ca tuplu în args, cel cu un asterisc, devenind (3,). Fiind un tuplu cu un singur element, apare cu virgulă la sfârșit. Nu s-au pasat argumente cu nume, deci kwargs este dicționarul gol {}, nu None. Dacă se uită asteriscul și se scrie f(vals), lista în sine ajunge la primul parametru, rezultând ([1, 2, 3], 2, (), {}) — nu apare nicio eroare, dar rezultatul se schimbă complet. În args ajunge un tuplu, deci nu apare afișat cu paranteze pătrate.

Î9 | Tratarea adnotărilor de tip

Următoarea funcție este apelată cu o valoare de alt tip decât cel scris în adnotare. Ce se întâmplă?

def ann(a: int, b: str = "x") -> bool:    return aprint(ann("文字列", 999))print(ann.__annotations__['a'])
  1. Pe prima linie apare șir de caractere, iar pe a doua <class 'int'>
  2. Pe prima linie apare TypeError, semnalând că nu se poate pasa un tip diferit de adnotare
  3. Pe prima linie apare șir de caractere, iar pe a doua int
  4. Pe prima linie apare ValueError, semnalând că nu se poate converti la int
Răspuns corectA. Pe prima linie apare șir de caractere, iar pe a doua <class 'int'>

Adnotările sunt doar înregistrate în dicționarul __annotations__ al obiectului funcției; tipul nu este verificat la execuție. Deși a este adnotat cu int, i se poate pasa un șir de caractere; deși b este adnotat cu str, i se poate pasa un întreg. Deși valoarea returnată este adnotată cu bool, ceea ce se returnează efectiv este exact șirul de caractere primit. Prin urmare, pe prima linie apare direct șir de caractere. Ceea ce se află în __annotations__['a'] de pe a doua linie este obiectul de tip int, deci afișat cu print apare sub forma <class 'int'>. Nu apar doar cele 3 litere int. Pentru a verifica efectiv tipurile, e nevoie de un instrument dedicat de verificare, sau de o verificare scrisă manual în corpul funcției.

Î10 | Corectarea valorii implicite

Următorul cod a fost rescris cu valoarea implicită None. Care sunt cele 3 linii afișate?

def add2(x, lst=None):    if lst is None:        lst = []    lst.append(x)    return lstprint(add2(1))print(add2(2))print(add2(3, [9]))
  1. Se afișează, de sus în jos, [1] / [1, 2] / [9, 3]
  2. Se afișează, de sus în jos, [1] / [2] / [9, 3]
  3. Se afișează, de sus în jos, [1] / [2] / [3, 9]
  4. Se afișează, de sus în jos, [1] / [2] / [3]
Răspuns corectB. Se afișează, de sus în jos, [1] / [2] / [9, 3]

Practica standard este să se folosească None ca valoare implicită, iar la începutul corpului să se verifice dacă este None, creând atunci o listă nouă și vidă. None este o valoare care nu se poate modifica, deci, chiar dacă e evaluat o singură dată, nu apare problema partajării. La primul și al doilea apel, unde lst este omis, se creează de fiecare dată câte o listă nouă și vidă, deci rezultă [1] și [2], fără ca elementele apelului anterior să rămână. La al treilea apel, apelantul pasează [9], deci lst is None devine fals, iar 3 se adaugă la sfârșitul listei primite, rezultând [9, 3]. Dacă s-ar fi păstrat lst=[] fără rescriere, la al doilea apel, unde argumentul e omis, ar rămâne elementul apelului anterior, rezultând [1, 2]. Dacă s-ar ignora lista pasată de apelant și s-ar crea mereu o listă nouă, ar rezulta [1] / [2] / [3], pierzându-se tocmai avantajul acestei practici, „dacă se pasează ceva, se folosește acel ceva”. Dacă în loc de append, care adaugă la sfârșit, s-ar folosi insert(0, x), care inserează la început, al treilea apel ar deveni [3, 9].

Î11 | Ordinea de căutare a numelor

Care este ordinea corectă LEGB, în care Python caută un nume de variabilă?

  1. Se caută în ordinea: funcția în execuție, nivelul de vârf al modulului, funcția exterioară, predefinit
  2. Se caută în ordinea: funcția în execuție, funcția exterioară, nivelul de vârf al modulului, predefinit
  3. Se caută în ordinea: nivelul de vârf al modulului, predefinit, funcția în execuție, funcția exterioară
  4. Se caută în ordinea: predefinit, nivelul de vârf al modulului, funcția exterioară, funcția în execuție
Răspuns corectB. Se caută în ordinea: funcția în execuție, funcția exterioară, nivelul de vârf al modulului, predefinit

LEGB este acronimul de la Local, Enclosing, Global, Built-in și indică o căutare de la cel mai interior nivel spre exterior. Se caută mai întâi în interiorul funcției în execuție, apoi în funcția exterioară care o cuprinde, apoi la nivelul de vârf al modulului și, în cele din urmă, printre numele predefinite precum len sau print. Căutarea se oprește de îndată ce este găsit ceva, deci, dacă interiorul folosește același nume, numele exterior este ascuns. Dacă ordinea ar fi de la exterior spre interior, o variabilă locală creată în interiorul funcției ar fi învinsă de o variabilă cu același nume de la nivelul de vârf al modulului, ceea ce nu corespunde comportamentului real. O greșeală frecventă este inversarea ordinii Global și Enclosing, dar funcția exterioară este mai interioară decât nivelul de vârf al modulului.

Î12 | Atribuire locală

Se face, în interiorul unei funcții, o atribuire la un nume identic cu unul din exterior. Care sunt cele 2 linii afișate de codul următor?

x = 'global'def f():    x = 'local'    print(x)f()print(x)
  1. local, apoi local
  2. local, apoi global
  3. global, apoi global
  4. global, apoi local
Răspuns corectB. local, apoi global

O atribuire făcută la un nume în interiorul unei funcții creează o variabilă locală nouă cu acel nume, fără să afecteze deloc variabila cu același nume de la nivelul de vârf al modulului. print(x) din interiorul funcției citește x-ul local, deci afișează local, dar print(x) de după ieșirea din funcție citește x-ul de la nivelul de vârf al modulului, deci afișează global. Dacă s-ar crede că atribuirea din interiorul funcției ajunge și în exterior, ambele linii ar deveni local, dar pentru asta ar fi necesar să se declare global x la începutul funcției. Invers, dacă s-ar crede că atribuirea din interior este ignorată și se citește tot valoarea din exterior, ambele linii ar deveni global, dar, odată făcută atribuirea, variabila locală chiar există. Această asimetrie — citirea ajunge până în exterior, dar scrierea nu — este de bază pentru domeniul de vizibilitate din Python.

Î13 | nonlocal

O funcție interioară încearcă să modifice o variabilă din exterior, în codul următor. Ce se afișează?

def outer():    n = 1    def inner():        nonlocal n        n = 2    inner()    return ndef outer2():    n = 1    def inner():        n = 2    inner()    return nprint(outer(), outer2())
  1. 1 2
  2. 2 2
  3. 2 1
  4. 1 1
Răspuns corectC. 2 1

Funcția inner din outer are scris nonlocal n, deci o atribuire la n ajunge la n-ul funcției exterioare imediate, outer. După apelul lui inner, n-ul returnat de outer devine 2. Funcția inner din outer2 nu are declarația, deci n = 2 doar creează o variabilă locală nouă în inner, iar n-ul lui outer2 rămâne neschimbat, 1. Prin urmare, se afișează 2 1. Dacă s-ar crede că ambele modificări ajung în exterior, s-ar afișa 2 2; dacă s-ar crede că niciuna nu ajunge, s-ar afișa 1 1, dar diferența este produsă exact de acea singură linie cu nonlocal. De notat că nonlocal nu poate fi folosit dacă în funcția exterioară nu există deja o variabilă cu același nume, și nici nu poate indica o variabilă de la nivelul de vârf al modulului. Pentru a modifica o variabilă de la nivelul de vârf, se folosește global.

Î14 | Eroare de nume nelegat

Un nume care are o atribuire pe un rând ulterior este citit pe un rând anterior. Ce se întâmplă la rularea următorului cod?

z = 100def bad():    print(z)    z = 1bad()
  1. Se produce UnboundLocalError, fără să se afișeze nimic, iar execuția se oprește
  2. Se afișează 100, apoi execuția se încheie normal, până la capăt
  3. Se afișează None, apoi execuția se încheie normal, până la capăt
  4. Se afișează 1, apoi execuția se încheie normal, până la capăt
Răspuns corectA. Se produce UnboundLocalError, fără să se afișeze nimic, iar execuția se oprește

Python decide, chiar la compilarea corpului unei funcții, că orice nume care are o atribuire în acea funcție este variabilă locală. Această decizie se ia pentru întreaga funcție, indiferent de ordinea rândurilor. În funcția bad, cum există o atribuire z = 1 pe un rând ulterior, z este tratat ca variabilă locală a acestei funcții. Prin urmare, la momentul print(z) nu există încă nicio valoare atribuită, deci apare UnboundLocalError. Formularea exactă a mesajului acestei excepții diferă în funcție de versiunea Python, dar numele tipului rămâne UnboundLocalError. Chiar dacă există un z = 100 la nivelul de vârf, acesta nu este referit. Dacă s-ar crede că, fiind înainte de atribuire, se citește exteriorul, ar rezulta 100, dar Python nu funcționează astfel. Nici o variabilă locală fără valoare nu devine None.

Î15 | Declarația global

Se compară, în codul următor, o funcție cu declarație global și una fără. Ce se afișează?

n = 10def f():    global n    n = 20    return ndef g():    n = 30    return nprint(f(), g(), n)
  1. 20 30 30
  2. 20 30 20
  3. 20 30 10
  4. 10 30 10
Răspuns corectB. 20 30 20

f are scris global n, deci n = 20 modifică n-ul de la nivelul de vârf al modulului. f însuși returnează 20, iar n-ul de la nivelul de vârf devine și el 20. g nu are declarația, deci n = 30 doar creează o variabilă locală în g, fără să afecteze n-ul de la nivelul de vârf. g returnează propriul său n local, 30. n-ul citit de ultimul print este cel de la nivelul de vârf, care, după modificarea făcută de f, este 20. Dacă s-ar crede că și atribuirea din g ajunge în exterior, s-ar obține 20 30 30; dacă s-ar crede că global nu are efect, s-ar obține 20 30 10. Trebuie reținut și că declarația global este o instrucțiune care spune „această atribuire să ajungă la nivelul de vârf” — pentru simpla citire, declarația nu este necesară.

Î16 | Efectul lui __all__

S-a pregătit fișierul mymod.py de mai jos, iar într-un alt fișier s-a scris from mymod import *. Ce nume ajung la partea apelantă?

# mymod.py__all__ = ['pub']pub = 'P'other = 'O'def hello():    return 'H'
  1. Toate cele trei — pub, other și hello — sunt aduse
  2. Sunt aduse pub și hello, dar not other
  3. Sunt aduse other și hello, dar nu pub
  4. Este adus doar pub, iar other și hello nu sunt aduse
Răspuns corectD. Este adus doar pub, iar other și hello nu sunt aduse

Importul cu asterisc aduce, în mod normal, toate numele care nu încep cu underscore, dar dacă în modul există o listă de șiruri numită __all__, se aduc doar numele enumerate acolo. În acest exemplu, __all__ conține doar pub, deci nici other, nici hello nu sunt aduse. other nu începe cu underscore, dar, câtă vreme __all__ există, lista sa are prioritate. Nu există niciun tratament special pentru funcții, deci nici hello nu este adus. __all__ are efect doar la importul cu asterisc — importul explicit al unui nume, ca în from mymod import hello, sau folosirea sub forma import mymod urmată de mymod.hello(), nu sunt afectate.

Î17 | Valoarea lui __name__

Pentru fișierul mymod2.py de mai jos, care este combinația corectă a valorii lui __name__ la rularea directă cu python3 mymod2.py, respectiv la import cu import mymod2 dintr-un alt fișier?

# mymod2.pyprint('mymod2:', __name__)if __name__ == '__main__':    print('直接実行')
  1. La rulare directă devine mymod2, la import devine __main__
  2. Atât la rulare directă, cât și la import, devine __main__
  3. La rulare directă devine __main__, la import devine mymod2
  4. Atât la rulare directă, cât și la import, devine mymod2
Răspuns corectC. La rulare directă devine __main__, la import devine mymod2

Atunci când fișierul este rulat direct cu python3, __name__ primește valoarea '__main__'. Atunci când este importat dintr-un alt fișier, primește numele fișierului fără extensie, 'mymod2'. Prin urmare, la rulare directă apar cele 2 rânduri mymod2: __main__ și rulare directă, iar la import apare doar un singur rând, mymod2: mymod2. Construcția if __name__ == '__main__': folosește exact această diferență pentru a separa, în mod standard, „procesarea care trebuie rulată doar la execuția directă”. Dacă cele două s-ar inversa, la import ar rula procesarea de verificare gândită pentru rularea directă, ceea ce ar contrazice scopul acestei construcții. Dacă în ambele cazuri valoarea ar fi identică, nu ar mai fi posibilă deloc această separare.

Î18 | sys.argv

Fișierul args.py de mai jos este rulat cu comanda python3 args.py foo bar. Care sunt cele 2 linii afișate?

# args.pyimport sysprint(len(sys.argv))print(sys.argv[1:])
  1. 2, apoi ['foo', 'bar']
  2. 3, apoi ['args.py', 'foo']
  3. 3, apoi ['foo', 'bar']
  4. 2, apoi ['args.py', 'foo']
Răspuns corectC. 3, apoi ['foo', 'bar']

sys.argv este o listă cu valorile pasate din linia de comandă, iar la începutul ei se află chiar numele scriptului. În acest exemplu devine o listă cu 3 elemente, precum ['args.py', 'foo', 'bar'], deci len este 3. sys.argv[1:], care extrage de la indexul 1 în sus, devine ['foo', 'bar'], fără numele scriptului. Dacă s-ar crede că lungimea este direct numărul de argumente pasate, s-ar obține 2, uitându-se de numărat numele scriptului de la început. A crede că sys.argv[1:] devine ['args.py', 'foo'] corespunde unei confundări a poziției de start a feliei. Rulat fără niciun argument, lungimea este 1, iar sys.argv[1:] este o listă vidă.

Î19 | Reîncărcarea unui modul

Care este comportamentul corect atunci când același modul este importat de două ori în cadrul unui program?

  1. A doua oară corpul modulului este recitit de la zero și reexecutat
  2. A doua oară se creează un obiect diferit, iar starea nu mai este partajată
  3. A doua oară se folosește cache-ul, iar corpul modulului nu se reexecută
  4. A doua oară se produce ImportError, deoarece același nume nu poate fi folosit decât o singură dată
Răspuns corectC. A doua oară se folosește cache-ul, iar corpul modulului nu se reexecută

Un modul, odată încărcat, este păstrat în cache în dicționarul sys.modules. Al doilea import doar leagă din nou numele de același obiect de modul, deja înregistrat acolo, deci nu are loc nici recitirea fișierului, nici reexecutarea corpului. Se poate confirma acest lucru punând un print la nivelul de vârf al modulului: chiar dacă este importat de două ori, apare o singură dată. Chiar dacă importul se face sub un alt nume, se indică tot același obiect. Prin urmare, dacă se modifică o variabilă a unui modul, valoarea modificată se vede și din alte locuri care importă acel modul. Importul aceluiași nume de două ori nu produce eroare; dacă se dorește, în timpul dezvoltării, o recitire efectivă a corpului, trebuie folosite explicit funcțiile din importlib.

Î20 | Detectarea mediului virtual

Care este metoda potrivită pentru a determina dacă Python-ul care rulează acum se află într-un mediu virtual creat cu venv?

  1. Dacă sys.prefix nu este definit, se poate determina că se află într-un mediu virtual
  2. Dacă sys.prefix și sys.base_prefix coincid, se poate determina că se află într-un mediu virtual
  3. Dacă sys.base_prefix este definit, se poate determina că se află într-un mediu virtual
  4. Dacă sys.prefix și sys.base_prefix diferă, se poate determina că se află într-un mediu virtual
Răspuns corectD. Dacă sys.prefix și sys.base_prefix diferă, se poate determina că se află într-un mediu virtual

sys.prefix indică locul unde este instalat Python-ul care rulează acum, iar sys.base_prefix indică locul Python-ului original din care provine. În afara unui mediu virtual, ambele au aceeași valoare, dar în interiorul unui mediu creat cu python3 -m venv, sys.prefix indică directorul mediului creat, în timp ce sys.base_prefix continuă să indice Python-ul original, deci cele două diferă. Prezența sau absența acestei diferențe este indiciul folosit pentru determinare. Ideea că, dacă coincid, s-ar afla într-un mediu virtual, este condiția inversă. sys.base_prefix este întotdeauna definit, chiar și în afara unui mediu virtual, deci prezența sau absența definirii nu poate servi la determinare. La fel, sys.prefix este întotdeauna definit. Calea concretă sau versiunea lui pip diferă de la un mediu la altul, deci este suficient să se rețină doar această relație.

Î21 | Valoarea returnată de write

Se preia valoarea returnată de metoda de scriere, în codul următor. Ce se afișează?

with open('/tmp/sample.txt', 'w') as f:    n = f.write("one\ntwo\nthree\n")print(n)
  1. Se afișează None
  2. Se afișează 3
  3. Se afișează 14
  4. Se afișează 11
Răspuns corectC. Se afișează 14

write returnează numărul de caractere scrise. "one\ntwo\nthree\n" are one cu 3 caractere, two cu 3 caractere, three cu 5 caractere, adică 11 în total, plus cele 3 caractere de linie nouă, deci 14 în total. Dacă se uită de numărat liniile noi, rezultă 11. Dacă s-ar crede că se returnează numărul de linii scrise, ar rezulta 3, dar ceea ce numără write este strict numărul de caractere. A alege None, crezând că nu se returnează nimic, este de asemenea o greșeală frecventă, ușor de confundat cu metode precum append sau sort, care returnează None, dar write returnează un număr. De notat că write nu adaugă automat un caracter de linie nouă, deci, dacă se dorește separarea liniilor, acesta trebuie scris explicit în șirul de caractere.

Î22 | readlines

Un fișier cu 3 linii scrise este citit cu readlines. Ce se afișează?

with open('/tmp/sample.txt', 'w') as f:    f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f:    print(f.readlines())
  1. Se afișează ['one\ntwo\nthree\n']
  2. Se afișează 'one\ntwo\nthree\n'
  3. Se afișează ['one\n', 'two\n', 'three\n']
  4. Se afișează ['one', 'two', 'three']
Răspuns corectC. Se afișează ['one\n', 'two\n', 'three\n']

readlines returnează toate liniile ca listă, dar caracterul de linie nouă de la sfârșitul fiecărui element rămâne. Prin urmare, rezultă ['one\n', 'two\n', 'three\n']. Dacă s-ar crede că liniile noi sunt eliminate, s-ar obține ['one', 'two', 'three'], dar pentru asta ar trebui aplicat rstrip pe fiecare element. Ceea ce returnează un singur șir de caractere, fără să separe pe linii, este read; readlines returnează o listă. Dacă s-ar crede că nu se tratează linia nouă drept separator, ci se împachetează întregul fișier ca un singur element, s-ar obține ['one\ntwo\nthree\n'], dar readlines separă elementele la fiecare linie nouă. Și atunci când se scrie for line in f, pentru a extrage linie cu linie, linia primită are, la fel, linia nouă atașată.

Î23 | Al doilea read

Se citește de două ori consecutiv din același obiect fișier. Ce se afișează?

with open('/tmp/sample.txt') as f:    a = f.read()    b = f.read()print(len(a), b == '')
  1. Se afișează 14 False
  2. Se afișează 14 True
  3. Se afișează 0 False
  4. Se afișează 0 True
Răspuns corectB. Se afișează 14 True

Un obiect fișier își memorează poziția curentă. Primul read citește complet de la început până la sfârșit, deci a primește cele 14 caractere, inclusiv linia nouă, iar poziția se mută la sfârșit. Al doilea read încearcă să citească pornind de la sfârșit, deci nu mai rămâne nimic, iar rezultatul este un șir vid. Prin urmare, len(a) este 14, iar b == '' este True. Dacă s-ar crede că și a doua citire returnează același conținut, b == '' ar deveni False, dar pentru a reciti același conținut este nevoie de f.seek(0), care readuce poziția la început. len(a) devine 0 doar dacă fișierul era gol sau poziția era deja la sfârșit. Este o situație ușor de interpretat greșit ca eșec al citirii, dar cauza este că poziția se afla la sfârșit.

Î24 | with și close

S-a verificat closed atât în interiorul, cât și în afara blocului with. Care sunt cele 2 linii afișate?

with open('/tmp/sample.txt') as f:    print(f.closed)print(f.closed)
  1. False, apoi False
  2. False, apoi True
  3. True, apoi False
  4. True, apoi True
Răspuns corectB. False, apoi True

În interiorul blocului with, fișierul este încă deschis, deci closed este False. La ieșirea din bloc, fișierul se închide automat, deci closed devine True imediat după aceea. Prin urmare, se afișează False, apoi True. Scopul folosirii lui with este să se asigure că închiderea are loc chiar dacă cineva uită să scrie acest cod, inclusiv atunci când apare o excepție în interiorul blocului și se iese pe neașteptate. Separând prin virgulă, se pot deschide mai multe fișiere într-un singur with, iar toate sunt închise la fel. Încercarea de a citi dintr-un obiect fișier deja închis produce ValueError, deci nu se mai poate reciti în afara blocului with.

Î25 | Modul de scriere

Același fișier este deschis de 3 ori pentru scriere, apoi citit. Ce conține fișierul?

p = '/tmp/sample.txt'with open(p, 'w') as f:    f.write('AAA')with open(p, 'a') as f:    f.write('BBB')with open(p, 'w') as f:    f.write('C')with open(p) as f:    print(f.read())
  1. Conținutul fișierului este AAABBB
  2. Conținutul fișierului este AAABBBC
  3. Conținutul fișierului este C
  4. Conținutul fișierului este CAABBB
Răspuns corectC. Conținutul fișierului este C

Deschis cu 'w', fișierul este trunchiat la lungime 0 în acel moment, indiferent de conținutul existent anterior. Urmărind pas cu pas: prima deschidere cu 'w' scrie AAA, apoi deschiderea cu 'a', fiind adăugare, produce AAABBB. Însă, în momentul în care se deschide a treia oară cu 'w', AAABBB dispare, iar C este scris acolo, deci la final rămâne doar C. Dacă s-ar crede că toate cele 3 deschideri s-au făcut cu 'a', ar rezulta AAABBBC; dacă s-ar crede că ultima scriere a fost ignorată, ar rezulta AAABBB. Suprascrierea de la început, fără trunchiere, este comportamentul lui 'r+', caz în care s-ar înlocui doar primul caracter, rezultând CAABBB. Trunchierea are loc chiar în momentul deschiderii, nu al scrierii, deci, dacă se deschide cu 'w' și nu se scrie nimic, fișierul rămâne gol. Pentru a adăuga la conținut se folosește 'a', iar pentru a-l reface complet, 'w' — intenția se exprimă prin mod.

Î26 | Operații după închidere

Ce se întâmplă atunci când se încearcă citirea dintr-un obiect fișier deja închis?

f = open('/tmp/sample.txt')f.close()print(f.read())
  1. Se produce ValueError, semnalând o operație pe un fișier închis
  2. Se afișează un șir vid, iar execuția se încheie normal
  3. Fișierul este redeschis automat, iar întregul conținut este afișat
  4. Se produce OSError, semnalând imposibilitatea de a redeschide fișierul
Răspuns corectA. Se produce ValueError, semnalând o operație pe un fișier închis

Apelarea lui read pe un obiect fișier deja închis produce ValueError, cu mesajul I/O operation on closed file. Faptul că fișierul a fost închis este memorat de obiect, deci nu are loc o redeschidere automată doar pentru că e nevoie de conținut. Nu se returnează nici un șir vid în sensul „nu mai e nimic de citit” — acesta este comportamentul lui read după ce s-a citit complet până la sfârșit. FileNotFoundError, care apare când fișierul nu este găsit, face parte din familia OSError, dar excepția produsă la operarea pe un fișier închis este alta, ValueError. Folosind instrucțiunea with, se evită structural chiar și posibilitatea de a încerca să se citească după ieșirea din bloc.

Î27 | Doar pentru citire

Ce se întâmplă atunci când se încearcă scrierea într-un fișier deschis fără să se specifice modul?

with open('/tmp/sample.txt', 'w') as f:    f.write('data')with open('/tmp/sample.txt') as f:    f.write('more')
  1. Se produce io.UnsupportedOperation, semnalând imposibilitatea de a scrie
  2. Se produce ValueError, semnalând că specificarea modului este incorectă
  3. Textul more este adăugat la sfârșitul conținutului, iar execuția se încheie normal
  4. Conținutul este înlocuit cu more, iar execuția se încheie normal
Răspuns corectA. Se produce io.UnsupportedOperation, semnalând imposibilitatea de a scrie

Dacă al doilea argument al lui open este omis, se folosește implicit modul de citire 'r'. Apelarea lui write pe un obiect fișier deschis doar pentru citire produce io.UnsupportedOperation, cu mesajul not writable. Fiind o construcție sintactic corectă, eroarea de mod devine vizibilă abia la rulare. Pentru a suprascrie se folosește 'w', pentru a adăuga la sfârșit 'a', iar pentru citire și scriere combinate, redeschiderea cu 'r+'. Invers, apelarea lui read pe un fișier deschis cu 'w' sau 'a' produce, la fel, io.UnsupportedOperation, dar cu mesajul not readable. Se poate spune că modul este locul unde se declară ce este permis pentru acel obiect fișier.

Î28 | seek și tell

Se citesc doar primele 5 caractere de la început, iar apoi se verifică poziția, în codul următor. Ce se afișează?

with open('/tmp/sample.txt') as f:    head = f.read(5)    pos = f.tell()    f.seek(0)    print(repr(head), pos, f.tell())
  1. 'one\ntwo\nthree\n' 14 0
  2. 'one\nt' 5 0
  3. 'one\nt' 5 5
  4. 'one\nt' 0 0
Răspuns corectB. 'one\nt' 5 0

Dacă i se pasează un număr lui read, se citește doar atâtea caractere. Primele 5 caractere sunt cele 3 din one, linia nouă, și t din two, deci rezultă 'one\nt'. Poziția avansează cu atâtea caractere cât s-a citit, deci tell-ul imediat următor returnează 5. Cu seek(0), care urmează, poziția revine la început, deci ultimul tell returnează 0. Dacă s-ar crede că read nu modifică poziția, ambele apeluri tell ar returna 0; dacă s-ar crede că seek nu are efect, ambele ar returna 5. Dacă argumentul lui read este confundat cu numărul de linii în loc de numărul de caractere, s-ar putea alege greșit forma cu întregul fișier 'one\ntwo\nthree\n' și 14, gândindu-se că se citesc primele 5 linii. Folosind seek(0) se poate reciti de la început, din același obiect fișier.

Î29 | JSON și tuple

Un dicționar ce conține un tuplu este transformat în JSON și apoi citit înapoi, în codul următor. Care sunt cele 2 linii afișate?

import jsons = json.dumps({'t': (1, 2)})back = json.loads(s)print(s)print(back, type(back['t']).__name__)
  1. {"t": "(1, 2)"}, apoi {'t': '(1, 2)'} str
  2. {"t": [1, 2]}, apoi {'t': (1, 2)} tuple
  3. {"t": (1, 2)}, apoi {'t': (1, 2)} tuple
  4. {"t": [1, 2]}, apoi {'t': [1, 2]} list
Răspuns corectD. {"t": [1, 2]}, apoi {'t': [1, 2]} list

În JSON există doar tablouri (array), nu și tipul tuplu. De aceea, la momentul dumps, un tuplu este convertit la tablou, iar rezultatul este {"t": [1, 2]}. La citirea înapoi, tabloul este reconstituit ca listă, deci tipul lui back['t'] este list, nu revine la tuplu. Nicăieri nu rămâne informația că originalul era un tuplu, deci un ciclu dus-întors schimbă tipul. Dacă se dorește păstrarea ca tuplu, trebuie aplicat manual tuple() după citirea înapoi. În ieșirea JSON nu apar niciodată paranteze rotunde, iar tuplul nu se transformă în șir de caractere. Din același motiv, dacă se folosește un întreg drept cheie în dicționar, dumps îl convertește la șir de caractere, iar la citirea înapoi nu revine la întreg.

Î30 | Cheile în JSON

Ce se întâmplă atunci când se încearcă transformarea în JSON a unui dicționar cu chei întregi și a unei mulțimi?

import jsonprint(json.dumps({1: 'a'}))print(json.dumps({1, 2}))
  1. Pe prima linie apare {1: "a"}, iar pe a doua [1, 2]
  2. Pe prima linie se produce TypeError, iar a doua linie nu se execută
  3. Pe prima linie apare {"1": "a"}, iar pe a doua se produce TypeError
  4. Pe prima linie apare {"1": "a"}, iar pe a doua [1, 2]
Răspuns corectC. Pe prima linie apare {"1": "a"}, iar pe a doua se produce TypeError

Cheile unui obiect JSON trebuie să fie șiruri de caractere, deci, la dumps pe un dicționar cu cheie întreagă, cheia este convertită la șir, iar rezultatul afișat este {"1": "a"}. Aceasta este o operație care reușește normal, fără să producă excepție aici. Trebuie observat totuși că, la citirea înapoi, cheia rămâne șir de caractere, fără să revină la întreg. Mulțimea de pe a doua linie nu are un tip corespunzător în JSON, deci încercarea de a face dumps produce TypeError, cu mesajul Object of type set is not JSON serializable. Dacă s-ar crede că mulțimea se scrie ca tablou, s-ar obține [1, 2], dar pentru a obține acea conversie trebuie aplicat manual list() înainte de a pasa valoarea. Pentru valorile care conțin caractere japoneze, implicit acestea sunt scrise cu escapare, iar pasând fals la ensure_ascii, sunt scrise direct, cu caracterele originale.

Exersare: rezolvați întrebările de pe această pagină

Acesta este un instrument de exersare cu întrebări în ordine aleatorie (funcționează când JavaScript este activat). Puteți citi oricum toate întrebările și explicațiile de mai sus.

* Explicațiile sunt informații în scop de studiu. Tematica și sistemul examenelor se schimbă de la an la an, așa că verificați întotdeauna anunțurile oficiale ale organizatorului examenului.

Această pagină este o traducere a textului original în japoneză. Dacă traducerea diferă de original, prevalează versiunea în japoneză. Vedeți originalul în japoneză