Wyślij zapytanie Dołącz do Sii

Zarówno Python, jak i Java, to języki programowania cieszące się w ostatnich latach niesłabnącą popularnością. Nie dziwi zatem fakt, iż duże grono osób utożsamia programowanie ze znajomością właśnie jednego z nich. Dotyczy to szczególnie osób znajdujących się na początku swojej drogi w branży IT i którym przyszło zmierzyć się z pytaniem: „Od czego zacząć i jaki język programowania będzie najlepszy na start? Nie ma tutaj prostej odpowiedzi i jedyną słuszną wydaje się być – a jakże – „to zależy” 😉

Oba języki mają swoje zalety i wady oraz dziedziny, w których sprawdzają się lepiej lub gorzej, dlatego w tym wpisie chciałbym przybliżyć kilka cech, które odróżniają Pythona od Javy (i na odwrót) i które szczególnie zwróciły moją uwagę podczas nauki. Nie będę tutaj jednak skupiał się na różnicach, które są widoczne gołym okiem (jak np.: składnia czy szerokie omówienie dostępnych typów danych), a przede wszystkim na koncepcjach charakteryzujących oba języki w kontekście programowania zorientowanego obiektowo.

Przetwarzanie kodu źródłowego

Wykonanie napisanego przez nas programu w obu językach odbywa się na innych zasadach.

W Pythonie mamy do czynienia z tzw. interpreterem, przez co język ten jest określany mianem języka interpretowanego. Uruchomienie programu wiąże się z tym, że kod źródłowy jest czytany linijka po linijce w czasie rzeczywistym. Może więc dojść do sytuacji, iż fragment programu wykona się poprawnie, a dopiero, gdy interpreter napotka błąd, dalsza egzekucja zostanie przerwana.

W Javie taka sytuacja jest niemożliwa i aby uzyskać działający program, kod źródłowy musi zostać najpierw w całości poddany procesowi kompilacji, czyli zamianie na tzw. kod bajtowy (ang. bytecode). Dopiero jeśli kompilacja się powiedzie (tj. nie zostaną zidentyfikowane żadne błędy, np. dotyczące niepoprawnie zadeklarowanych typów), program jest gotowy do uruchomienia przez maszynę wirtualną Javy (ang. Java Virtual Machine, JVM).

Zalety i wady języków

Atutem programów napisanych w językach interpretowanych jest to, że są łatwiej przenoszalne, gdyż w przeciwieństwie do programów wymagających kompilacji, nie zależą od specyficznej platformy, na której chcemy je uruchamiać. Umożliwiają one również szybsze prototypowanie oraz niemal natychmiastowe uruchamianie i testowanie aplikacji, co nie pozostaje bez znaczenia w przypadku konieczności wprowadzenia zmian w kodzie.

Niestety, brak kompilacji oraz specyfika Pythona jako języka łatwego do zrozumienia i modyfikacji znacznie ułatwia, zazwyczaj niepożądany, reverse engineering. Fakt posiadania niemal pełnego dostępu do kodu źródłowego programu wymusza więc konieczność stosowania dodatkowych zabezpieczeń, np. w formie tzw. zaciemniania kodu (ang. obfuscating).

Kod bajtowy Javy ma w tym aspekcie wyraźną przewagę. Trudność wynikająca z potrzeby jego dekompilacji oraz późniejszej czasochłonnej analizy znacznie wpływa na bezpieczeństwo końcowego produktu.

Programy wykonywane przez interpreter, pomimo szeregu swoich zalet, są z reguły także wolniejsze od tych działających bezpośrednio na poziomie kodu maszynowego. Języki kompilowane lepiej sprawdzają się ponadto, gdy musimy mierzyć się z aplikacjami o wysokim poziomie skomplikowania. Jest to związane z faktem, iż oferują one zaawansowane mechanizmy zarządzania pamięcią, silne wsparcie dla wielowątkowości oraz tzw. kompilację Just-In-Time, która pozwala optymalizować kod „w locie”, a co za tym idzie – lepiej wykorzystywać dostępne zasoby.

Komentarz

W rzeczywistości zarówno Java jak i Python łączą w sobie cechy języków interpretowanych i kompilowanych. Zagłębiając się w sposób działania pythonowego interpretera, kod źródłowy w pierwszej kolejności jest zamieniany właśnie na kod bajtowy (tzw. kod pośredni), a następnie analizowany i wykonywany kawałek po kawałku. Do uruchomienia skompilowanego kodu Javy wykorzystywana jest z kolei maszyna wirtualna, której sposób działania jest zbliżony do interpretera.

