Diversity w polskim IT
Fabio Veronese
Fabio VeroneseSoftware Engineer

Jaką przewagę ma Python nad Javą?

Poznaj przypadki, w których Python może poradzić sobie o wiele lepiej, niż Java.
19.02.20226 min
Jaką przewagę ma Python nad Javą?

Wypad na plażę latem. Jaka jest różnica między wyjazdem nad morze z rodzicami jako małe dziecko a byciem tam z przyjaciółmi, jak jest się na studiach? Na studiach po raz pierwszy czujesz wolność od zasad, które narzucają Ci rodzice (mniej więcej). 

Przenieśmy się teraz do czasów, gdy kodujesz już na poważnie. W Javie mamy zasady, które ograniczają wolność w programowaniu - narzucając Ci pewne wybory i swoją strukturę. Filozofia Pythona jest zupełnie inna - sam bierzesz na siebie odpowiedzialność zepsucie designu. 

Poniżej przyjrzymy się najbardziej irytującym ograniczeniom, które można spotkać w Javie, będąc developerem Pythona. 

Private, Public, and Protected 

Java posiada szereg kwalifikatorów, które pomagają sprawdzić, czy mamy dostęp do zmiennej, metody, lub nawet klasy. Ograniczanie dostępu do metod można porównać do zakładania użytkownikom kajdanek. 

Myślę, że ograniczenie mi dostępu do jakiegoś pola, czy metody nie sprawi, że nie popełnię błędu przy użyciu jakiejś części oprogramowania. Świadomość tego, że nie powinienem czegoś robić, jest dla mnie wystarczająca. Gdy już to wiem, to i tak mogę podjąć decyzję, żeby jednak coś z tym zrobić. Jeśli przestanie to działać, to będzie to tylko i wyłącznie moja wina. Jeśli chcę, aby dana klasa nie była dziedziczona, to tworzę tym samym wyjątek dziedziczenia - mogę sobie jakoś bez tego poradzić. 

public class Planet {
  public Amosphere atmosphere;
  private Core core;  // no direct access to this
public Planet(){
    this.atmosphere = new Atmosphere();
    this.core = new Core();
  }
public Core getCore() {
    this.atmosphere.addGas(self.core.releaseGas())
    return this.core
  }
public Atmosphere getAtmosphere() {
    return this.atmosphere
  }
}
...
Planet myWorld = new Planet()
System.out.printf(myWorld.getCore())
System.out.printf(myWorld.atmosphere)
System.out.printf(myWorld.getAtmosphere())


Python ma pewne konwencje kodowania, które pozwalają nam oznaczać pola, metody lub klasy, które mają być prywatne. Mamy wtedy świadomość tego, że dostęp do nich jest nieoczekiwany. Może lepiej jest znaleźć inne rozwiązanie, ale jeśli wiesz, co robisz, to po prostu to rób. 

class Planet:
  def __init__(self):
     self.atmosphere = Atmpsphere()
     self._core = Core()
  def get_core(self):
     self.atmosphere += self._core.release_gas()
     return self._core
my_planet = Planet()
print(my_planet._core) # just printing the value, no volcano!
print(my_planet.atmosphere)
print(my_planet.get_core())
print(my_planet.atmosphere)


Powyższy przykład pokazuje, że nie będziesz mieć dostępu do myPlanet.core, ponieważ element ten nie jest wyeksponowany. my_planet._core jest jednak dostępny, co pozwala na śledzenie błędów, dalszy development lub zniszczenie planety. 

Boilerplate

Boilerplate to liczba linijek kodu, które dany developer musi napisać dla powtarzalnych zadań - w takim kodzie nie ma praktycznie żadnych zmian. Kod napisany w Javie jest bardzo rozwlekły - zazwyczaj potrzeba wielu linijek, aby cokolwiek zrobić, a to może niestety wykończyć. 

Są oczywiście sposoby, aby tego uniknąć (kod generowany automatycznie, adnotacje itd.). Czasami sobie jednak myślę: “Hej, właśnie wstawiłem tutaj zmienną. Dlaczego muszę napisać settera i gettera, aby mieć do niej dostęp?” Dlaczego muszę w całości zaprojektować coś przypominającego klasę (co nie jest w ogóle klasą, tylko interfejsem) tylko po to, aby zadeklarować strukturę konkretnej implementacji, którą chciałbym gdzieś wstawić? Za bardzo skomplikowane, prawda? 

Interfejsy mają oczywiście swoje uzasadnienie w Javie. Pisanie tych samych sygnatur 3 razy i przypisywanie im dwóch implementacji jest niezwykle czasochłonne. 

