Co mnie wkurza w Pythonie

W poprzednim wpisie pisałem, jak bardzo mi się podoba metoda __call__ w Pythonie. I rzeczywiście, uważam, że to fajna rzecz i szkoda, że nie mamy czegoś podobnego w Javie.

Python ma jednak kilka właściwości, które powodują, że podczas pracy nad MoodOfTheSong często tęsknię za Javą. Oto one:

1. Map, filter, reduce

Bardzo lubię programować w funkcyjnym stylu, często więc używam metod map, filter, reduce i im podobnych. I właśnie dlatego tak wkurza mnie ich wygląd w języku Python. map, dla przykładu wygląda tak:

map(transformacja, dane)

Spróbujmy napisać kod, który weźmie ciąg liczb od 1 do 10, podwoi każdą z nich, usunie wszystkie niepodzielne przez 3 oraz wypisze każdą w formie: "Number 3"
Widzę dwa sposoby na wykonanie tego i obydwa są brzydkie:

print(
    list(
        map(
            lambda n: "Number {}".format(n),
            filter(
                lambda n: n % 3 == 0,
                map(
                    lambda n: n * 2,
                    range(10))))))
for number in filter(
        lambda n: n % 3 == 0,
        map(lambda n: n * 2, range(10))):
    print("Number {}".format(number))

Można zrobić to też bez używania operatorów map i filter:

 for text in ["Number {}".format(n) for n in [2 * i for i in range(10)] if n % 3 == 0]:
 print(text)
 

Taki zapis jest bardziej idiomatyczny dla Pythona, ale pozbawia nas możliwości łatwego korzystania z przetwarzania równoległego. No i nadal nie jest zbyt czytelny (choć z pewnością można by go nieco poprawić).

 

Teraz dla odmiany, jak wyglądałoby to w Javie:

        IntStream.range(0, 10)
                .map(n -> n * 2)
                .filter(n -> n % 3 == 0)
                .mapToObj(n -> String.format("Number %s", n))
                .forEach(System.out::println);

Oraz w Kotlinie:

  (1..10)
      .map { it * 2 }
      .filter { it % 3 == 0 }
      .map { "Number $it" }
      .forEach(::println)

Taki sposób zapisu – jedna operacja po drugiej – jest bardzo czytelny i naturalny, zwłaszcza dla osób dużo pracujących z Bashem i mu podobnymi powłokami.

Widzę za to pewną zaletę zapisu pythonowego na poziomie konceptualnym: w Javie czy Kotlinie każdy nowy operator to kolejna metoda klasy Stream, w Pythonie to zupełnie osobny byt. Mamy więc zachowaną SRP.

2. Wincyj mutowalności!

Jeszcze nie odpuszczę operatorowi map. Zgadnijcie jaki będzie wynik wykonania poniższego kodu:

numbers = map(lambda n: n * 10, range(3))
for n in numbers:
    print(n)
for n in numbers:
    print(n)

Można by się spodziewać, że dwa razy na ekran wypisane zostaną liczby 0, 10 i 20, prawda? Tymczasem są wypisywane tylko raz. Dzieje się tak dlatego, że operator map zwraca iterator, który można raz wykorzystać. Niektórzy takie zachowanie wzięli nawet za bug 😉

Dość łatwo można zaradzić temu problemowi na co najmniej dwa sposoby:

# 1
numbers = [n * 10 for n in range(3)]
for n in numbers:
    print(n)
for n in numbers:
    print(n)
# 2
numbers = list(map(lambda n: n * 10, range(3)))
for n in numbers:
    print(n)
for n in numbers:
    print(n)

W tym przypadku pierwszy sposób jest oczywiście najczytelniejszy, za to – ponownie – problematyczne byłoby korzystanie z przetwarzania równoległego. Dodatkowo tracimy jedną z dużych zalet operatora map: leniwość. Co to znaczy? Chodzi o to, że zdefiniowana operacja na zbiorze jest wykonywana dopiero podczas odczytu.

Czemu więc nie można by połączyć tych dwóch rzeczy – stałości (przy każdym odczycie dostajemy te same dane) oraz leniwości?

O ile doskonale rozumiem, że za decyzją o tym, że map zwraca jednorazowy iterator stały argumenty mocne dotyczące (zapewne) wydajności, o tyle osobiście mi się nie podoba to, że moja zmienna może zmienić swój stan w wyniku, de facto jej… odczytu.

To, że obiekt zwrócony przez map jest mutowalny to jednak mały problem przy tym, że w Pythonie nie ma stałych. Są, oczywiście, sposoby na ich użycie, ale nie da się zrobić tego tak łatwo jak w innych językach, gdzie można napisać po prostu final int czy let.

3. Duck typing

Ostatnia rzecz. Oczywiście nie będę wchodził tu w dyskusję czy statyczne typowanie jest lepsze czy gorsze, ponieważ na każdym forum znajdziecie baaardzo długie wątki na ten temat 😉

Temat postu to co mnie wkurza, więc wymienię dwie rzeczy, które mi osobiście utrudniają pracę:

To, że nie ma możliwości zdefiniowania typu zmiennej czy parametru, powoduje, że IDE nie wie z czym mam do czynienia i trudno mu podpowiadać np. jakie metody ma używany przez nas obiekt. Czasem się domyśla z kontekstu, ale czasem nie. Szkoda, bo nie lubię sam pisać kodu; lubię jak kod za mnie pisze IDE, a ja tylko je naprowadzam na poprawne rozwiązania.

I jeszcze jedna rzecz. Najczęstszy błąd jaki ja popełniam związany z kaczkowym typowaniem to zwracanie metody zamiast wyniku jej działania. Na przykład chcąc napisać:

def foo(bar):
    return bar.capitalize()

print(foo("hello"))
# Hello

…piszę:

def foo(bar):
    return bar.capitalize

print(foo("hello"))
# <built-in method capitalize of str object at 0x7f58cde2a810>

Jako, że nie dostaję żadnego błędu, ani wyjątku, często prowadzi to do trudno wykrywalnych bugów.

Podsumowując

Oczywiście poza tym, Python ma dużo zalet. To co wymieniłem to kilka wad, które nieco obniżają moją produktywność.