Programiści bardzo rzadko są w stanie utrzymać się znając tylko jedną technologię. Języki oprogramowania realizują podobne zadania w różnyh sposób. W polskim internecie brakuje kompleksowego zestawienia różnic pomiędzy różnymi językami w formie znanego programistom "diffa" z komentarzem.
W niniejszym artykule omówię różnice między językiem C++ (wersja C++23) a językiem Python (wersja 3.11) w formie "diffa" z komentarzem. Podkreślam jednak, będą to najczęstsze/najpopularniejsze aspekty. Porównuję odmienne podejścia w programowaniu - język wieloparadygmatowy kompilowalny z językiem skryptowym. Zatem takie zestawienie nie odwzorowuje stopnia zaawansowania między językami.
Celem pracy jest zestawienie różnic składniowych w programowaniu w tych językach. Realia w wielu firmach są takie, że kluczowe projekty powstają w C++, a w Pythonie przygotowuje się mniejsze narzędzia w formie skryptów. W dużych firmach osoby uczące się jednego z tych języków mogą nie znać drugiego, takie zestawienie im pomoże.
Podstawy
1. Pierwszy program wyświetlający tekst wraz z komentarzem:
W C++ każdy program musi mieć funkcję
main
aby był programem. Jednakże dla prostoty dalszych przykładów będę czasami pomijał nagłówki i jawne napisanie funkcji
main
. W Python każdy poprawny program będzie działać od początku.
2. Rozszerzenia plików
3. Wykonanie programu
4. Typy wbudowane:
5. Czas życia zmiennych:
6. Wskaźniki i zarządzanie pamięcią
7. Operatory:
Mimo pozorów występują tutaj rozbieżności, warto to przejrzeć szczegółowo.
Jak widać występuje duża rozbieżność w operatorach rzutowania, osoby nie znające ich powinny poczytać na ich temat w zewnętrznym źródle np. dobrej książce.
Język python zawiera jeszcze operatory do operowania na listach
"kot" in ["ale", "ma", "kota"]
. Ich opis znajduje się w dalszej części artkułu.
8. Instrukcje sterujące:
9. Operowanie na tekście:
kryterium |
C++ |
python3 |
typ do obsługi tekstu |
obsługa tekstu nie jest wbudowana w język, za to jest elementem biblioteki standardowej std::string . Do jego użycia konieczny jest nagłówek #include <string> |
jest to typ wbudowany str |
obsługa znaków narodowych (polskie znaki) |
typ std::string używa znaków z zakresu char , jest to zakres jedno-bajtowy, z założenia pozbawiony znaków narodowych (poza ASCII). Jednakże jeśli umieścimy polskie znaki np. std::string text = "Żółta łąka. ĘÓĄŚŁŻŹĆŃ ęóąśłżźćń. Zażółć gęślą jaźń"; std::cout << text << '\t' << text.size() << endl; to zostaną one zakodowane jako większa liczba znaków, a jaka to już zależy od kodowania aktualnego pliku. Aby polskie znaki działały w konsoli jej kodowanie musi być takie same jak kodowanie pliku źródłowego z kodem programu. Systemy Linuxowe i MacOS domyślnie koduje pliki i tekst w konsoli jako UFT-8, więc powyższy kod zadziała nam od strzała na tych systemach. Niestety na systemie Windows zapewne będziemy się musieli nagimnastykować aby uzyskać polskie znaki w konsoli. Kodowanie pliku źródłowego można sprawdzić w ustawieniach środowiska programistycznego (w razie czego mamy jeszcze Notepad++), kodowanie w konsoli można sprawdzić komendą chcp , przy pomocy tej komendy można zmienić kodowanie np. chcp 65001 ustawi kodowanie UTF-8 w konsoli, ale na starszych wersjach Windowsa zapewne będziemy musieli się trochę więcej nagimnastykować. Temat ten został szczegółowo omówiony w tutorialu https://miroslawzelent.pl/kurs-c++/polskie-znaki-konsola-terminal-windows-linux-macos/ |
Domyślnym kodowaniem kodu źródłowego Pythona jest UTF-8. Możesz użyć prawie dowolnego kodowania, jeśli zadeklarujesz używane kodowanie. Odbywa się to poprzez dodanie specjalnego komentarza w pierwszej lub drugiej linii pliku źródłowego poniższej deklaracji: Jeśli nie dołączysz takiego komentarza, domyślnym zastosowanym kodowaniem będzie UTF-8. Komentarz umieszcza się w drugiej lini, gdy pierwszą jest deklaracja interpretera języka python np.: Kodowanie w python opisano szczegółowo na stronie https://docs.python.org/3/howto/unicode.html
|
przykłady użycia: |
#include <iostream> #include <string> using namespace std;
int main() { string s1 = "Hello"; string s2( "C++" ); string s3 = s1 + s2; string s4 = s1 + ' ' + s2; cout << "s3: " << s3 << endl; cout << "s4: " << s4 << endl; cout << "Pierwszy znak s2: " << s2[ 0 ] << endl; cout << "Rozmiar s3: " << s3.size() << endl; if( auto position = s4.find( "C++" ); position != string::npos ) { cout << "Znaleziono 'C++' w s4 na pozycji: " << position << endl; } else { cout << "Nie znaleziono 'C++' w s4" << endl; } cout << "Pierwszy znak: " << s2.front() << endl; cout << "Ostatni znak: " << s2.back() << endl; cout << "Czy ciąg jest pusty? " <<( s2.empty() ? "Tak" : "Nie" ) << endl; string substring = "++"; cout << "Czy ciąg zawiera '" << substring << "'? " <<( s4.contains( substring ) ? "Tak" : "Nie" ) << endl; string prefix = "He"; cout << "Czy ciąg rozpoczyna się od '" << prefix << "'? " <<( s3.starts_with( prefix ) ? "Tak" : "Nie" ) << endl; string suffix = "++"; cout << "Czy ciąg kończy się '" << suffix << "'? " <<( s4.ends_with( suffix ) ? "Tak" : "Nie" ) << endl; string replacedString = s4; string replacement = "Best Language "; replacedString.replace( 6, 0, replacement ); cout << "Zamiana '" << s1 << "' na '" << replacedString << "': " << replacedString << endl; string sub = s4.substr( 6, 3 ); cout << "Podciąg od indeksu 7 o długości 5 znaków: " << sub << endl; }
Pewne z użytych funkcji zostały dodane do C++ w nowych wersjach C++, są to:
w starszych wersjach mogą nie działać. |
if __name__ == "__main__": s1 = "Hello" s2 = "Python"
s3 = s1 + s2 s4 = s1 + ' ' + s2 print("s3:", s3) print("s4:", s4) print("Pierwszy znak s2:", s2[0]) print("Rozmiar s3:", len(s3)) if "Python" in s4: position = s4.index("Python") print("Znaleziono 'Python' w s4 na pozycji:", position) else: print("Nie znaleziono 'Python' w s4")
print("Pierwszy znak:", s2[0]) print("Ostatni znak:", s2[-1]) print("Czy ciąg jest pusty?", "Tak" if not s2 else "Nie") substring = "Py" print("Czy ciąg zawiera '", substring, "'?", "Tak" if substring in s4 else "Nie") prefix = "He" print("Czy ciąg rozpoczyna się od '", prefix, "'?", "Tak" if s3.startswith(prefix) else "Nie") suffix = "on" print("Czy ciąg kończy się '", suffix, "'?", "Tak" if s4.endswith(suffix) else "Nie") replacedString = s4.replace("Python", "Best Language") print("Zamiana 'Python' na 'Best Language':", replacedString) start_index = 6 length = 5 sub = s4[start_index:start_index + length] print("Podciąg od indeksu", start_index, "o długości", length, "znaków:", sub) |
ciekawy przypadek - metoda replace |
std::string::replace zamienia jedynie pierwsze wystąpienie |
str.replace zamienia wszystkie wystąpienia |
konwersja między tekstem a liczbami |
#include <iostream> #include <string> using namespace std; using namespace std::literals::string_literals; int main() { cout << to_string( 112 ) << ' ' << to_string( 3.14f ) << ' ' << to_string( 2.71 ) << ' ' << to_string( 10'000'000'000LL ) << ' ' << endl; cout << stoi( "112"s ) << ' ' << stof( "3.14"s ) << ' ' << stod( "2.71"s ) << ' ' << stoll( "2304"s ) << endl; }
Dla typu użytkownika można zdefiniować operator konwersji na std::string
|
if __name__ == "__main__": print(str(112), str(3.14), end=' ') print(str(2.71), str(10_000_000_000))
print(int("112"), float("3.14"), end=' ') print(float("2.71"), int("2304"))
Dla typu użytkownika można zdefiniować metodę __str__ lub __repr__ zamieniającą obiekt użytkownika na tekst. |
zmiana wielkości liter |
nie jest to wbudowane w std::string , trzeba użyć tricku:
#include <iostream> #include <string> #include <algorithm> #include <cctype> using namespace std;
int main() { string text { "Musicie od siebie wymagac, nawet gdyby inni od Was nie wymagali!" }; ranges::transform( text, begin( text ), static_cast < int( * )( int ) >(::toupper ) ); cout << text << endl; ranges::transform( text, begin( text ), static_cast < int( * )( int ) >(::tolower ) ); cout << text << endl; } |
wbudowane w język:
if __name__ == "__main__": text = "Musicie od siebie wymagac, nawet gdyby inni od Was nie wymagali!" text_upper = text.upper() print(text_upper)
text_lower = text.lower() print(text_lower)
|
użycie funkcji do podziału tekstu wg słowa, złączenia słów wg tekstu, przycięcie białych znaków |
C++ nie posiada wbudowanych funkcji do tego celu. Można to obejść używając biblioteki boost, więcej informacji w artykule Praca z tekstem przy użyciu dostępnych narzędzi w C++, przykładowy kod używający biblioteki:
#include <iostream> #include <string> #include <vector> #include <boost/algorithm/string.hpp> int main() { std::string sentence = "To jest przykładowe zdanie do podziału na słowa."; std::vector < std::string > words; boost::split( words, sentence, boost::is_any_of( " " ) ); std::cout << "\nOryginalne zdanie: " << sentence << std::endl; std::cout << "Podzielone słowa:"; for( const std::string & word: words ) std::cout << " " << word; std::cout << std::endl; std::string text_with_spaces = " Tekst z dodatkowymi spacjami "; boost::trim( text_with_spaces ); std::cout << "\nOryginalny tekst z spacjami: " << text_with_spaces << std::endl; std::vector < std::string > list_of_words = { "To", "jest", "lista", "słów" }; std::string joined_text = boost::algorithm::join( list_of_words, " " ); std::cout << "Lista słów:"; for( const std::string & word: list_of_words ) std::cout << " " << word; std::cout << "\nPołączony tekst: " << joined_text << std::endl; }
|
sentence = "To jest przykładowe zdanie do podziału na słowa." words = sentence.split() print("\nOryginalne zdanie:", sentence) print("Podzielone słowa:", words)
text_with_spaces = " Tekst z dodatkowymi spacjami " stripped_text = text_with_spaces.strip() print("\nOryginalny tekst z spacjami:", text_with_spaces) print("Tekst po usunięciu zbędnych spacji:", stripped_text)
list_of_words = ["To", "jest", "lista", "słów"] joined_text = " ".join(list_of_words) print("\nLista słów:", list_of_words) print("Połączony tekst:", joined_text) |
klasyfikacja tekstu (np. czy zawiera tylko liczby) |
Można tylko sprawdzić pojedyńczy znak, zatem aby sprawdzić cały tekst należy użyć pewnego tricku:
#include <iostream> #include <algorithm> #include <cctype> using namespace std;
int main() { string characters = "123456"; if( std::ranges::all_of( characters,[ ]( auto c ) { return std::isdigit( c ); } ) ) ) { cout << characters << " to cyfra" << endl; } else { cout << characters << " nie jest cyfrą" << endl; } characters = " \t\n"; if( std::ranges::all_of( characters,[ ]( auto c ) { return std::isspace( c ); } ) ) ) { cout << "'" << characters << "' to biały znak" << endl; } else { cout << "'" << characters << "' nie jest białym znakiem" << endl; } }
|
characters = "123456" if characters.isdigit(): print(f"'{characters}' to cyfra") else: print(f"'{characters}' nie jest cyfrą")
characters = " \t\n" if characters.isspace(): print(f"'{characters}' to biały znak") else: print(f"'{characters}' nie jest białym znakiem")
|
wygodne formatowanie tekstu zawierającego zawartość innych zmiennych |
#include <iostream> #include <format> using namespace std;
int main() { string name = "John"; int age = 30; double number = 12345.6789; string formatted = std::format( "Hello, {}! You are {} years old.", name, age ); std::string formatted_number = std::format( "{:.1f}", number ); cout << formatted << endl; cout << formatted_number << endl; }
std::format zostało dodane do C++ w wersji C++20. Jeśli potrzebujesz tej funkcjonalności we wcześniejszych wersjach możesz użyć biblioteki fmt. W C++23 została dodana funkcja znana dotychczas z Javy: std::println() , której można wygodnie używać do formatowania tak jak przy użyciu std::format . |
if __name__ == '__main__': name = "John" age = 30 number = 12345.6789
formatted = f"Hello, {name}! You are {age} years old." formatted_number = "{:.1f}".format(number) print(formatted) print(formatted_number) |
10. Podstawowe kontenery na dane: tablice (C++) vs listy i tuple (Python):
Jest to aspekt, który ciężko jest porównać w takiej formie jak do tej pory.
kryterium |
C++ |
python3 |
podstawowa różnica |
W C++ mamy lepszą kontrolę nad pamięcią, dlatego możemy tworzyć tablice o rozmiarze znanym w trakcie kompilacji, jak również przy pomocy wskaźników alokować (a potem pamiętać o jej zwolnieniu) pamięć wg potrzeb. Istotne jest, że tablice zawierają elementy tego samego typu. Co prawda możemy mieć tablice specjalnego typu std::any ale jest to rzadko praktykowane. |
Nie mamy aż takich możliwości zarządzania pamięcią. |
podstawowa struktura danych do przechowywania wielu elementów |
std::vector , który jest elementem biblioteki standardowej, ale nie jest wbudowany w język:
#include <iostream> #include <vector> using namespace std;
int main() { vector numbers = { 6, 24, 496 }; numbers.push_back( 8124 ); cout << "Rozmiar wektora: " << numbers.size() << endl; cout << "Pierwszy element: " << numbers[ 0 ] << endl; cout << "Ostatni element: " << numbers[ numbers.size() - 1 ] << endl; numbers[ 1 ] = 28; cout << "Elementy wektora:"; for( auto i: numbers ) { cout << " " << i; } cout << endl; numbers.pop_back(); numbers.clear(); if( numbers.empty() ) cout << "Wektor jest pusty." << endl; else cout << "Wektor nie jest pusty." << endl; }
|
Podstawową strukturą jest lista, która nie jest implementowana jako lista tylko tablica
if __name__ == '__main__': numbers = [6, 24, 496]
numbers.append(8124)
print("Rozmiar listy:", len(numbers))
print("Pierwszy element:", numbers[0]) print("Ostatni element:", numbers[-1])
numbers[1] = 28
print("Elementy listy:", end=" ") for i in numbers: print(i, end=" ") print()
numbers.pop()
numbers.clear()
if not numbers: print("Lista jest pusta.") else: print("Lista nie jest pusta.") |
struktura danych o stałym rozmiarze do przechowywania wielu elementów |
W C++ można używać zwykłych tablic znanych z języka C, natomiast zalecanym jest używanie std::array . Jest to opakowanie na tablicę o stałym rozmiarze, czyli nie można elementów dodawać i usuwać, ale można je zmieniać (chyba, że użyjemy słówka const ), przykładowo:
#include <iostream> #include <array> using namespace std;
int main() { array numbers = { 6, 24, 496 }; const array numbers2 = numbers; numbers[ 1 ] = 28; cout << "Rozmiar tablicy: " << numbers.size() << endl; cout << "Pierwszy element: " << numbers[ 0 ] << endl; cout << "Ostatni element: " << numbers[ numbers.size() - 1 ] << endl; cout << "Elementy tablicy:"; for( auto i: numbers ) { cout << " " << i; } cout << endl; if( numbers.empty() ) cout << "Tablica jest pusta." << endl; else cout << "Tablica nie jest pusta." << endl; numbers = { 1, 3, 5 }; numbers = { 44 }; }
|
Występuje tupla, która ma operacje jak lista, z tymże nie można w niej dodać/usunąć elementów ani ich zmienić, przykładowy kod:
if __name__ == '__main__': numbers = (6, 24, 496)
print("Rozmiar tupli:", len(numbers))
print("Pierwszy element:", numbers[0]) print("Ostatni element:", numbers[-1])
print("Elementy tupli:", end=" ") for i in numbers: print(i, end=" ") print()
if not numbers: print("Tupla jest pusta.") else: print("Tupla nie jest pusta.")
numbers = (2, 4, 6, 8, 10) W języku Python3 występuje struktura danych array , która przechowuje elementy tego samego typu: import array
my_array = array.array('i', [1, 2, 3, 4, 5]) print(len(my_array)) my_array[3] = 44 |
przechowywanie elementów różnych typów |
C++ jest językiem o silnym typowaniu. Wskutek tego przechowywanie elementów różnych typów nie jest wspierane. Aby przypisać konkretnej zmiennej wartości różnych typów stosuje się str::any , które można umieścić w kontenerze np.:
#include <iostream> #include <vector> #include <any>
int main() { std::vector < std::any > mixedVector; mixedVector.push_back( 42 ); mixedVector.push_back( 3.14 ); mixedVector.push_back( std::string( "Hello" ) ); for( const auto & item: mixedVector ) if( item.type() == typeid( int ) ) std::cout << "int: " << std::any_cast < int >( item ) << std::endl; else if( item.type() == typeid( double ) ) std::cout << "double: " << std::any_cast < double >( item ) << std::endl; else if( item.type() == typeid( std::string ) ) std::cout << "std::string: " << std::any_cast < std::string >( item ) << std::endl; }
Jednakże raczej się nie praktykuje takiego postępowania w C++ (jak ktoś takie rzeczy chce zrobić to wybiera inny język programowania, albo musi się douczyć).
|
Python nie ma silnego typowania, więc można przypisać dowolne typy:
if __name__ == '__main__': mixed_list = []
mixed_list.append(42) mixed_list.append(3.14) mixed_list.append("Hello") for item in mixed_list: print(f'{type(item).__name__}: {item}')
|
wyjście poza zakres |
Niezdefiniowane zachowanie programu, czyli może się program wykonywać nadal, a błąd nie zostanie od razu zauważony, może też program się wykrzaczyć/wywalić zwane segmentation fault. Dlatego w C++ zamiast używając operator[ ] można zastosować metodę at() , która w razie wyjścia poza zakres tablicy wyrzuci wyjątek, przykładowo:
#include <iostream> #include <vector>
int main() { std::vector < int > numbers = { 1, 2, 3 }; try { auto value = numbers.at( numbers.size() ); std::cout << "Wartość: " << value << std::endl; } catch( const std::out_of_range & e ) { std::cerr << "Błąd: Przekroczono zakres wektora." << std::endl; } }
|
Język python zawsze sprawdza i w razie czego wyrzuci wyjątek:
numbers = [1, 2, 3]
try: value = numbers[len(numbers)] print(f'Wartość: {value}') except IndexError: print('Błąd: Przekroczono zakres listy.')
|
11. Inne struktury danych (inne kontenery na dane)
Często używanymi kontenerami są słowniki i zbiory, które mają swoje odpowiedniki w porównywanych językach, poza tym jest wiele kontenerów, które można uznawać za równoważne. Przykładowo C++ ma około
20 kontenerów standardowych, z kolei Python3 ma kilka
kontenerów wbudowanych w język, oraz wiele innych w ramach modułu
modułu collections. W poniższej tabeli zastosowałem porównanie odpowiadających sobie kontenerów demonstrując przykłady użycia jedynie dla najpopularniejszych:
Poza powyższymi kontenerami warto uwzględnić kontenery dostępne tylko w jednym z języków, bez bezpośrednich odpowiedników w drugim języku, dostępne w standardzie obydwu języków (czyli bez konieczności doinstalowywania czegokolwiek):
12. Funkcje
kontener |
C++ |
python3 |
deklaracja i definicja funkcji |
funkcja musi być co najmniej zadeklarowana przed jej użyciem. Robi się to przykadowo tak:
void print( int number ); . Przykład deklaracji i użycia:
#include <iostream>
void print( double value );
int main() { print( 3.14 ); }
void print( double value ) { std::cout << "--->>" << value << "<<---" << std::endl; }
|
funkcji się nie deklaruje, tylko definiuje. Aby jej użyć powinna być w załączonym module, lub być zdefiniowana w tym samym pliku. Nie musi to być przed jej użyciem ale w tej sytuacji musi interpreter "zauważyć jej definicje", co widać w poniższym przykładzie:
def print_text(value): print_text_impl("--->>" + value + "<<---")
def print_text_impl(value): print("--->>", value, "<<---")
if __name__ == "__main__": print_text(3.14)
|
definicja funkcji |
w C++ definiujemy funkcje przez jej nazwę, typ zwracany i argumenty z podaniem typu, przykładowo:
#include <iostream>
int add( int a, int b );
int main() { int result = add( 5, 3 ); std::cout << "Wynik dodawania: " << result << std::endl; }
int add( int a, int b ) { return a + b; }
Funkcje najczęściej się deklaruje w jednym pliku, a w innym je definiuje. Plik z deklaracjami się załącza w miejscach gdzie funkcje są wywoływane, natomiast same definicje można dostarczyć w formie skompilowanej. Typy argumentów są wiążące, ale można użyć tzw. szablonów, aby generować funkcje dla różnych typów argumentów. W C++20 została wprowadzona skrócona forma szablonów np.: #include <iostream>
void print( auto value );
template < typename T > void print_old( T value ) { print( value ); }
int main() { print( 3.14 ); print( std::string( "Text" ) ); print( 44u ); }
void print( auto value ) { std::cout << "--->>" << value << "<<---" << std::endl; }
|
definicje funkcji się zaczyna słówkiem kluczowym def następnie nazwa funkcji, argumenty i dwukropek. Nie trzeba precyzować ani typów argumentów, ani typu zwracanego aczkolwiek można dla czytelności (program się nie wywali z powodu podania innego typu). Przykład:
def add(a, b): return a + b
def multiply(a: float, b: float) -> float: return a * b
print("Wynik dodawania:", add(5, 3)) print("Wynik mnozenia:", multiply(5, 3))
|
wartości domyślne i ich przypisywanie |
można używać domyślnych wartości argumentów, jednakże musi się to odbywać od końca argumentów. Korzystając z argumentów nie można wskazać który argument się chce przypisać ręcznie, trzeba iść od lewej do prawej, przykładowo:
#include <iostream>
void exampleFunc( int a = 1, int b = 2, int c = 3 ) { std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl; }
int main() { exampleFunc(); exampleFunc( 5 ); exampleFunc( 5, 10 ); } |
można używać domyślnych wartości argumentów, jednakże musi się to odbywać od końca argumentów. Korzystając z argumentów można wskazać który argument chce się przypisać ręcznie, wtedy nie trzeba iść od lewej do prawej wręcz można przypisać w innej kolejności niż w definicji funkcji:
def example_func(a=1, b=2, c=3): print(f"a: {a}, b: {b}, c: {c}")
def example_func2(a, *, b=4, c=5): print(f"a: {a}, b: {b}, c: {c}")
example_func() example_func(5) example_func(5, 10) example_func(5, c=15) example_func(c=7, a=2) example_func2(5, b=3) |
uchwyty do funkcji (wskaźniki i referencje) |
C++ posiada zarówno wskaźniki, jak i referencje do funkcji. Wskaźnik do funkcji można zdefiniować w klasyczny sposób, jak i przy użyciu słówka auto . Referencji do funkcji nie można przestawić na inna funkcję. Przykładowo:
#include <iostream>
void exampleFunction( int lineNumber ) { std::cout << "Funkcja zawolana z linii: " << lineNumber << std::endl; } void exampleFunction2( int ) { std::cout << "Funkcja2" << std::endl; }
int main() { void( * functionPointer1 )( int ) = exampleFunction; auto functionPointer2 = exampleFunction; void( * functionPointer3 )( int ) = & exampleFunction; void( & functionReference )( int ) = exampleFunction; functionPointer1( __LINE__ ); functionPointer2( __LINE__ ); functionPointer1 = exampleFunction2; functionPointer1( __LINE__ ); }
|
W języku python mamy referencje do funkcji, które tworzymy jak zwykłe zmienne i do nich przypisujemy funkcje, następnie wywołujemy z argumentami jakie przyjmuje ta funkcja - tylko tyle, co widać w poniższym kodzie:
import sys
def example_function(line_number): print(f"Funkcja zawolana z linii: {line_number}")
def example_function2(_): print("Funkcja2")
if __name__ == "__main__": function_reference = example_function
function_reference(sys._getframe().f_lineno) function_reference = example_function2 function_reference(None) |
przeciążanie funkcji |
Można tworzyć wiele funkcji o tej samej nazwie, ale różniących się argumentami, ale nie wystarczy aby się dwie funkcje różniły tylko typem zwracanym:
#include <iostream>
int add( int a, int b ) { return a + b; } double add( double a, double b ) { return a + b; } int main() { std::cout << "Wynik int: " << add( 5, 3 ) << std::endl; std::cout << "Wynik double: " << add( 2.5, 3.7 ) << std::endl; }
|
Nie ma przeciążania funkcji, może być jedna funkcja o takiej nazwie. Podobne zachowanie można osiągnąć przez fakt, że argumenty mogą mieć różne typy, oraz przez domyślne wartości argumentów, można też wykrywać z jakim typem mamy do czynienia np.:
def add(a, b=None): if b is None: return a else: if isinstance(a, str) or isinstance(b, str): return str(a) + str(b) else: return a + b
print("Wynik int:", add(5)) print("Wynik double:", add(2.5, 3.7)) print("Wynik string+int:", add("Przyklad:", 3))
|
przeźroczyste funkcje, czyli takie, które przyjmują uchwyt do funkcji, oraz doskonale przekazują jej argumenty |
Mamy mechanizm perfect forwarding, który umożliwia przekazanie argumentów bez ich dodatkowego kopiowania, przykład użycia:
#include <iostream> #include <chrono> #include <thread> template < typename Func, typename ... Args > double measureExecutionTime( Func && func, Args && ... args ) { auto start = std::chrono::high_resolution_clock::now(); func( std::forward < Args >( args ) ... ); auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration < double > duration = end - start; return duration.count(); }
void sampleFunction( int seconds ) { std::this_thread::sleep_for( std::chrono::seconds( seconds ) ); std::cout << "Funkcja wykonana po oczekiwaniu przez " << seconds << " sekundy." << std::endl; }
int main() { double executionTime = measureExecutionTime( sampleFunction, 1 ); std::cout << "Czas wykonania funkcji: " << executionTime << " sekundy." << std::endl; }
|
Do przekazywania otrzymanych argumentów funkcji do innej funkcji możemy użyć jednej gwiazdki na przekazanie argumentów pozycyjnych, oraz dwóch gwiazdek na przekazanie argumentów z nazwami, konwencjonalnie nazywają się te zmienne *args i **kwargs , ale to tylko konwencja, bo nazwy mogą być dowolne.A oto kod obudowujący wywołanie funkcji:
import time
def measure_execution_time(func, *args, **kwargs): start_time = time.time() func(*args, **kwargs)
end_time = time.time() execution_time = end_time - start_time return execution_time
def sample_function(seconds): time.sleep(seconds) print(f"Funkcja wykonana po oczekiwaniu przez {seconds} sekundy.")
if __name__ == "__main__": execution_time = measure_execution_time(sample_function, 1) print(f"Czas wykonania funkcji: {execution_time} sekundy.")
|
13. Typy użytkownika - klasy
Język C++ w przeciwieństwie do Pythona posiada stałe, których wartości po zdefiniowaniu nie można zmienić. Jeśli stałą jest obiekt (klasa) to nie można zawołać funkcji składowych klasy, która nie jest stała (czyli nie posiada
const
z prawej strony). Dlatego w wielu przypadkach poniżej wiele funkcji składowych, które tylko zwracają wartość nie modyfikując składowych klasy są oznaczone jako stałe, a niektóre z metod mają dwie wersje, różniące się wizualnie tylko obecnością słówka
const
z prawej strony.
kontener |
C++ |
python3 |
prosta klasa (agregat na dane bez enkapsulacji) |
Wszystkie składowe klasy muszą być zadeklarowane, wraz z typami#include <iostream>
class Fraction { public: int numberator_, denominator_; };
int main() { Fraction f { 1, 2 }; f.numberator_ = 3; std::cout << f.numberator_ << "/" << f.denominator_ << '\n'; }
W takich sytuacjach (gdy klasa jest agregatem na dane pozbawionym logiki) konwencjonalnie używa się struktur, wtedy kod wyglądałby tak: struct Fraction { int numberator_, denominator_; }; |
Nie musimy deklarować składowych klasy: class Fraction: pass
f = Fraction() f.numerator = 1 f.denominator = 2
print(f.numerator, "/", f.denominator)
|
metody klasy i enkapsulacja danych |
Klasy (oraz struktury) mogą mieć metody składowe. W klasach jest możliwe umieszczanie pól (zmiennych) klasy, oraz metod z różnym dostępem. W C++ mamy do wyboru dostęp public (dla wszystkich), private (tylko dla metod tejże klasy i przyjaciół), protected (tylko dla metod tej klasy, klas pochodnych i przyjaciół).#include <iostream>
class Fraction { private: int numberator_, denominator_; public: auto numberator() const { return numberator_; } void setNumberator( int newNumberator ) { numberator_ = newNumberator; } auto calculateValue() const { return static_cast < double >( numberator() ) / denominator(); } int denominator() const; void setDenominator( int newDenominator ); };
int main() { Fraction f; f.setNumberator( 1 ); f.setDenominator( 2 ); std::cout << f.numberator() << "/" << f.denominator() << '\n'; }
int Fraction::denominator() const { return denominator_; }
void Fraction::setDenominator( int newDenominator ) { denominator_ = newDenominator; }
|
Klasy mogą mieć metody składowe, których pierwszym argumentem jest obiekt danej klasy (konwencjonalnie nazywany self ). Klasy mogą mieć metody składowe. Python nie posiada nazwanych modyfikatorów dostępu, natomiast stosuje się pewne konwencje do dostępu do składowych. W przypadku składowych protected (dostępnych dla dzieci) stosuje się przed nazwą zmiennej jedno podkreślenie _ , ale można tej zmiennej/metody użyć na zewnątrz. Inaczej wygląda sprawa z prywatnymi składowymi - wtedy stosujemy przed nazwą składowej dwa podkreślenia __ - wtedy już nie ma dostępu z zewnątrz.
class Fraction: def __init__(self, numerator=0, denominator=1): self.__numerator = numerator self._denominator = denominator def numerator(self): return self.__numerator
def set_numerator(self, new_numerator): self.__numerator = new_numerator
def calculate_value(self): return float(self.__numerator) / self._denominator
def denominator(self): return self._denominator
def set_denominator(self, new_denominator): self._denominator = new_denominator
if __name__ == "__main__": f = Fraction(1, 2)
print(f.numerator(), "/", f.denominator()) print(f._denominator) Do oznaczenia getterow i setterow stosuje sie specjalne atrybuty metod, wtedy kod wygladal by tak: class Fraction: def __init__(self, numerator=0, denominator=1): self.__numerator = numerator self.__denominator = denominator
@property def numerator(self): return self.__numerator
@numerator.setter def numerator(self, new_numerator): self.__numerator = new_numerator
def calculate_value(self): return float(self.__numerator) / self.__denominator
@property def denominator(self): return self.__denominator
@denominator.setter def denominator(self, new_denominator): self.__denominator = new_denominator
if __name__ == "__main__": f = Fraction()
f.numerator = 1 f.denominator = 2
print(f.numerator, "/", f.denominator)
|
składowe statyczne (wspólne dla wszystkich obiektów klas) |
W C++ jest możliwe tworzenie zarówno zmiennych statycznych jak i metod. Zmienne statyczne muszą być zainicjalizowane poza klasą:
#include <iostream>
class MyClass { public: static int static_variable; static void static_method() { std::cout << "This is a static method." << std::endl; ++static_variable; } };
int MyClass::static_variable = 0;
int main() { MyClass::static_variable = 10; MyClass::static_method(); MyClass myClass; myClass.static_method(); std::cout << "Static variable: " << MyClass::static_variable << std::endl; }
W C++17 została dodana możliwość zdefiniowania zmiennej statycznej w klasie, przy pomocy słówka kluczowego, dla powyższego kodu wyglądało by to tak: static inline int static_variable; |
W Pythonie jest możliwe tworzenie zarówno zmiennych statycznych jak i metod:
class MyClass: static_variable = 0 @staticmethod def static_method(): print("This is a static method.") MyClass.static_variable += 1 MyClass.static_variable = 10 MyClass.static_method()
my_class = MyClass() my_class.static_method() print("Static variable:", MyClass.static_variable)
|
dziedziczenie składowych |
Aby wywołać konstruktor klasy bazowej należy użyć jego nazwy na liście inicjalizacyjnej konstruktora jeśli tego nie zrobimy to konstruktor bezargumentowy się wywoła.#include <iostream>
class Base { public: Base( int value ) : baseValue( value ) { std::cout << "Base constructor called with value: " << baseValue << std::endl; } void showBaseValue() { std::cout << "Base value: " << baseValue << std::endl; } private: int baseValue; };
class Derived : public Base { public: Derived( int derivedValue, int baseValue ) : Base( baseValue ) , derivedValue( derivedValue ) { std::cout << "Derived constructor called with value: " << derivedValue << std::endl; } void showDerivedValue() { std::cout << "Derived value: " << derivedValue << std::endl; } private: int derivedValue; };
int main() { Derived derivedObj( 42, 10 ); derivedObj.showBaseValue(); derivedObj.showDerivedValue(); }
Dziedziczenie w C++ jest bardziej skomplikowanym zagadnieniem, wchodzą takie zagadnienia jak m.in. kolejność wołania konstruktorów i destruktorów, wielodziedziczenie, dziedziczenie wirtualne, kolejność inicjalizacji składowych, dlatego zachęcam aby poczytać na ten temat w dobrej książce. |
Aby wywołać konstruktor klasy bazowej używamy słówka super , przykładowy kod:
class Base: def __init__(self, value): self.baseValue = value print(f"Base constructor called with value: {self.baseValue}")
def showBaseValue(self): print(f"Base value: {self.baseValue}")
class Derived(Base): def __init__(self, derivedValue, baseValue): super().__init__(baseValue) self.derivedValue = derivedValue print(f"Derived constructor called with value: {self.derivedValue}")
def showDerivedValue(self): print(f"Derived value: {self.derivedValue}")
derivedObj = Derived(42, 10) derivedObj.showBaseValue() derivedObj.showDerivedValue() |
polimorfizm |
W C++ w tym celu używa się dziedziczenia, funkcji wirtualnych i wskaźników/referencji.
Jeśli nasza klasa ma być klasą bazową przy polimorfiźmie, to raczej na pewno powinniśmy pamiętać o dodaniu wirtualnego destruktora w klasie bazowej, która ma służyć do wywołań polimorficznych. Bez tego może nie dojść do wywołania odpowiedniego destruktora z klasy pochodnej i w rezultacie nawet wycieku zasobów. #include <iostream> #include <vector>
class Shape { public: virtual void draw() const = 0; virtual ~Shape() = default; };
class Circle : public Shape { public: void draw() const override { std::cout << "Drawing a circle" << std::endl; } };
class Square : public Shape { public: void draw() const override { std::cout << "Drawing a square" << std::endl; } };
int main() { std::vector < Shape * > shapes = { new Circle(), new Square() }; for( int i = 0; i < shapes.size(); ++i ) { shapes[ i ]->draw(); delete shapes[ i ]; } }
|
Jest to łatwiejsze w użyciu, gdyż polimorfizm nie wymaga klas w hierarchii dziedziczenia aby wywoływać odpowiednie metody. Operujemy na tylko na referencjach, więc nie trzeba "specjalnie" tworzyć obiektu, a metody są wirtualne automatycznie (nie trzeba dodawać specjalnych słówek kluczowych):
class Shape: def draw(self): pass
class Circle(Shape): def draw(self): print("Drawing a circle")
class Square(Shape): def draw(self): print("Drawing a square")
shapes = [Circle(), Square()]
for shape in shapes: shape.draw() W powyższym przykładzie klasa Shape nie jest abstrakcyjna (można utworzyć jej instancje), aby jednak ją zrobić abstrakcyjną należy użyć modułu abc , wtedy klasa bazowa wyglądała by w następujący sposób: from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod def draw(self): pass |
14. Metody specjalne
Dzięki nim nasza klasa może się zachowywać jak typy wbudowane lub konwencjonalnie w danym języku programowania sposób. Poniżej przykładowe porównanie:
metoda |
C++ |
python3 |
wprowadzenie |
C++ umożliwia przeciążenie większości operatorów, dzięki którym zachowanie naszego typu może być podobne do typów wbudowanych. Poza tym możemy definiować operatory konwersji naszego typu do dowolnego innego typu. Przeciążanie operatorów jest definiowaniem zachowania operatora lub przesłanianiem zachowania wygenerowanego przez kompilator. Operatory można przeciążyć jako metoda klasy (wtedy pierwszym argumentem jest obiekt tej klasy this , albo jako funkcja globalna poza klasą, ale funkcje nie mają dostępu do składowych niepublicznych (za wyjątkiem funkcji zaprzyjaźnionych). Poniżej metody specjalne dla klasy class MyClass { std::string text_; public: }; W C++ aby zgłębić temat przeciążania operatorów trzeba poświęcić trochę czasu, dlatego zachęcam aby zapoznać się z tym zagadnieniem w dobrej książce, chociaż nie jest to zagadnienie, które trzeba koniecznie znać korzystając z C++, gdyż przeciążanie operatorów nie jest konieczne - to samo zachowanie (często czytelniej) osiąga się przy pomocy metod klasy. |
W języku python również można ustawiać domyślne zachowanie dla naszego typu w momencie wołania operatorów. Do tego celu są specjalne metody zaczynające i kończące się od dwóch podkreśleń. Poniżej metody specjalne dla klasy |
konstruktory (metody wołane w momencie tworzenia obiektu) |
Można zdefiniować dowolną liczbę konstruktorów, byleby różniły się argumentami np.:
MyClass() : text_( "(empty1)" ) { } MyClass( const std::string & text ) : text_( text ) { } }; Powyższy kod można zamknąć w jednym konstruktorze z wartościami domyślnymi: MyClass( const std::string & text = "empty" ) : text_( text ) { } Możemy też zdefiniować własny konstruktor kopiujący lub przenoszący, który wykona kopię tak jak chcemy (przydatne gdy mamy uchwyty do zasobów, jeśli nie mamy żadnych zasobów to zakładamy, że domyślnie wygenerowany konstruktor kopiujący załatwi sprawę kopiując składowe jedna za drugą):
MyClass( const MyClass & myClass ) : text_( myClass.text_ ) { } MyClass( MyClass && myClass ) : text_( std::move( myClass.text_ ) ) { } }; Kompilator generuje pewne konstruktory, chyba, że działania programisty wyłączą tę opcje, przykładowo konstruktor bezargumentowy jest generowany, o ile nie utworzymy dowolnego innego konstruktora. Zachęcam aby się zapoznać z tymi zasadami. |
Można zdefiniować jeden konstruktor. Konstruktora kopiującego się nie definiuje (python sam zarządza pamięcią)
class MyClass: def __init__(self, text=''): self.text = text Gdybyśmy potrzebowali zrobić głęboką kopię (czyli nie tylko referencje do obiektu, a cały obiekt) można użyć funkcji deepcopy z modułu copy . |
operatory przypisania - wywolywane gdy obiekt docelowy juz istnieje i chcemy mu przypisac nowa wartosc |
W C++ występuje semantyka przenoszenia (możemy zamiast kopiować przenieść zawartość): MyClass & operator =( const MyClass & myClass ) { if( & myClass != this ) text_ = myClass.text_; return * this; }
MyClass & operator =( MyClass && myClass ) { if( & myClass != this ) { text_ = std::move( myClass.text_ ); } return * this; } |
W języku python kopiowane są tylko referencje do obiektów, jeśli chcemy go skopiować dogłębnie możemy użyć deepcopy z modułu import copy , przykładowo:
from copy import deepcopy class MyClass: def __init__(self, text=''): self.text = text
my_class1 = MyClass("text1") my_class2 = MyClass("text2")
print(my_class1.text, id(my_class1)) print(my_class2.text, id(my_class2))
my_class1 = my_class2 my_class2.text += 'a' print(my_class1.text, id(my_class1)) print(my_class2.text, id(my_class2))
my_class1 = deepcopy(my_class2) my_class2.text += 'b' print(my_class1.text, id(my_class1)) print(my_class2.text, id(my_class2))
|
destruktor (metoda wołana przed usunięciem obiektu) |
W C++ jest to bardzo ważne zagadnienie. Kompilator dla każdego obiektu generuje destruktor, który jednak możemy zdefiniować. Jeśli nasz obiekt korzysta z zasobów (np. pamięci dynamicznej) to zapewne będziemy potrzebowali sami zdefiniować destruktor aby zwolnić pamięć. Przykład destruktora:
#include <iostream>
class MyClass { std::string text_; public: MyClass() { std::cout << " + MyClass()\n"; } ~MyClass() { std::cout << " - MyClass()\n"; } };
int main() { std::cout << "Przed utworzeniem klasy\n"; { MyClass myClass; } std::cout << "Po usunieciu klasy\n"; }
Wydruk z programu:
Przed utworzeniem klasy + MyClass() - MyClass() Po usunieciu klasy |
W języku Python nie praktykuje się czegoś takiego, ze względu na automatyczne zarządzanie pamięcią z poziomu samego języka. Jednakże możemy zdefiniować metodę __del__ , która będzie zawołana przed usunięciem obiektu (ale nie wiadomo kiedy - to zależy od odśmieczacza):
class MyClass: def __init__(self): print(" + MyClass()")
def __del__(self): print(" - MyClass()")
print("Przed utworzeniem klasy") my_class = MyClass() del my_class print("Po usunieciu klasy")
Aby mieć pewność, że dana metoda została zawołana przed wyjściem z programu stosuje się jeszcze atexit.register - wtedy dana funkcja/metoda będzie zawołana w momencie wyłączania interpretera pythona: import atexit import os
class MyClass: def __init__(self): print(" + MyClass()") atexit.register(self.cleanup)
def __del__(self): print(" - MyClass()")
def cleanup(self): print("Running cleanup...")
print("Przed utworzeniem klasy") my_class = MyClass() del my_class print("Po usunieciu klasy")
Wyjście programu nas zaskoczy: Przed utworzeniem klasy + MyClass() Po usunieciu klasy Running cleanup... - MyClass() |
porównywanie obiektów |
W C++ można przy pomocy operatorów sprawdzać czy dwa obiekty mają tę samą zawartość (nie muszą to być obiekty tego samego typu):
bool operator ==( const MyClass & myClass ) const { return text_ == myClass.text_; } bool operator !=( const MyClass & myClass ) const { return text_ != myClass.text_; } bool operator <( const MyClass & myClass ) const { return text_ < myClass.text_; } bool operator >( const MyClass & myClass ) const; bool operator >=( const MyClass & myClass ) const; bool operator <=( const MyClass & myClass ) const; W C++20 został wprowadzony nowy sposób porównywania obiektów, który wszystkie te operatory generuje: auto operator <=>( const MyClass & myClass ) const { return text_ <=> myClass.text_; } |
W języku python definiujemy specjalne metody, wtedy możemy dwa obiekty porównywać jak typy arytmetyczne, dla naszej klasy będą to:
def __eq__(self, other): return self.text == other.text
def __ne__(self, other): return self.text != other.text
def __lt__(self, other): return self.text < other.text
def __le__(self, other): return self.text <= other.text
def __gt__(self, other): return self.text > other.text
def __ge__(self, other): return self.text >= other.text |
konwersja na typ logiczny (celem sprawdzenia warunku) |
W C++ definiuje się metodę będącą operatorem konwersji:operator bool() const { return !text_.empty(); } |
def __bool__(self): return len(self.text) > 0 |
konwersja obiektu na tekst |
W C++ jest to rzadko praktykowane, gdyż wyświetlanie obiektu robi się inaczej niż poprzez konwersje na obiekt std::string . Można to zrobić definiując operator konwersji na typ std::string w następujący sposób:
operator std::string() const { return "MyClass(" + text_ + ")"; } Aby potem skorzystać z tej konwersji wystarczy przypisać do zmiennej typu tekstowego:
MyClass myClass( "Musicie od siebie wymagac nawet jakby inni od was nie wymagali!" ); std::string text = myClass; std::cout << text << std::endl; Warto aby konwersja nie odbywała się w sposób niejawny, dlatego można skorzystać ze słówka kluczowego explicit : explicit operator std::string() const { return "MyClass(" + text_ + ")"; } dzięki temu, aby skorzystać z konwersje należy zrobić jawne rzutowanie np.: MyClass myClass( "Musicie od siebie wymagac nawet jakby inni od was nie wymagali!" ); std::string text = static_cast < std::string >( myClass ); |
W Python3 definiuje się metodę __str__ dla naszej klasy można ją zdefiniować następująco:
def __str__(self): return "MyClass(" + self.text + ")" Warto podkreślić, że funkcja print() woła pod spodem metodę __str__ , co widać w poniższym wywołaniu:
my_class = MyClass("text1") text = str(my_class) print(text) print(my_class) |
wyświetlanie na ekran |
W C++ jest to dość rzadko stososowane, ale jeśli chcemy wyświetlić nasz typ na ekran to konwencjonalnie robi się poprzez zdefiniowanie operatora strumienia dla naszego typu (jako funkcja globalna). Często funkcja ta musi mieć dostęp do składowych prywatnych, dlatego się ją deklaruje jako przyjaciela. Przykładowo dla naszej klasy może to wyglądać tak:
friend std::ostream & operator <<( std::ostream & outputStream, const MyClass & myClass ) { return outputStream << "MyClass(" << myClass.text_ << ")"; }
Następnie możemy z tego skorzystać w następujący sposób:
MyClass myClass( "Musicie od siebie wymagac nawet jakby inni od was nie wymagali!" ); std::cout << myClass << std::endl; |
W języku python do wyświetlania stosuje się funkcję print() , która pod spodem woła metodę __str__ , przykład w powyższym wierszu. W aspekcie wyświetlania na ekran należy uwzględnić, że język python można wykonywać w trybie interaktywnym, aby w nim wyświetlić dany obiekt wystarczy jego nazwę napisać w linii np.
>>> myclass = MyClass("Jest tylko jedno dobro: wiedza, i jedno zło: ignorancja") >>> myclass <MyClass(Jest tylko jedno dobro: wiedza, i jedno zło: ignorancja)>
Aby jednak wydruk był jak powyżej należy zdefiniować w naszej klasie metodę:
def __repr__(self): return "<MyClass(" + self.text + ")>" Jeśli przy pomocy funkcji print() wypisujemy całą tablicę naraz to również jest wołana metoda __repr__ : my_classes = [MyClass("text1"), MyClass("text2"), MyClass("text3")] print(my_classes) |
iterowanie po obiekcie za pomocą pętli: gdy obiekt jest agregatem (bardziej zaawansowane) |
W C++ trzeba zdefiniować metody begin() i end() , które będą zwracać obiekt iteratora. Który ma kilka metod koniecznych dla tego typu obiektu, co jest dość skomplikowane, na szczęście nasza klasa zawiera typ, który już ma iterator, dlatego metody te mogą wyglądać tak:
auto begin() { return text_.begin(); } auto end() { return text_.end(); } Wywołanie z kolei:
MyClass myClass( "Przyszlosc zaczyna sie dzisiaj, nie jutro" ); for( auto c: myClass ) std::cout << '_' << c;
std::cout << '\n'; |
W Python3 również należy zdefiniować namiastkę iteratora przez zdefiniowanie metod __iter__ i __next__ . Druga z nich po zakończeniu iterowania musi wyrzucić wyjątek StopIteration , przykładowa implementacja dla naszej klasy:
def __iter__(self): self.index = 0 return self
def __next__(self): if self.index < len(self.text): result = self.text[self.index] self.index += 1 return result else: raise StopIteration wywołanie:
my_class = MyClass("Przyszlosc zaczyna sie dzisiaj, nie jutro")
for c in my_class: print('_' + c, end='')
print() |
dodawanie obiektów |
MyClass operator +( const MyClass & myClass ) const { return MyClass( text_ + myClass.text_ ); } |
def __add__(self, other): if isinstance(other, MyClass): return MyClass(self.text + other.text) else: raise TypeError(f"Unsupported operand type for +: MyClass and {type(other)}" |
dostęp do elementów kontenera po indeksie |
const auto & operator[ ]( int index ) const { if( index < 0 || index >= text_.size() ) throw std::out_of_range( "Index out of range!" ); return text_[ index ]; } auto & operator[ ]( int index ) { if( index < 0 || index >= text_.size() ) throw std::out_of_range( "Index out of range!" ); return text_[ index ]; } |
def __getitem__(self, index): return self.text[index]
def __setitem__(self, key, value): self.text = self.text[0:key] + value + self.text[key+1:] |
nasz obiekt obiektem funkcyjnym |
auto operator()( const std::string & surroundingText ) const { return surroundingText + text_ + surroundingText; } |
def __call__(self, surrounding_text): return surrounding_text + self.text + surrounding_text |
Poza powyższymi jest wiele metod specjalnych, które się tworzy analogicznie (np. odejmowanie obiektów od siebie). Poza tym jest wiele metod specjalnych w obydwu językach, które nie mają swoich odpowiedników w metodach specjalnych drugiego języka (oczywiście można je zaimplementować dowolną metodą).
Zarzuty względem poszczególnych języków (z punktu widzenia drugiego porównywanego języka)
Podsumowanie
W opracowaniu skupiłem się na zestawieniu składni i mechaniźmie działania porównywanych języków, aby znawca jednego języka mógł użyć drugiego języka. Powyższe porównanie dwóch języków nie jest porównaniem pełnym. Wśród pominiętych aspektów są m.in. wyjątki, wyrażenia lambda, operacje wejścia/wyjścia na plikach, szablony, kuroutyny/generatory, obsługa argumentów uruchomienia programu, wczytywanie z klawiatury, obsluga stałych, typy wyliczeniowe, wielowątkowość.
Każdy język istnieje po coś i w tym przoduje, wygoda języka Python wydaje się być większa, ale w pewnych zastosowaniach bardziej cenimy szybkość, większą kontrolę nad programem i wykrywanie błędów na etapie kompilacji. Nie można powiedzieć, że któryś z języków jest w każdym aspekcie lepszy od drugiego.
Autorzy artykułu