Typowanie

Różnicą, której nie sposób nie zauważyć podczas tworzenia kodu, jest typowanie. Python jest językiem dynamicznie typowanym, zaś Java – statycznie. Co to oznacza w praktyce?

Typowanie w Pythonie

Deklarując zmienne, argumenty, czy zwracane wartości, w Pythonie nie musimy jawnie określać typów danych, jakie elementy te będą przechowywać. Ma to miejsce automatycznie, tj. po przypisaniu do elementu konkretnej wartości i – co bardzo istotne – typ ten nie jest stały, a więc może ulegać zmianie w trakcie wykonywania programu.

Takie zachowanie zdecydowanie zwiększa szybkość i elastyczność kodowania poprzez uproszczenie składni. Jednakże dla wielu programistów jest to niewątpliwie wada, ponieważ dynamicznie zmieniające się typy mogą powodować sporo problemów np. przy wykrywaniu błędów.

W związku z tym od wersji 3.5 Python daje nam możliwość deklaracji typów za pomocą mechanizmu zwanego type hinting. Zadeklarowane w ten sposób typy nadal nie są jednak stałe i mogą zostać zignorowane przez programistę. Nie jest to zatem sposób na wprowadzenie statycznego typowania, a jedynie podpowiedź służąca łatwiejszej analizie kodu pod kątem pierwotnych założeń. 

# Type hinting in Python
def calculate_bmi(person: str, weight: float, height: int) -> str:
   return f"BMI of {person}: {weight/((height * 0.01) ** 2)}"


if __name__ == '__main__':
   name: str = "John"
   w: float = 78.8
   h: int = 182
   print(calculate_bmi(name, w, h))

   # change the variable types to be different from the type hints
   name: list = ["John", "Smith"]
   w: int = 120
   h: float = 182.5
   print(calculate_bmi(name, w, h))

# Output:
# BMI of John: 23.789397415771038
# BMI of 123456: 36.02927378495027

Typowanie w Javie

Java, jako język statycznie typowany, w większości przypadków wymaga od programisty jasnej deklaracji typu już w momencie inicjalizacji elementu. Raz określony typ jest niezmienny i weryfikowany przez kompilator, który sprawdza czy operacje, które chcemy wykonać są zgodne z typem, który zadeklarowaliśmy. Dzięki temu możliwe jest wychwycenie wszelkich niespójności jeszcze przed wykonaniem programu.

Należy podkreślić, że wyróżnia się dwa rodzaje typów w Javie:

  • typy prymitywne (ang. primitive types),
  • typy referencyjne (ang. reference types).

Podstawową różnicą jest sposób alokacji pamięci. Typy prymitywne z uwagi na swoją prostotę i lekkość cechują się wysoką szybkością, ale mają znacznie ograniczone możliwości pod względem operacji, jakie można na nich wykonywać w porównaniu do typów referencyjnych, dlatego w zasadzie każdy typ prymitywny posiada odpowiadający mu typ „obiektowy” (ang. wrapper class).

Statyczne typowanie niewątpliwie zwiększa niezawodność i bezpieczeństwo tworzonego programu, aczkolwiek może utrudniać jego czytelność. W związku z tym, od wersji 10 wprowadzono w Javie typ var, który pozwala na większą swobodę poprzez automatyczne określenie typu zmiennej przez kompilator w oparciu o przypisaną do niej wartość. Nie jest to mimo wszystko zmiana, która powoduje drastyczną różnicę w kwestii przejrzystości czy łatwości tworzenia kodu, przez co Python jest językiem o zdecydowanie niższym progu wejścia.

// File: BMI.java
public class BMI {

   public static String calculateBMI(String person, double weight, int height) {
       double meters = height * 0.01;
       double squaredHeight = meters * meters;
       return "BMI of " + person + ": " + weight/squaredHeight;
   }

   public static void main(String[] args) {
       var name = "John";
       double w = 78.8;
       int h = 182;

       System.out.println(calculateBMI(name, w, h));
   }
}

//Output:
//BMI of John: 23.789397415771038

Hermetyzacja

Na czym polega hermetyzacja (enkapsulacja) wspominałem już w artykule Programowanie obiektowe na przykładzie Pythona.