public interface Habitable {
  public Settlement settle(List<People> settlerList);
}
public class Planet {
  public Amosphere atmosphere;
  private Core core;  // no direct access to this
  public Planet(){
    this.atmosphere = new Atmosphere();
    this.core = new Core();
  }
  public Core getCore() {
    this.atmosphere.addGas(self.core.releaseGas())
    return this.core
  }
  public Atmosphere getAtmosphere() {
    return this.atmosphere
  }
}
public class HabitablePlanet extends Planet implements Habitable {
  private List<People> population;
  public HabitablePlanet(){
    super();
    this.population = new ArrayList();
  }
  public Settlement settle(List<People> settlerList){
    this.population.addAll(settlerList);
    return new Settlement(settlerList);
}
public class Continent implements Habitable {
  private List<People> population;
  private Planet planet;
  public Continent(HabitablePlanet planet) {
    this.planet = planet;
    this.population = new ArrayList();
  public Settlement settle(List<People> settlerList) {
    this.population.addAll(settlerList);
    return this.planet.settle(settlerList);
}


Python jest bardziej schludny - często potrzeba mniejszej ilości linijek kodu, aby wykonać to samo zadanie. Nie będziemy tutaj porównywać składni, aby pokazać, o ile bardziej Python jest bardziej kompaktowy od Javy. Poniżej szybki przykład - bez żadnych interfejsów!

class Planet:
  def __init__(self):
     self.atmosphere = Atmosphere()
     self._core = Core()
  def get_core(self):
     self.atmosphere += self._core.release_gas()
     return self._core
class HabitablePlanet(Planet):
  def __init__(self):
    self.population = []
  
  def settle(self, settler_list):
    self.population += settler_list
    return Settlement(settler_list)
class Continent:
  def __init__(self, planet):
    self.population = []
    self._planet = planet
  def settle(self, settler_list):
    self.population += settler_list
    return self.planet.settle(settler_list)

Wielokrotne dziedziczenie i typowanie

Java ma wielokrotne dziedziczenie oparte na interfejsach. Zmusza to programistów do stworzenia kolejnej warstwy abstrakcji, która składa się z definicji, którą trzeba zaimplementować. Jest to niestety przeciwieństwo prostoty i pragmatyzmu. Zasady typowania w Javie są dość rygorystyczne, a sygnatury metod, typy ogólne, interfejsy, klasy abstrakcyjne i rzutowanie to problem, zarówno jeśli chodzi i design, jak i o debugowanie. 

Czy AbandonedAsteroid nadaje się do zamieszkania? Poniżej widzimy przykładowy problem z typowaniem w Javie.

public class ColonizingCapsule{
    ...
  public Settlement colonize(Habitable habitat){
    return habitat.settle(this.settlerList}
}
public class AbandonedAsteroid {
  private boolean isRestored = false;
  private List<People> population;
  ...
  public Settlement settle(List<People> settlerList) {
    if (!this.isRestored) throw new UnfitForLifeException();
    this.population.addAll(settlerList);
    return this.planet.settle(settlerList);
  }
}


Uwaga! Typ zmiennej jest dobrze zdefiniowany, ale użycie konkretnej instancji nie jest powiązane z żadną klasą, czy typem. Oznacza to również, że możemy natrafić na niezamierzone użycie Twojego kodu przez wykonanie testu kaczki. 

class ColonizingCapsule:
  ...
  def colonize(habitat):
     return habitat.settle(this.settler_list)
class AbandonedAsteroid:
  def __init__(self):
    self.is_restored = False
    self._population = []
  ...
  def settle(self, settler_list):
    if not self.is_restored:
      raise UnfitForLifeError
    self._population += settler_list
    return Settlement(settler_list)

Czym jest obiekt? Czym jest this?

Java ma typy i klasy, które opisują elementy tego języka. Czym jest jednak klasa? Czym jest typ? Funkcja? Metoda? Czym jest this? Abstrakcja i magia, która odbywa się za kulisami, sprawiają, że konstruktor generuje instancje.  

public class Spaceship {
  public int crewCapacity;
  public String name;
  private List<People> crew;
  public Spaceship(int crewCapacity, String name) {
    this.crewCapacity = crewCapacity;
    this.name = name;
  }
  public void onboardAll(List<People> crew) {
    if (crew.size() > this.crewCapacity - this.crew.size())
      throw new CapacityReachedException():
    this.crew.addAll(crew);
  }
}


Python ma dwie bardzo proste odpowiedzi na te pytania: wszystko jest obiektem, a wszystkie metody klasy zachowują się, jak statyczne metody Javy. 

Pierwsza asercja sugeruje, że wszystkie elementy oprócz typów bazowych są instancjami klasy. Co więcej, definicje typów są klasami, a ich instancje to obiekty. Funkcje, metody i lambdy to też obiekty.

Tą drugą rzecz wyraźnie widać we wszystkich definicjach klas: wszystkie sygnatury metod mają self jako pierwszy argument, który traktowany jest jak referencja, oznacza to modyfikację oryginalnej instancji. Niesamowite jest tylko to, że nie piszesz jej w czasie wywoływania metody (ale do tego jesteśmy przyzwyczajeni). Rzeczywiste metody statyczne są opatrzone adnotacją @staticmethod i nie mają argumentu self.

class Spaceship:
  def __init__(self, crew_capacity, name):
    self.crew_capacity = crew_capacity
    self.name = name
  def onboard_all(self, crew):
    if len(crew) > self.crewCapacity - len(self.crew))
      raise CapacityReachedException
    self.crew += crew

Duża władza oznacza wielką odpowiedzialność 

Wolność i władza nie przychodzi bez kosztów - trzeba być bardziej odpowiedzialnym. Dotyczy to zarówno życia, jak i programowania. Wolna wola oznacza, że możemy popełniać błędy. A tylko my jesteśmy za te błędy odpowiedzialni.

Najlepszą odpowiedzią jest tutaj pewnie „Znajdź coś, co Ci odpowiada”. Ja czuję się bardziej komfortowo z filozofią Pythona i nie muszę się zmuszać do projektowania rzeczy. Z drugiej strony, ktoś inny może pokochać Javę za jej rygor i szczegółowość. Kto wie?


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>