PEP 572 - czyli co ciekawego może znaleźć się w Pythonie 3.8 - przypisywanie zmiennej w wyrażeniach

Posted on Mon 02 July 2018 in python

Niedawno miała miejsce premiera Pythona 3.7, który przyniósł ze sobą kilka ciekawych rzeczy, jak async i await jako słowa kluczowe, opóźnioną ewaluację adnotacji typów czy breakpoint(). Prace nad nowymi wersjami jednak nie ustają i nadchodzaca wersja - Python 3.8 prawdopodobnie również przyniesie ze sobą coś ciekawego, o czym ostatnio sporo się rozmawia. Co to takiego?

Otóż chodzi o możliwość przypisywania zmiennych nie tylko w stwierdzeniach a również i w wyrażeniach. Cóż to takiego znaczy?

Mała notka, nie jestem pewien, czy poprawnie przetłumaczyłem te zdania. Raczej nie korzystam z polskich źródeł i nie wiem, czy takich też używają polscy autorzy. Dla pewności chodzi mi o statement i expression.

EDIT: operator został formalnie zaakceptowany i na pewno znajdzie się w standardzie : )

PEP 572

W tym dokumencie, tej propozycji wprowadzenia zmiany, którego autorami są Guido van Rossum, Chris Angelico czy Tim Peters, proponowane jest, by od wersji 3.8 Python wspierał przypisywanie zmiennych nie tylko w stwierdzeniach, ale również i wyrażeniach, za pomoca operatora NAME := expr.

No dobrze, ale co to znaczy w praktyce. Spójrzmy na kod.

data = None
if our_function_getting_json(some_arg) is not None:
    data = our_function_getting_json(some_arg)
    data.do_stuff()

Raczej proste do zrozumienia, zasadne, racja? Przykład z pewnego kodu wzięty. Przykład brzydki. Powyższy kod ssie w tym kontekście. Jak można by go poprawić?

data = our_function_getting_json(some_arg)
if data is not None:
    data.do_stuff()

Możemy zrobić coś takiego, ale czy to najkrócej jak się da, najlepiej jak się da? Obecnie raczej tak, ale...

Fajnie by było, gdyby można było zadeklarować tą zmienną tam w tym ifie - zapisać po prostu wynik funkcji tam, gdzie jest ona pierwotnie używana. Ważne jest to tam, gdzie chcemy później np. wykonać jakieś operacje na wyniku wyrażenia, które wykonaliśmy, np. w warunku, ale przez to, że w wyrażeniach obecnie nie można zapisywać zmiennych, to musimy zapisać ją sobie sami, wcześniej. Czy to w pętlach, czy w list comprehensions, lambda functions czy w innych.

Obecnie inaczej się nie da. To znaczy, w tym wypadku:

if (data = our_function_getting_json(some_arg)) is not None:
    data.do_stuff()

Jedyne co otrzymamy, to błąd. Taki zapis jest nieprawidłowy.

Od Pythona 3.8 najprawdopodobniej będziemy jednak mogli zrobić coś podobnego:

if (data := our_function_gettin_json(some_arg)) is not None:
    data.do_stuff()

Krócej, czyściej, lepiej.

Popatrzmy na inne przykłady.

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    match.do_stuff()

Ponownie, zamiast musieć wcześniej definować zmienną, zapisać do niej dane wyrażenie, sprawdzić ją potem, możemy zrobić to w jednym miejscu.

while (value := read_next_item()) is not None:
    ...

Alternatywa do 2 argumentowej wersji inwokacji iter().

Teraz coś ciekawego - użycie tego w list comprehensions. Wydaje mi się, że to tutaj ten proponowany element języka będzie błyszczał.

filtered_data = [y for x in data if (y := f(x)) is not None]

Inny fajny przykład, gdzie można użyć tego operatora:

results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0] 
# lub też coś takiego jak niżej
stuff = [[y := f(x), x/y] for x in range(5)]

Gdzie to nie przejdzie?

Na razie podałem wam przykłady tego, gdzie nowego operatora := można by użyć, ale gdzie będzie to zakazane?

Zacznijmy od pierwszego obostrzenia - := nie będzie można używać po prostu jako zamiennika =. To by było bez sensu - dwa operatory wykonujące tą samą czynność w tych samych miejscach. Skąd wtedy wiedzieć, którego lepiej użyć?

Zatem, jeśli wcześniej mieliśmy kod:

something = 'lalala'
something2 = 'hey'

I zmienimy go na:

something := 'lalala' # BŁĄD
something2 = 'hey' # OK

Bo tak po prostu jakoś nam się uwidzi, to kod taki nie zadziała. Nie ma zatem takiego miejsca, gdzie operator := i = byłyby jednocześnie poprawne. Zwykłe, tradycyjne przypisanie? =. Miejsce, gdzie nie możesz użyć =, czyli domyślnie wyrażenie? :=. Prosta sprawa, nie inaczej.

Nie będzie ich też można 'łączyć ze sobą.' Co to znaczy?

y = y1 := f(x) # BŁĄD

Taki zapis nie przejdzie. Ponownie byłoby to niepotrzebne, mylące i zbędne. Nie chcemy redundancji w Pythonie. Wszystko ma być czytelne, dlatego też taką operację można zastąpić następującym zapisem:

y = (y1 := f(x)) # OK

Innym tego przykładem mogło by być:

bar(x = y := f(x)) # BŁĄD
bar(x = (y := f(x))) # OK

To również nie zadziała - bez nawiasów nie wolno używać przypisania w wyrażeniu jako wartości dla keyword-argument. Ponownie - żeby czytelność była.

Dyskusja toczy się, czy zapis poniżej powinien być dozwolony

foo(x=0, y := f(0))
bar(x := 0, y = f(x))

Czyli używanie keyword-arguments i przypisania w wyrażeniu jednocześnie w wywołaniu funkcji. Co z tego wyjdzie, to się okaże.

Przypisania w wyrażeniu nie będzie można też użyć podczas definiowania domyślnych wartości argumentów funkcji czy też w pośrednich jego wyrażeniach. Co to znaczy?

def foo(answer = p := 42):  # BŁĄD
def bar(answer = (p := 42)):  # BŁĄD
def baz(callback = (lambda arg: p := arg)):  # BŁĄD

To chyba tyle, jeśli chodzi o główne przypadki gdzie można, a gdzie nie można będzie użyć :=. Oczywiście, to, że dana konstrukcja jest dozwolona, nie znaczy, że należy jej używać w każdym miejscu, gdyż większość z tych możliwości wymienionych wyżej, jest niepotrzebnie komplikującym elementem kodu i raczej nie powinno się ich tak wykorzystywać, := powinien byc używany tam, gdzie faktycznie uprości on kod, a nie wszędzie, bo to jakaś fajna nowinka technologiczna.

Różnice między dwoma operatorami

Różnice są widoczne chociażby wtedy, kiedy chcemy przypisać wartości do wielu zmiennych jednocześnie. Jak to wygląda?

x = y = z = 0 
(x := (y := (z := 0)))

Bardzo brzydki konstrukt, racja?

Przypsanie w wyrażeniu może być tylko w postaci NAME := exp, to znaczy, że takie operacje jak:

x[0] = 0
some_obj.prop1 = 'something'

Nie będą wspierane przez NAME := exp.

Podobnie +=, -=, *= itd. również nie zadziała podczas używania :=.

x += y

Trzeba będzie to zrobić tak:

(x := x + y)

Podsumowanie

Nowego operatora przypisania w wyrażeniu, będzie można użyć do uproszczania list comprehensions, zapisywania wartości warunków czy innych wyrażeń.

Oczywiście nic nie jest jeszcze pewne, wciąż prowadzi się dyskusje na temat tego, jak to wszystko ma wyglądać i działać, zmienić się może nawet sam operator, bo rozważane, zamiast :=, są jeszcze as albo -> itd., ale taki mniej więcej będzie tego obraz.