Enkapsulacja w Pythonie

Python traktuje wszystkie atrybuty i metody domyślnie jako publiczne, tj. nie ogranicza dostępu do nich w żaden sposób. Istnieją jednak mechanizmy, które to umożliwiają i są to name mangling oraz internal use.

Pierwszy z nich pozwala traktować metody i atrybuty jako prywatne (ang. private) poprzez poprzedzenie ich nazw podwójnym znakiem podkreślenia. Taki zabieg sprawia, iż są one bezpośrednio dostępne jedynie w klasie, w której zostały zdefiniowane. Drugi mechanizm natomiast jest swego rodzaju rozszerzeniem pierwszego, ponieważ wprowadza pojęcie atrybutów i metod chronionych (ang. protected), które są osiągalne również w klasach potomnych. W celu zdefiniowania elementów chronionych używamy pojedynczego znaku podkreślenia.

class BankAccount:

   def __init__(self, account_name):
       self.account_name = account_name  # public attribute
       self._operations = 10  # protected attribute
       self.__account_balance = 1500  # private attribute


if __name__ == '__main__':
   my_account = BankAccount("My private account")
   print("Account name:", my_account.account_name)
   print("Number of operations:", my_account._operations)
   print("Account balance:", my_account.__account_balance) # AttributeError


# Output:
# Account name: My private account
# Number of operations: 10

# Traceback (most recent call last):
#   File "C:\Users\sii_user\PycharmProjects\python_vs_Java\encapsulation.py", line 13, in <module>
#     print(my_account.__account_balance)  # AttributeError
#           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# AttributeError: 'BankAccount' object has no attribute '__account_balance'

Jak można zauważyć powyżej, w przypadku atrybutu chronionego self._operations nie ma żadnych ograniczeń dostępu z zewnątrz. Z kolei próba wywołania atrybutu prywatnego self.__account_balance zwróci błąd, aczkolwiek nie oznacza to, iż dostęp do niego jest całkowicie zablokowany. Stosowanie powszechnie akceptowanych rozwiązań służących do operowania na elementach „z ograniczeniami” (np. poprzez definiowanie getterów i setterów), to nie jedyny sposób na dostanie się do nich i istnieją również pewne „obejścia”:

# Workaround #1:
if __name__ == '__main__':
   my_account = BankAccount("My private account")
   print("Account balance (using 'name mangling'):", my_account._BankAccount__account_balance)

# Output:
# Account balance (using 'name mangling'): 1500

Powyższe rozwiązanie w postaci poprzedzenia atrybutu prywatnego dodatkowym podkreśleniem i nazwą klasy jest złamaniem konwencji i nie powinno się go stosować. Edytor kodu prawdopodobnie wyróżni nam je jako błąd, ale program wykona się bez problemu, co jasno pokazuje, że Python nie jest w kwestii hermetyzacji bardzo restrykcyjny.

Poniższe przykłady z użyciem __dict__ oraz inspect również potwierdzają, iż używanie „nazw zastępczych” nie jest pełnym zabezpieczeniem. Ciężko jednak nie zgodzić się ze stwierdzeniem, że działa ono jako warstwa ochrony, motywując niejako programistów do przestrzegania dobrych praktyk.

# Workaround #2:
if __name__ == '__main__':
   my_account = BankAccount("My private account")
   print("Account balance (using '__dict__):",
         my_account.__dict__['_BankAccount__account_balance'])

# Output:
# Account balance (using __dict__): 1500


# Workaround #3:
if __name__ == '__main__':
   import inspect

   my_account = BankAccount("My private account")

   # get all members of my_account object as (name, value) pairs
   # for name, value in inspect.getmembers(my_account):
   #     print(f"{name}: {value}")

   # based on the returned object members
   print(
       "Account balance (using 'inspect'):",
       getattr(my_account, '_BankAccount__account_balance', None)
   )

# Output:
# Account balance (using 'inspect'): 1500

Enkapsulacja w Javie

Java podchodzi do tematu bardziej bezkompromisowo. Widoczność klas, metod i atrybutów określamy tutaj za pomocą modyfikatorów dostępu w postaci następujących słów kluczowych:

  • public – element jest dostępny z dowolnego miejsca w obrębie pakietu i poza nim;
  • protected – element jest dostępny w obrębie klasy w której został zdefiniowany, jak również w klasach dziedziczących niezależnie od pakietu, w którym się znajdują;
  • private – element jest dostępny jedynie w klasie, w której został zdefiniowany;
  • default – brak modyfikatora; element, który nie został poprzedzony żadnym z powyższych modyfikatorów ma tzw. dostęp pakietowy (domyślny), czyli jest dostępny w obrębie pakietu, w którym został zdefiniowany.
// File: BankAccount.java
public class BankAccount {
   public String accountName; // Available everywhere
   protected Integer operations = 0; // Available in the class, subclasses and package
   private Integer accountBalance = 0; // Available only in the class
   String generalInfo; // Available in the package (default)

   // public constructor
   public BankAccount(String accountName, Integer accountBalance, String generalInfo) {
       this.accountName = accountName;
       this.accountBalance = accountBalance;
       this.generalInfo = generalInfo;
   }

   // Methods:
   public String getAccountName() { return this.accountName; }

   protected void getOperations() { System.out.println("Number of operations: " + this.operations); }

// File: Runner.java
public class Runner {

   public static void main(String[] args) {
       BankAccount my_account = new BankAccount(
               "Savings account",
               12000,
               "Savings for holidays"
       );

       // Access to public attr:
       System.out.println("Name: " + my_account.accountName);
       // Access to protected attr (in the same package):
       System.out.println("Operations: " + my_account.operations);
       // Access to private attr (not possible - commented out):
       // System.out.println(my_account.accountBalance);
       // Access to default attr (in the same package):
       System.out.println("Info: " + my_account.generalInfo);
       // Access to public mtd:
       System.out.println("Name: " + my_account.getAccountName());
       // Access to protected mtd (in the same package):
       my_account.getOperations();
       // Access to private mtd (not possible - commented out)
       // System.out.println(my_account.getAccountBalance());
       // Access to default mtd (in the same package):
       my_account.getInfo();
   }
}

//Output:
//Name: Savings account
//Operations: 0
//Info: Savings for holidays
//Name: Savings account
//Number of operations: 0
//Account name: Savings account
//Account balance: 12000
//Number of operations: 0
//Savings for holidays

Dzięki modyfikatorom dostępu Java jest językiem bardziej rygorystycznym – ciężej tutaj o doprowadzenie do braku spójności danych, ponieważ nie posiada ona analogicznych do Pythona rozwiązań pozwalających na omijanie hermetyzacji.

Nie jest jednak prawdą, że podobny efekt jest niemożliwy do osiągnięcia. Chcąc uzyskać dostęp do metod i atrybutów określonych jako private, można posłużyć się mechanizmem zwanym refleksją (ang. reflection), który pozwala nam na dynamiczne badanie i modyfikowanie struktury programu w czasie jego działania.

// File: Reflection.java
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Reflection {

   public static void main(String[] args) {
       BankAccount my_account = new BankAccount(
               "Savings account",
               12000,
               "Savings for holidays"
       );

       try {
           Field privateField = BankAccount.class.getDeclaredField("accountBalance");
           privateField.setAccessible(true);
           System.out.println("Account balance (using privateField): " + privateField.get(my_account));

           Method privateMethod = BankAccount.class.getDeclaredMethod("getAccountBalance");
           privateMethod.setAccessible(true);
           System.out.println("Account balance (using privateMethod): " + privateMethod.invoke(my_account));
       } catch (Exception err) {
           err.printStackTrace();
       }
   }
}

//Output:
//Account balance (using privateField): 12000
//Account balance (using privateMethod): 12000

Interfejsy

Każdy rodzaj obiektów, czy to w Pythonie, czy w Javie, udostępnia pewien zestaw operacji, które można na nim wykonać, a co za tym idzie – rozwiązać za jego pomocą jakiś problem. Przykładowo, w Pythonie dla obiektu o typie str będzie to metoda upper(), zaś w Javie – typ String i metoda toUpperCase().

To, jakie zachowanie jest możliwe do uzyskania na obiekcie danej klasy (jakie metody będzie ten obiekt posiadał – co będzie robił, czy inaczej – jaki interfejs będzie udostępniał), określają tzw. kontrakty. To właśnie one zawierają reguły, jakim powinna podlegać konkretna implementacja, tj. deklaracje jakich metod są wymagane oraz jakie są ich oczekiwane zachowania.

Definiując nowy interfejs, założenie jest więc proste: definiujemy w nim tylko metody określone w „kontrakcie”, a ciała tych metod pozostawiamy puste, tj. nie implementujemy ich zachowania – stanowią one jedynie swego rodzaju placeholdery i dopiero klasy zbudowane w oparciu o dany interfejs dostarczają własne implementacje owych metod. Takie podejście sprawia, że tworzony przez nas kod jest spójny i spełnia określone wymagania.

Interfejsy w Pythonie

Jak łatwo się domyślić, Python i Java w kwestii tworzenia interfejsów preferują odmienne podejścia. Można nawet przyjąć, że w Pythonie pojęcie interfejsu w ogóle nie istnieje, gdyż język ten nie posiada osobnych struktur do ich opisywania. W celu utworzenia „zbioru” metod dla innych klas stosuje się tutaj klasy i metody abstrakcyjne.

# Python abstract class
from abc import ABC, abstractmethod


class Animal(ABC):

