CodeGym /Corso Java /Python SELF IT /Errori Standard, Parte 2

Errori Standard, Parte 2

Python SELF IT
Livello 20 , Lezione 2
Disponibile

8.1 Fraintendimento delle regole di scope in Python

Lo scope in Python si basa su quello che viene chiamato la regola LEGB, che è un acronimo:

  • Local (nomi assegnati in qualsiasi modo all'interno di una funzione (def o lambda), e non dichiarati globali in questa funzione);
  • Enclosing (nomi nell'ambito locale di qualsiasi funzione includente staticamente (def o lambda), dall'interno all'esterno);
  • Global (nomi assegnati al livello superiore del file di un modulo, o tramite l'esecuzione della direttiva global all'interno di def nel file);
  • Built-in (nomi assegnati in precedenza nel modulo dei nomi incorporati: open, range, SyntaxError, e altri).
  • Sembra abbastanza semplice, giusto?

    Tuttavia, ci sono alcune sfumature nel funzionamento in Python, che ci porta a un problema complesso nella programmazione in Python. Consideriamo il seguente esempio:

    
    x = 10
    def foo():
        x += 1
        print(x)
                
    foo()
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 2, in foo
    UnboundLocalError: local variable 'x' referenced before assignment

    Qual è il problema?

    L'errore sopra citato si verifica perché, quando assegni un valore a una variabile nell'ambito, Python considera automaticamente come locale quella variabile per quell'ambito e nasconde qualsiasi variabile con lo stesso nome in qualsiasi ambito superiore.

    Di conseguenza, molti rimangono sorpresi quando ricevono un UnboundLocalError in un codice che prima funzionava, quando viene modificato con l'aggiunta di un'istruzione di assegnazione da qualche parte nel corpo della funzione.

    Questa caratteristica è particolarmente sconcertante per gli sviluppatori quando si utilizzano liste. Consideriamo il seguente esempio:

    
    lst = [1, 2, 3]
    def foo1():
        lst.append(5)  # Questo funziona normalmente...
            
    foo1()
    print(lst)
    [1, 2, 3, 5]
            
    lst = [1, 2, 3]
    def foo2():
        lst += [5]  # ... ma questo va in crash!
            
    foo2()
    Traceback (most recent call last):
        File "", line 1, in 
        File "", line 2, in foo
    UnboundLocalError: local variable 'lst' referenced before assignment

    Perché foo2 va in crash mentre foo1 funziona senza problemi?

    La risposta è la stessa dell'esempio precedente, ma comunemente si ritiene che questo caso sia più sottile. foo1 non applica un'istruzione di assegnazione a lst, mentre foo2 lo fa. Ricordando che lst += [5] è semplicemente un'abbreviazione per lst = lst + [5], vediamo che stiamo cercando di assegnare un valore a lst (quindi Python presume che sia nell'ambito locale). Tuttavia, il valore che vogliamo assegnare a lst si basa su lst stesso (ancora una volta, ora si presume che sia nell'ambito locale), che non è stato ancora definito. E otteniamo un errore.

    8.2 Modifica di una lista durante l'iterazione su di essa

    Il problema nel seguente pezzo di codice dovrebbe essere abbastanza evidente:

    
    odd = lambda x: bool(x % 2)
    numbers = [n for n in range(10)]
        
    for i in range(len(numbers)):
        if odd(numbers[i]):
            del numbers[i]  # MALE: Cancellazione di un elemento da una lista durante l'iterazione
        
    Traceback (most recent call last):
            File "", line 2, in 
    IndexError: list index out of range

    Eliminare un elemento da una lista o array durante l'iterazione su di esso è un problema di Python ben noto a qualsiasi sviluppatore software esperto. Ma, mentre l'esempio sopra potrebbe essere abbastanza evidente, anche sviluppatori esperti possono incappare in questo errore in un codice molto più complesso.

    Per fortuna, Python include una serie di paradigmi di programmazione eleganti che, se correttamente utilizzati, possono portare a una significativa semplificazione e ottimizzazione del codice. Un piacevole effetto collaterale è che in un codice più semplice, la probabilità di incappare nell'errore di eliminazione accidentale di un elemento dalla lista durante l'iterazione è notevolmente ridotta.

    Uno di questi paradigmi sono le list comprehension. Inoltre, comprendere come funzionano le list comprehension è particolarmente utile per evitare questo problema specifico, come mostrato in questa alternativa del codice sopra, che funziona perfettamente:

    
    odd = lambda x: bool(x % 2)
    numbers = [n for n in range(10)]
    numbers[:] = [n for n in numbers if not odd(n)]  # semplicemente filtrando nuovi elementi
    print(numbers)
    # [0, 2, 4, 6, 8]

    Importante! Qui non si sta assegnando un nuovo oggetto lista. L'uso di numbers[:] è un'assegnazione di gruppo di nuovi valori a tutti gli elementi della lista.

    8.3 Fraintendimento di come Python associa le variabili nei closure

    Consideriamo il seguente esempio:

    
    def create_multipliers():
        return [lambda x: i * x for i in range(5)]  #Restituisce un elenco di funzioni!
    
    for multiplier in create_multipliers():
        print(multiplier(2))

    Potresti aspettarti il seguente output:

    
    0
    2
    4
    6
    8
    

    Ma in realtà otterrai questo:

    
    8
    8
    8
    8
    8
    

    Sorpresa!

    Questo avviene a causa del binding ritardato in Python, il che significa che i valori delle variabili utilizzate nei closure vengono cercati al momento della chiamata della funzione interna.

    Quindi, nel codice sopra, ogni volta che viene chiamata una delle funzioni restituite, il valore di i viene cercato nell'ambito circostante al momento della chiamata (e a quel punto il ciclo è già terminato, quindi a i è stato già assegnato il valore finale — 4).

    La soluzione a questo comune problema in Python sarebbe:

    
    def create_multipliers():
        return [lambda x, i = i : i * x for i in range(5)]
    for multiplier in create_multipliers():
        print(multiplier(2))
    
    # 0
    # 2
    # 4
    # 6
    # 8

    Voilà! Utilizziamo qui gli argomenti di default per generare funzioni anonime per ottenere il comportamento desiderato. Alcuni lo definirebbero una soluzione elegante. Altri la considerano sottile. Alcuni odiano questo tipo di cose. Ma se sei uno sviluppatore Python, è importante capirlo in ogni caso.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION