CodeGym /Kursy /Python SELF PL /Błędy standardowe, część 2

Błędy standardowe, część 2

Python SELF PL
Poziom 20 , Lekcja 2
Dostępny

8.1 Niezrozumienie zasad zasięgu w Pythonie

Zasięg w Pythonie opiera się na tak zwanej zasadzie LEGB, która jest skrótem:

  • Local (nazwy przypisane w dowolny sposób wewnątrz funkcji (def lub lambda), i nieogłoszone jako globalne w tej funkcji);
  • Enclosing (nazwy w lokalnym zasięgu dowolnych statycznie obejmujących funkcji (def lub lambda), od wewnątrz na zewnątrz);
  • Global (nazwy przypisane na najwyższym poziomie pliku modułu lub przez wykonanie instrukcji global w def wewnątrz pliku);
  • Built-in (nazwy wcześniej przypisane w module wbudowanych nazw: open, range, SyntaxError, i inne).
  • Brzmi prosto, prawda?

    Niemniej jednak istnieją pewne niuanse w tym, jak to działa w Pythonie, co prowadzi nas do złożonego problemu programowania w Pythonie. Przyjrzyjmy się poniższemu przykładowi:

    
    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

    Gdzie jest problem?

    Powyższy błąd pojawia się, ponieważ, kiedy przypisujesz wartość do zmiennej w zasięgu, Python automatycznie uznaje ją za lokalną dla tego zasięgu i ukrywa wszelkie zmienne o tej samej nazwie w jakimkolwiek nadrzędnym zasięgu.

    Wiele osób zdziwi się, kiedy dostają UnboundLocalError w kodzie, który wcześniej działał, kiedy zostanie zmodyfikowany poprzez dodanie operatora przypisania gdziekolwiek w ciele funkcji.

    Ta cecha szczególnie myli programistów przy użyciu list. Rozważmy poniższy przykład:

    
    lst = [1, 2, 3]
    def foo1():
        lst.append(5)  # To działa normalnie...
            
    foo1()
    print(lst)
    [1, 2, 3, 5]
            
    lst = [1, 2, 3]
    def foo2():
        lst += [5]  # ... a to już się zawiesza!
            
    foo2()
    Traceback (most recent call last):
        File "
           
       
         ", line 1, in 
        
          File " 
         
           ", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment 
          
         
       

    Dlaczego foo2 się zawiesza, a foo1 działa normalnie?

    Odpowiedź jest taka sama, jak w poprzednim przykładzie, ale powszechnie uważa się, że sytuacja tutaj jest bardziej subtelna. foo1 nie stosuje operatora przypisania do lst, podczas gdy foo2 — tak. Pamiętając, że lst += [5] to w istocie tylko skrót dla lst = lst + [5], widzimy, że próbujemy przypisać wartość do lst (więc Python przypuszcza, że znajduje się w lokalnym zasięgu). Jednak wartość, którą chcemy przypisać do lst, opiera się na samym lst (znowu, teraz przypuszcza się, że znajduje się w lokalnym zasięgu), który jeszcze nie został zdefiniowany. I mamy błąd.

    8.2 Zmiana listy podczas iteracji po niej

    Problem w tym kawałku kodu powinien być dość oczywisty:

    
    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]  # ZŁE: Usuwanie elementu z listy podczas iteracji po niej
        
    Traceback (most recent call last):
            File "
           
       
         ", line 2, in 
        
          IndexError: list index out of range 
         
       

    Usuwanie elementu z listy lub tablicy podczas iteracji po niej to problem w Pythonie, który jest dobrze znany wszystkim doświadczonym programistom. Ale choć powyższy przykład może być wystarczająco oczywisty, nawet doświadczeni programiści mogą wpaść na te pułapki w dużo bardziej skomplikowanym kodzie.

    Na szczęście Python zawiera szereg eleganckich paradygmatów programowania, które użyte poprawnie mogą prowadzić do znacznego uproszczenia i optymalizacji kodu. Dodatkowym przyjemnym skutkiem tego jest to, że w bardziej uproszczonym kodzie szansa na napotkanie błędu przypadkowego usunięcia elementu listy podczas iteracji jest znacznie mniejsza.

    Jednym z takich paradygmatów są generatory list. Co więcej, zrozumienie działania generatorów list jest szczególnie przydatne do uniknięcia tego konkretnego problemu, jak pokazano w tej alternatywnej realizacji powyższego kodu, która działa idealnie:

    
    odd = lambda x: bool(x % 2)
    numbers = [n for n in range(10)]
    numbers[:] = [n for n in numbers if not odd(n)]  # po prostu wybieramy nowe elementy
    print(numbers)
    # [0, 2, 4, 6, 8]

    Ważne! Tutaj nie ma przypisania nowego obiektu listy. Użycie numbers[:] — to zbiorcze przypisanie nowych wartości do wszystkich elementów listy.

    8.3 Niezrozumienie, jak Python wiąże zmienne w zamknięciach

    Przyjrzyjmy się poniższemu przykładowi:

    
    def create_multipliers():
        return [lambda x: i * x for i in range(5)]  #Zwraca listę funkcji!
    
    for multiplier in create_multipliers():
        print(multiplier(2))

    Możesz spodziewać się następującego wyjścia:

    
    0
    2
    4
    6
    8
    

    Ale w rzeczywistości otrzymasz:

    
    8
    8
    8
    8
    8
    

    Niespodzianka!

    To się dzieje z powodu późnego wiązania w Pythonie, które oznacza, że wartości zmiennych używanych w zamknięciach są wyszukiwane podczas wywołania wewnętrznej funkcji.

    Zatem w powyższym kodzie za każdym razem, gdy któraś z zwracanych funkcji jest wywoływana, wartość i jest wyszukiwana w otaczającym zasięgu podczas jej wywołania (w tym momencie pętla już się zakończyła, więc i zostało przypisane do ostatecznego wyniku — wartości 4).

    Rozwiązanie tego powszechnego problemu w Pythonie wygląda następująco:

    
    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à! Używamy tutaj domyślnych argumentów do generowania funkcji anonimowych, aby osiągnąć pożądane zachowanie. Niektórzy nazwaliby to rozwiązanie eleganckim. Niektórzy — subtelnym. Niektórzy nienawidzą takich sztuczek. Ale jeśli jesteś programistą Pythona, to ważne, aby to zrozumieć w każdym przypadku.

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