   @abstractmethod
   def move(self):
       pass

   @abstractmethod
   def eat(self):
       pass

   @abstractmethod
   def make_sound(self):
       print("Making sound")

   @staticmethod
   def double_number(a: int):
       return a * 2

Wykorzystanie abstrakcji do utworzenia struktury imitującej interfejs powoduje, że dopuszczalne są pewne odstępstwa od głównych założeń, jakie prawidłowy interfejs powinien spełniać. Nie ma w tym przypadku problemu, aby metoda należąca do klasy abstrakcyjnej (metoda abstrakcyjna) posiadała własną implementację (ciało). Możliwe jest definiowanie metod w ogóle niezwiązanych z określoną funkcjonalnością, zaś konkretna implementacja (tworzenie klas na podstawie ogólnej klasy abstrakcyjnej) odbywa się z wykorzystaniem standardowego dziedziczenia.

Oprócz tego, z definiowaniem interfejsów w Pythonie łączy się pojęcie protokołów (ang. protocols). Pozwalają one również określać zbiór metod i atrybutów, które dana klasa powinna zawierać, aby wspierać konkretną funkcjonalność, ale odbywa się to bez konieczności stosowania dziedziczenia. Jest to zatem mechanizm bardzo elastyczny, gdzie programista skupia się głównie na implementacji pożądanych zachowań bez potrzeby poświęcania swojej uwagi na złożone relacje pomiędzy klasami.

from typing import Protocol


class Figure(Protocol):

   def create(self, tool): ...


class Circle:

   def create(self, radius):
       print(f"Creating a circle with a radius: {radius}")


class Cube:

   def create(self, side):
       print(f"Creating a cube with a side: {side}")


class Triangle:

   def draw(self, height):
       print(f"Creating a triangle with a height: {height}")


def create_item(item: Figure, *args):
   item.create(*args)


if __name__ == '__main__':
   circle = Circle()
   cube = Cube()
   triangle = Triangle()

   create_item(circle, 5)
   create_item(cube, 2)
   create_item(triangle, 10)  # Expected type 'Figure', got 'Triangle' instead

# Output:
# Creating a circle with a radius: 5
# Creating a cube with a side: 2
# Traceback (most recent call last):
#   File "C:\Users\sii_user\PycharmProjects\python_vs_Java\protocol.py", line 39, in <module>
#     create_item(triangle, 10)  # Expected type 'Figure', got 'Triangle' instead
#     ^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\Users\mwilk\PycharmProjects\python_vs_Java\protocol.py", line 29, in create_item
#     item.create(*args)
#     ^^^^^^^^^^^
# AttributeError: 'Triangle' object has no attribute 'create'

Przykład ilustruje tworzenie protokołu Figure oraz trzech klas, z których dwie (Circle i Cube) implementują go poprzez posiadanie metody create i jedna (Triangle) nieodnosząca się do niego w żaden sposób. Wszystko odbywa się bez dziedziczenia, przez co klasy są niezależne, a tym co łączy je ze sobą (lub nie) jest posiadanie wspólnej metody create.

Interfejsy w Javie

W Javie protokoły nie są obecne, ale klasy abstrakcyjne już tak i działają one na zasadach podobnych, jak w Pythonie – mogą zawierać zarówno deklaracje metod abstrakcyjnych, jak i ich implementacje. Nie są one natomiast bezpośrednio wykorzystywane do budowy interfejsów. W tym celu służy dedykowana struktura zwana po prostu interface pozwalająca tworzyć tzw. sygnatury metod tj. deklaracje składające się jedynie z nazwy, listy parametrów wraz z typami oraz niekiedy obsługiwanymi wyjątkami.

//File: Interfaces.java
interface FirstInterface {
   void methodA();

   void methodB();
}

interface SecondInterface {
   void methodX();

   default void defaultMethod() {
       System.out.println("Default methods are allowed!");
   }

   static void staticMethod() {
       System.out.println("Static methods are allowed!");
   }
}


//File: InterfaceImp.java
public class InterfaceImp implements FirstInterface, SecondInterface {
   @Override
   public void methodA() {
       System.out.println("Implementation of methodA");
   }

   @Override
   public void methodB() {
       System.out.println("Implementation of methodB");
   }

   @Override
   public void methodX() {
       System.out.println("Implementation of methodX");
   }
}


//File: InterfaceMain.java
public class InterfaceMain {
   public static void main(String[] args) {
       InterfaceImp object = new InterfaceImp();

       object.methodA();
       object.methodB();
       object.methodX();
       object.defaultMethod();
       SecondInterface.staticMethod();
   }
}

//Output:
//Implementation of methodA
//Implementation of methodB
//Implementation of methodX
//Default methods are allowed!
//Static methods are allowed!

Warto odnotować, że implementacja interfejsów w Javie nie odbywa się poprzez dziedziczenie (jak w Pythonie), a z wykorzystaniem słowa kluczowego implements. Pozwala to w pewnym sensie na osiągnięcie wielodziedziczenia, które domyślnie nie jest w tym języku możliwe – klasa w Javie może dziedziczyć (za pomocą słowa kluczowego extends) tylko po jednej klasie, ale implementować (implements) wiele interfejsów naraz.

Klasa abstrakcyjnaInterfejs
Może zawierać zarówno metody abstrakcyjne (bez ciała), jak i nieabstrakcyjneMoże zawierać tylko metody abstrakcyjne (bez ciała) przewidziane dla danego interfejsu
Może implementować inny interfejs oraz rozszerzać inną klasęMoże implementować inny interfejs
Klasa pochodna może rozszerzać tylko jedną klasę abstrakcyjną (brak wielodziedziczenia w Javie)Klasa pochodna może implementować wiele interfejsów naraz (wielodziedziczenie)
Modyfikatory dostępu dla klasy abstrakcyjnej:
public, protected, private, default
Modyfikatory dostępu dla interfejsu: public, default
Tab. 1 Klasa abstrakcyjna a interfejs

Osobna konstrukcja dedykowana interfejsom dostępna w Javie jest jej niewątpliwą zaletą. Od razu nasuwa się przez to na myśl, że w przypadku tego języka nie ma mowy o żadnych osobliwościach i wszystkie restrykcje stawiane interfejsom z definicji muszą zostać spełnione.

Nie jest to do końca prawda, gdyż – jak wykazałem powyżej – od wersji 8+ dopuszcza się deklarowanie w interfejsach metod domyślnych (ang. default methods) i statycznych. Powodem takiej zmiany jest m.in.: chęć trzymania funkcjonalności wspólnych dla klas implementujących dany interfejs w jednym miejscu, co zdecydowanie zmniejsza redundancję i ułatwia utrzymanie kodu.

Polimorfizm

Polimorfizm ma szczególne znaczenie w programowaniu obiektowym, ponieważ wspiera tworzenie elastycznego i reużywalnego kodu. Pozwala on zdefiniować pojedynczy interfejs, który może być wykorzystany dla różnych implementacji. Oznacza to, że mimo, iż posiadamy w naszym kodzie metody o identycznych nazwach, to ich zachowania różnią się od siebie zależnie od obiektu, na którym są wywoływane.

Mówiąc o polimorfizmie nie sposób nie wspomnieć o dwóch jego odmianach, czyli o method overriding (nadpisywanie metod) oraz method overloading (przeciążanie metod).

Nadpisywanie metod dotyczy dziedziczenia, gdzie każda z klas pochodnych może dostarczać swoją własną implementację metody odziedziczonej z klasy nadrzędnej. Mówiąc wprost: nadpisujemy (lub rozszerzamy) odziedziczoną metodę przez co każdy obiekty może zachowywać się w inny sposób. Z punktu widzenia Pythona method overriding wydaje się być bardziej „naturalny”, aczkolwiek występuje on w obu językach.

# Method overriding in Python
class Mammal:
   division = "mammal"

   def __init__(self, name):
       self.name = name

   def make_sound(self):
       print(f"{self.name} is making a sound...")


class Dog(Mammal):

   def __init__(self, name, breed):
       super(Dog, self).__init__(name) # extending inherited method
       self.breed = breed

   def make_sound(self):
       print("Woof woof!")         # overriding inherited method


if __name__ == '__main__':
   animal = Mammal("Animal")
   animal.make_sound()
   doggie = Dog("Pluto", "bulldog")
   doggie.make_sound()


# Output:
# Animal is making a sound...
# Woof woof!


// Method overriding in Java
// File: Mammal.java
public class Mammal {
   protected final String division = "mammal";
   private String name;

   public Mammal(String name) {
       this.name = name;
   }

   public void makeSound() {
       System.out.println(name + " is making sound...");
   }
}


// File: Dog.java
public class Dog extends Mammal {
   private String breed;

   public Dog(String name, String breed) {
       super(name);            // extending inherited method
       this.breed = breed;
   }

   @Override
   public void makeSound() {
       System.out.println("Woof woof!");   // overriding inherited method
   }
}

// File: MainRunner.java
public class MainRunner {
   public static void main(String[] args) {
       Mammal animal = new Mammal("Animal");
       animal.makeSound();
       Dog doggie = new Dog("Pluto", "bulldog");
       doggie.makeSound();
   }
}

// Output:
// Animal is making sound...
// Woof woof!

Inaczej ma się sprawa w przypadku method overloading’u, który charakteryzuje bardziej języki statyczne (Javę, C++) i w samym Pythonie, jako języku dynamicznie typowanym, bezpośrednio nie istnieje. Przeciążanie metod ma miejsce w sytuacji, gdy definiujemy kilka wersji tej samej metody, a tym co decyduje o jej zachowaniu w danej chwili jest liczba i typy przekazanych parametrów. W momencie wywołania kompilator sam decyduje, której implementacji użyć.

// File: OverloadMethods.java
public class OverloadMethods {

   public void combineItems(int itemOne, int itemTwo) {
       int result = itemOne + itemTwo;
       System.out.println("Result of combined items is: " + result);
   }

   public void combineItems(int itemOne, int itemTwo, int itemThree) {
       int result = itemOne + itemTwo + itemThree;
       System.out.println("Result of combined items is: " + result);
   }

   public void combineItems(String itemOne, String itemTwo) {
       String result = itemOne + itemTwo;
       System.out.println("Result of combined items is: " + result);
   }
}

// File: RunMethods.java
public class RunMethods {
   public static void main(String[] args) {
       OverloadMethods object = new OverloadMethods();
       object.combineItems(3, 5);
       object.combineItems(3, 5, 7);
       object.combineItems("One", "Two");
   }
}

// Output:
// Result of combined items is: 8
// Result of combined items is: 15
// Result of combined items is: OneTwo

Co z tym przeciążaniem w Pythonie?

Domyślnie Python nie umożliwia korzystania z method overloading’u w sposób, jaki robi to Java. Każda próba zdefiniowania metod o tej samej nazwie różniących się od siebie liczbą i/lub typem parametrów spowoduje, że skorzystać będziemy mogli tylko z jednej (ostatniej) z nich. Istnieje jednak pewien wyjątek, czyli tzw. przeciążanie operatorów (którego z kolei nie posiada Java ;). Charakteryzuje się ono tym, że konkretny operator (arytmetyczny, logiczny, porównania etc.) również może posiadać różne, zależne od kontekstu, implementacje.

from typing import List, Union


class Box:

   def __init__(self, elements: List = []):
       self.elements = elements

   def __add__(self, item: Union[str, int, bool]):
       return self.elements.append(item)

   def __str__(self):
       return f"Elements in the box: {self.elements}"


class IntNumbers:

   def __init__(self, a: int, b: int):
       self.a = a
       self.b = b

   def __add__(self, to_add: int):
       return IntNumbers(self.a + to_add, self.b + to_add)


if __name__ == '__main__':
   box = Box([1, "abc", True])
   print(box)
   box + "new_item"
   print(box)

   nums = IntNumbers(5, 10)
   print(f"Numbers: {nums.a}, {nums.b}")
   nums = nums + 5
   print(f"Numbers: {nums.a}, {nums.b}")


# Output:
# Elements in the box: [1, 'abc', True]
# Elements in the box: [1, 'abc', True, 'new_item']
# Numbers: 5, 10
# Numbers: 10, 15

Przeciążanie operatorów odbywa się za pomocą metod specjalnych. Przykład powyżej ilustruje, jak może zmieniać się zachowywanie operatora “+” w przypadku zastosowania go na różnych obiektach. Każda z klas zawiera tutaj własną definicję metody specjalnej __add__, odpowiadającej właśnie za sposób działania operatora dodawania – w przypadku klasy Box użycie “+” spowoduje dorzucenie elementu do listy, zaś dla klasy IntNumbers dodanie konkretnej wartości do każdego z dwóch jej atrybutów.

Inne metody

Przeciążanie operatorów to nie jedyny przykład method overloading’u w Pythonie. W pewnej uproszczonej formie możemy go uzyskać także poprzez:

  1. użycie instrukcji warunkowych wraz z metodą wbudowaną isinstance();
def ovrld_with_isinstance(param1, param2):
   if isinstance(param1, int) and isinstance(param2, int):
       print("Both parameters are compatible!")
   else:
       print("At least one parameter is incompatible!")


if __name__ == '__main__':
   ovrld_with_isinstance(11, 15)
   ovrld_with_isinstance(100, [1, 2])

# Output:
# Both parameters are compatible!
# At least one parameter is incompatible!
  1. przekazywanie zmiennej liczby argumentów za pomocą *args i/lub **kwargs;
def ovrld_with_args_kwargs(*args, **kwargs):
   if len(args) >= 1 and kwargs.get("key"):
       print("*args and **kwargs have been passed!")
   elif kwargs.get("key") is None:
       print("**kwargs have not been passed...")


if __name__ == '__main__':
   ovrld_with_args_kwargs(8, "string", key="value")
   ovrld_with_args_kwargs(8, "string")

# Output:
# *args and **kwargs have been passed!
# **kwargs have not been passed...
  1. przypisywanie argumentom domyślnej wartości None
def ovrld_with_none(param1=None, param2=None):
   if isinstance(param1, float) and param2 is None:
       print("Proceed with param1...")
   elif param1 is None and param2 is None:
       print("Received invalid arguments")


if __name__ == '__main__':
   ovrld_with_none()
   ovrld_with_none(10.120)

# Output:
# Received invalid arguments
# Proceed with param1...
  1. zastosowanie singledispatch z modułu functools
from functools import singledispatch


@singledispatch
def method(item):
   raise ValueError("Provided data type is not supported!")


@method.register
def _(item: str):
   print("Provided data type: string")


@method.register
def _(item: list):
   print("Provided data type: list")


if __name__ == '__main__':
   method("text")
   method([True, 150])
   # method(150)  # ValueError: Provided data type is not supported!

# Output:
# Provided data type: string
# Provided data type: list

Podsumowanie

Oba języki znacznie różnią się od siebie i każdy z nich ma swoje unikalne cechy, które sprawiają, że znajdują one zastosowanie w innych, specyficznych dziedzinach.

  • Python znany ze swojej prostej i zwięzłej składni, dynamicznego podejścia i szerokiej gamy dostępnych bibliotek świetnie sprawdza się w automatyzacji zadań, analizie danych czy przy tworzeniu aplikacji webowych.
  • Java cechująca się z kolei wysoką wydajnością i skalowalnością, wspierająca silne typowanie i posiadająca bogaty ekosystem, idealnie pasuje do rozwijania systemów rozproszonych, aplikacji mobilnych czy korporacyjnych.

Tak szerokie wykorzystanie każdego z podejść powoduje, że zdecydowanie warto posiąść przynajmniej ogólną wiedzę na temat każdego z nich.

Źródła

4.6/5 ( głosy: 5)
Ocena:
4.6/5 ( głosy: 5)
Autor
Avatar
Maciej Wilk

W Sii od 2020 roku. Aktualnie zajmuje się automatyzacją testów w Pythonie, choć posiada również doświadczenie w developmencie. W swojej dotychczasowej pracy miał ponadto styczność z takimi technologiami jak m.in.: PL/SQL, bazy danych Oracle czy R. Prywatnie pasjonat siłowni i jazdy na „szosie”. Lubi się uczyć. Pomieszkiwał w Finlandii i Australii

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?