W tym artykule wyprowadzamy od podstaw istotę programowania w paradygmacie obiektowym, a zagadnienia omawiamy konstruktywnie. Paradygmat object oriented to szeroki zakres pojęć i wiedzy, tutaj zaczynamy podstawy: dlaczego, skąd, jak i po co. Przykłady kodu obejmują języki Python, C, C++.
Model
Mamy punkt. Co o nim możemy powiedzieć? Hmmm... chyba nic. Nie znamy żadnej jego cechy, być może w ogóle nie istnieje. "Punkt". Umieśćmy go "gdzieś", w jakimś układzie współrzędnych, np. na kartce papieru w kratkę. Jeśli punkt jest gdzieś, to teraz ma jakieś cechy.
x = 1
y = 1
W komputerze to liczby, zatem w języku C napisalibyśmy:
int x = 1;
int y = 1;
Oto punkt. Na "kartce papieru" wygląda tak:
Ten punkt nie ma nazwy. Dla dalszych rozważań nazwijmy go E (Earth). Chcielibyśmy teraz mieć kolejny punkt S (Sun).
Ex = 1
Ey = 1
Sx = 0
Sy = 0
Mamy już 4 zmienne, a tylko 2 punkty. Co jeśli zechcielibyśmy mieć jeszcze kilka innych punktów: Mercury, Venus, Mars, Jupiter, Saturn, Uran, Neptune, Pluto?
Jeśli będziemy po prostu dokładać kolejne zmienne, to szybko się pogubimy. Moglibyśmy wykorzystać jakiś inny typ danych - np. listę w Python, tablicę w C.
E = [1, 1]
S = [0, 0]
...
W języku C:
int E[] = {1, 1};
int S[] = {0, 0};
// ...
Do współrzędnych tych punktów odnosilibyśmy się przez indeksy: E[0], E[1], S[0], S[1]. Ale to daje nam możliwość wyrażenia jedynie współrzędnych tych punktów. Chcielibyśmy jeszcze zapisać ich kolory, czy inne cechy (np. masę /punkt materialny/). Jeśli zapiszemy przykładowo:
E = [1, 1, "blue", (5.972 * 10 ** 24)]
S = [0, 0, "orange", (1.989 * 10 ** 30)]
...
... to będziemy musieli pamiętać, że pierwsze dwie cechy to współrzędne, trzecia z nich to kolor ("napis"), a czwarta to masa. W dodatku dane te są różnych typów.
O ile w Python jest to możliwe do zapisania w ten sposób, to już w języku C elementy tablicy muszą mieć taki sam typ. Jeśli tablica jest typu int, to elementy mogą być tylko typu int.
int E[] = {1, 1, /* ??? */, /* double? */};
int S[] = {0, 0, /* ??? */, /* double? */};
Oczywiście - te współrzędne (coordinates) będą innego typu jeśli zechcemy umieścić punkt np. w x = 0.005
. Typ będzie zależał od skali/jednostek w jakich operujemy. Jeśli punktów mielibyśmy wiele, to zmianę typu współrzędnych musielibyśmy wykonać w wielu miejscach. Brzmi to jak mozolne "kopiuj-wklej". Możemy zatem poszukać jakiegoś bardziej wyrafinowanego mechanizmu zapisu danych punktu. W Python moglibyśmy użyć np. słowników:
E = {
"x": 1.0,
"y": 1.0,
"color": "blue",
"mass": 5.972 * 10 ** 24
}
S = {
"x": 0.0,
"y": 0.0,
"color": "orange",
"mass": 1.989 * 10 ** 30
}
...
Odnosilibyśmy się do tych cech składowych poprzez nazwy: E["x"]
, E["mass"]
, S["x"]
, S["mass"]
.
W języku C użylibyśmy w tym celu struktury:
struct Point
{
double x;
double y;
char* color;
long double mass;
};
struct Point E = {1.0, 1.0, "blue", (5.972 * pow(10, 24))};
struct Point S = {0.0, 0.0, "orange", (1.989 * pow(10, 30))};
Do składowych (members) tej struktury odnosilibyśmy się już poprzez: E.x
, E.mass
, czy dla punku S: S.x
, S.mass
.
Mamy więc już pewne złożone typy i obiekty. Tutaj w języku C typem jest "Point", a konkretnymi obiektami (instancjami) są punkty E
oraz S
.
W kodzie Python powyżej mamy obiekty E
i S
, które są typu dict
(słownik).
Typ pozwala nam ująć cechy/naturę bytu (ens, entity), o którym rozmawiamy. Konkretne egzemplarze utworzone z definicji takiego typu to właśnie poszczególne "obiekty".
Zachowania
Natura jest dynamiczna. Oprócz samych cech byty posiadają również zachowania. Coś robią, coś się z nimi dzieje pod wypływem różnych czynników. Takie punkty jak nasze E
i S
moglibyśmy chcieć umieścić w innej lokalizacji na kartce papieru, "gdzieś indziej". Zachowania to funkcje (robią coś), a operują na obiektach (robią coś z czymś). Obiekty to rzeczowniki, zachowania to czasowniki.
W Pythonie mamy obecnie nasz obiekt E, który jest słownikiem:
>>> E
{'x': 1.0, 'y': 1.0, 'color': 'blue', 'mass': 5.972e+24}
Natura chciałaby przesunąć taki punkt. Dodamy zachowanie natury - funkcję move()
:
def move(p, dx, dy):
p["x"] += dx
p["y"] += dy
Teraz możemy zmieniać położenie punktów:
>>> E
{'x': 1.0, 'y': 1.0, 'color': 'blue', 'mass': 5.972e+24}
>>> move(E, 7, 13)
>>> E
{'x': 8.0, 'y': 14.0, 'color': 'blue', 'mass': 5.972e+24}
>>> S
{'x': 0.0, 'y': 0.0, 'color': 'orange', 'mass': 1.9890000000000002e+30}
>>> move(S, 0.000001, 0.000002)
>>> S
{'x': 1e-06, 'y': 2e-06, 'color': 'orange', 'mass': 1.9890000000000002e+30}
To samo możemy zrobić w C:
void move(struct Point* p, double dx, double dy)
{
p->x += dx;
p->y += dy;
}
move(&E, 3.14, 6.28);
Tutaj do funkcji przekazujemy już jakiś złożony obiekt umieszczony gdzieś w pamięci komputera, zatem do funkcji możemy przekazać po prostu adres tego miejsca w pamięci. Dlatego pierwszym argumentem tej funkcji jest struct Point*
(wskaźnik na adres), a przy wywołaniu przekazujemy &E
(ta "kaczka" to właśnie adres /liczba/ tego obszaru pamięci). Bez tego zabiegu podczas wywołania funkcji cały obiekt E musiałby zostać skopiowany, a nie ma takiej potrzeby. W Pythonie dzieje się podobnie - interpreter Python "pod spodem" zna adres słownika i do funkcji przekazuje również jedynie ten adres (referencja). W Pythonie po prostu nie musimy się tym przejmować, ale mechanizm jest identyczny, interpreter Python to program w C.
Metody
W programie możemy mieć jednak obiekty różnych typów, które mają swoje zachowania move()
. Moglibyśmy np. przesuwać sam układ współrzędnych, który przechowuje swoje cechy pod innymi nazwami i najpewniej w innych typach danych. Ciężko byłoby tworzyć programy ze wspólną funkcją move()
, która miałaby działać z wszelkimi typami. Musielibyśmy w funkcji przyjmować różne argumenty, a następnie w wielu "ifach" wykonywać różne operacje. Nie możemy też zdefiniować funkcji o tej samej nazwie wielokrotnie. Każda nowa definicja zastąpi poprzednią:
>>> move
<function move at 0x7f60a881a160>
>>> def move(coord_system): ...
...
>>> move
<function move at 0x7f60a881a0d0>
Teraz move()
to inna funkcja, gdzie indziej w pamięci, a więc ma też inny adres.
Dążymy zatem do powiązania zachowania z konkretnym typem. Niech Point
ma swoje move()
, a inne typy niech mają swoje move()
. Jak to zapisać?
Funkcje to po prostu bloki w pamięci, w których umieszczone są instrukcje. Nazwy funkcji/zmiennych to tylko wygodne aliasy dla nas. Pod nazwą funkcji kryje się tak naprawdę adres tego bloku z instrukcjami (w Python: obiektu funkcji). Niemniej, ostatecznie są to więc wskaźniki. Funkcje zresztą nie muszą mieć żadnych nazw.
>>> lambda: ...
<function <lambda> at 0x7f60a881a160>
Ot, anonimowa funkcja. Możemy przypisać taką funkcję do wygodnej nazwy:
>>> move = lambda *args, **kwargs: print("Moving", args, kwargs)
>>> move
<function <lambda> at 0x7f60a881a160>
Spróbujmy zatem do naszego punktu E
wpisać tak funkcję:
>>> E["move"] = lambda obj, dx, dy: obj.update(x=obj["x"] + dx, y=obj["y"] + dy)
>>> E
{'x': 8.0, 'y': 14.0, 'color': 'blue', 'mass': 5.972e+24, 'move': <function <lambda> at 0x7f60a881a0d0>}
Mamy zatem w naszym obiekcie funkcję!
Możemy teraz wywołać tę funkcję wpisaną do E["move"]
i przekazać jej jako pierwszy argument właśnie to E
(ten słownik):
>>> E["move"](E, 19, 27)
>>> E
{'x': 27.0, 'y': 41.0, 'color': 'blue', 'mass': 5.972e+24, 'move': <function <lambda> at 0x7f60a881a0d0>}
Współrzędne x
i y
punktu E
zostały zaktualizowane. Jeśli jednak chcielibyśmy aby punkt S
również posiadał taką funkcję, to musielibyśmy osobno wykonać podobny zapis do słownika S
.
W języku C możemy postąpić podobnie umieszczając w strukturze Point
wskaźnik na funkcję zdefiniowaną poza tą strukturą. Przeanalizujmy cały przykład w C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Kluczową sprawą jest tutaj fakt, że funkcja move()
(linia 16) zdefiniowana jest poza strukturą Point
, a struktura umożliwia po prostu przypisanie adresu bloku pamięci (kodu funkcji, wskaźnik w linii 12). Istnieje tylko jedna kopia tej funkcji move()
, a obiekty E
oraz S
używają jej przekazując siebie samych (linie 31, 35) jako jej pierwszy argument. Nic nie stoi na przeszkodzie, by S.move
move działało na E
. Czyli:
S.move(&E, 0.333, 0.444);
printf("E x: %f, y: %f # S.move(&E...)\n", E.x, E.y);
/* this is the same function! */
move(&E, -0.332, -0.443);
printf("E x: %f, y: %f # move(&E...)\n", E.x, E.y);
Po uruchomieniu:
$ gcc solar.c
$ ./a.out
E x: 1.000000, y: 1.000000
E x: 4.140000, y: 7.280000
S x: 0.000000, y: 0.000000
S x: 0.007000, y: 0.000090
E x: 4.473000, y: 7.724000 # S.move(&E...)
E x: 4.141000, y: 7.281000 # move(&E...)
W Pythonie można to zaprezentować bardzo podobnie na trywialnym kodzie używając choćby typów wbudowanych. Najpierw wywołujemy s.replace()
na obiekcie s
typu str
, a drugim razem statycznie str.replace()
przekazując obiekt s
.
>>> s = "hello"
>>> s.replace("h", "H")
'Hello'
>>> str.replace(s, "l", "L")
'heLLo'
Różnicą pomiędzy funkcją a metodą jest jej związanie z konkretnym obiektem. Funkcje to funkcje, a metody to funkcje związane (bound method).
Metody związane z obiektem
Wiemy zatem, że funkcja i metoda to w zasadzie to samo. Generalizując - różnica jest syntaktyczna.
W strukturze w języku C nie da się wyrazić "metody" w inny sposób niż wskazaniem zewnętrznej funkcji.
1 2 3 4 5 6 7 8 9 10 11 |
|
Ten kod się nie zbuduje.
$ gcc solar.c
solar.c:11:14: error: field ‘can_i_do_this’ declared as a function
11 | void can_i_do_this(struct Point* self);
| ^~~~~~~~~~~~~
W Pythonie w samych słownikach możemy zaprezentować to podobnie. W trakcie deklaracji słownika (kiedy on sam jeszcze nie jest zdefiniowany) nie możemy się jeszcze odnieść do tegoż słownika. Dopiero konstruujemy obiekt, zatem jeszcze nie istnieje, jeszcze nie ma adresu.
>>> D = {"x": 123, "y": D["x"]}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'D' is not defined
Potrzebna jest zatem lepsza i wygodniejsza składnia. To do czego dążymy to zapis naszego typu jako cechy i zachowania. Docieramy zatem do klas.
Klasy
Język C nie zawiera odpowiednika syntaktycznego klas jakie zwykle widujemy. Istnieje jednak język C++, który powstał najpierw jako rozszerzenie C ("C z klasami"), a następnie znacznie ewoluował zachowując jednak kompatybilność z C. Trywializując - "pod spodem" C++ to nadal to samo "C z klasami", czyli struct
i wskaźniki na funkcje. Tutaj zostawiamy więc C, a zaczynamy C++.
Klasa i metoda w C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Kompiluję, uruchamiam, patrzymy:
$ c++ solar.cxx
$ ./a.out
object address (&E):0x7fff991cbcb0
E: x=1, y=1
object address (this): 0x7fff991cbcb0
E: x=2.4142, y=3.7182
Tutaj znika nam już dopisywanie wszędzie struct
(jak było w C), a metody możemy definiować w kontekście klasy. Kompilator wewnątrz metody (funkcji zdefiniowanej w klasie) automatycznie "pod spodem" przekazuje adres obiektu (wskaźnik this
), na rzecz którego ta funkcja zostanie wywołana.
Adres obiektu E (czyli "kaczka e": &E
) to to samo, co this
użyte wewnątrz metody. To ten sam obiekt, a konkretnie ten sam obszar pamięci. Stąd wypisany wskaźnik this
oraz adres &E
to to samo: adres pamięci (0x7fff991cbcb0
).
Wywołanie E.move(1.4142, 2.7182)
w linii 29 to to samo co Point::move(&E, x, y)
, jednak nie możemy użyć tego drugiego zapisu ze względu na liczbę argumentów, które funkcja przyjmuje. Funkcja przyjmuje 2 argumenty, a kompilator C++ sam niejawnie podstawia nam this
.
Definiując typ w C++ możemy napisać struct
albo class
. Jedno i drugie to praktycznie to samo, różnica polega na widoczności pól ("public", "private"). W struct
domyślna widoczność to public
, a w class
domyślnie jest private
. Wrócimy do tego później.
W Pythonie klasy i ich metody to dokładnie taki sam powyższy mechanizm.
class Point:
x: float = 0.0
y: float = 0.0
mass: float = 0.0
color: str = ""
def move(kuku, dx, dy):
print(kuku)
print("object id", id(kuku))
print("type", type(kuku))
kuku.x += dx
kuku.y += dy
E = Point()
E.x = 1.0
E.y = 1.0
E.mass = 5.972 * 10 ** 24
E.color = "blue"
print("id(E):", id(E))
E.move(1.4142, 2.21782)
print(E.__dict__)
Uruchamiamy:
$ python solar.py
id(E): 139646969761408
<__main__.Point object at 0x7f021806ce80>
object id 139646969761408
type <class '__main__.Point'>
{'x': 2.4142, 'y': 3.21782, 'mass': 5.972e+24, 'color': 'blue'}
Stworzony obiekt E
jest typu Point
(klasa Point
). Obiekt ten ma identyfikator 139646969761408
. To dokładnie ten sam obiekt, który Python podstawia automatycznie jako pierwszy argument metody Point.move()
(tak jak C++ podstawia this
). Nazwałem ten argument kuku
, aby pokazać, że "self" to po prostu konwencja. W C++ czy Java to będzie this
, w Smalltalk będzie self
, a w Pythonie to zwykła zmienna, którą według konwencji nazywamy self
. "Self" czyli właśnie ten obiekt. Sam obiekt (self
) jest zbudowany na słowniku, dlatego w ostatniej linii kodu wypisać możemy po prostu E.__dict__
- cały słownik tego obiektu. W Pythonie istnieje jeszcze słownik klasy, ale ten temat nie jest tutaj w tej chwili istotny.
Czy Python posiada odpowiedniki "public/private"? Tak i nie. W C++ do publicznych składowych możemy dostawać się z zewnątrz (poza metodami klasy), czyli:
E.x = 17; // this will work
Jeśli double x
byłoby zdefiniowane w sekcji prywatnej, to kompilator nie pozwoliłby nam na to, kod nie skompilowałby się.
No to jak z tym jest w Python?
class X:
_var_a: str = "A" # single underscore
__var_b: str = "B" # double underscore
obj = X()
print(obj._var_a) # works
print(obj.__var_b) # does not work
Uruchamiamy:
$ python private.py
A
Traceback (most recent call last):
File "/tmp/private.py", line 8, in <module>
print(obj.__var_b) # does not work
AttributeError: 'X' object has no attribute '__var_b'
Jeśli nazwa składowej (member) zdefiniowanej w klasie zaczyna się od podwójnej podłogi, to Python udaje, że to jest prywatne.
Jest to tak proste... Python sprawdza po prostu warunek if func_name.startswith("__")
.
Możemy się do niej jednak dostać. To co Python robi to zmiana nazwy. Jeśli użylibyśmy kodu:
...
print(obj._X__var_b)
...
to otrzymamy wartość "B". W nazwie występuje jeszcze nazwa samej klasy.
$ python private.py
A
B
Python to język dynamiczny, możemy kod programu a nawet same typy zmieniać nawet wtedy, kiedy program już uruchomiliśmy. Wszystko jest obiektem, a obiekt jest słownikiem. Jako przykład przeanalizujmy poniższy fragment z uruchomionego już interpretera Python:
>>> class Foo: ...
...
>>> Foo.say_hello = lambda self, name: print("hello", name)
>>> f = Foo()
>>> f.say_hello("popo")
hello popo
>>> Foo.say_hello = lambda self, name: print("Hello dear", name.capitalize())
>>> f.say_hello("popo")
Hello dear Popo
Tak, zmieniamy definicję typu w już uruchomionym programie. Tutaj dla odmiany użyliśmy self
, zamiast kuku
.
Można też zademonstrować statyczne wywołanie, czego nie mogliśmy osiągnać w C++:
>>> Foo.say_hello(f, "popo")
Hello dear Popo
Wywołujemy tutaj bezpośrednio funkcję Foo.say_hello
, a jawnie przekazujemy zarówno obiekt, na którym funkcja działa, jak i jej kolejne argumenty. Ten obiekt to właśnie self
(vide wcześniejsze kuku
). Widać też "różnicę" pomiędzy metodą a funkcją:
>>> Foo.say_hello
<function <lambda> at 0x7f60a891bdc0>
>>> f.say_hello
<bound method <lambda> of <__main__.Foo object at 0x7f60a89129d0>>
>>> f
<__main__.Foo object at 0x7f60a89129d0>
Foo.say_hello
to ta funkcja, a kiedy jest związana już z obiektem, to jest metodą (bound method). Adres obiektu się nie zmienia: 0x7f60a89129d0
.
Dla innego obiektu zachowanie będzie takie samo:
>>> Foo.say_hello
<function <lambda> at 0x7f60a891bdc0>
>>> g = Foo()
>>> g
<__main__.Foo object at 0x7f60a89125b0>
>>> g.say_hello
<bound method <lambda> of <__main__.Foo object at 0x7f60a89125b0>>
To nadal ta sama funkcja. Jednak... w Pythonie funkcje to też obiekty, konkretnie obiekty typu (klasy) function
:
>>> type(Foo.say_hello)
<class 'function'>
Interpreter Python kopiuje czasem (kiedy trzeba) takie obiekty funkcji. Dlatego tutaj widzimy różne identyfikatory:
>>> id(f.say_hello)
140053120709184
>>> id(g.say_hello)
140053120708928
To jednak zostawiamy na inną okazję.
Wnioski
Klasy są mechanizmem syntaktycznym na powiązanie składowych i funkcji z konkretnymi instancjami tej klasy (obiektami).
Składowe definiowane w klasach są zenkapsulowane w obrębie tego typu. "Nie walają" się luźno w globalnym zasięgu (global scope), co było naszym problemem na początku. Wtedy mieliśmy luźne zmienne Ex, Ey, Sx, Sy
, itd. Tutaj x
, y
i reszta zmiennych trafia do definicji typu, a konkretny obiekt "zawiera je już w sobie".
Konstruktory, destruktory
Przyjrzyjmy się klasie Python:
class Example:
def __new__(cls): # constructor
print("NEW")
return super().__new__(cls)
def __init__(self): # initializer
print("Initializer")
def __del__(self): # finalizer
print("Finalizer")
Example()
Uruchamiamy:
$ python ctor_dtor.py
NEW
Initializer
Finalizer
__new__
to konstruktor. Jego rolą jest stworzenie obiektu. Aby obiekt w ogóle istniał, to system musi przydzielić pamięć na ten obiekt (alokacja) i zwrócić adres tego bloku pamięci. Alokację (malloc()
) wywołuje Python "pod spodem", ale my moglibyśmy w tymcls
coś dopisać. Dokładnie tak, jak wcześniej zrobiliśmy to z "pustą" klasąFoo
dopisując jejsay_hello()
.__init__
to inicjalizator. Skoroself
jest słownikiem i możemy na nim działać, to już istnieje, a zatem musiał zostać wcześniej skonstruowany.__del__
to finalizator. Uruchamiany jest kiedy obiekt "znika", czyli nie ma już żadnego uchwytu do tego obiektu (żadna zmienna go nie "trzyma").
Operatory
Paradygmat OOP powstał w celu ułatwienia pisania poprawnych programów. Kiedy mamy już obiekty, to możemy na nich operować. Źródłem rozwoju OOP są symulacje komputerowe. To było zresztą główną przesłanką dla twórcy C++.
Na obiektach możemy operować metodami. Metody mogą realizować różne czynności, ale najważniejsze metody, to te "magiczne", np. w Python __add__()
, czy __mul__()
. Takie nasze Point
moglibyśmy sumować, albo skalować (mnożenie). Sprawdźmy to najpierw w interpreterze Python na typach wbudowanych.
>>> x = 0
>>> x + 1
1
>>> x
0
>>> x.__add__(1)
1
>>> x
0
>>> int.__add__(x, 1)
1
>>> x
0
>>> x = 10
>>> x * 2
20
>>> int.__mul__(x, 3)
30
A teraz zaimplementujmy swój Integer
.
class Integer:
def __init__(self, value):
self.value = int(value)
def __add__(self, other):
return self.value + (
other.value if isinstance(other, Integer) else other
)
def __mul__(self, other):
return self.value * (
other.value if isinstance(other, Integer) else other
)
x = Integer(2)
print(x + 3)
y = Integer(5)
print(x * y)
print(x - 1) # won't work, __sub__ operator not implemented
Uruchamiamy i analizujemy:
$ python integer.py
5
10
Traceback (most recent call last):
File "/tmp/integer.py", line 20, in <module>
print(x - 1) # won't work, __sub__ operator not implemented
TypeError: unsupported operand type(s) for -: 'Integer' and 'int'
Nasza klasa Integer w inicjalizatorze przypisuje sobie przekazaną wartość i upewnia się, że jest to liczba (konwertuje to do int
).
Klasa implementuje dodawanie i mnożenie. Przy operacjach arytmetycznych sprawdzamy czy drugim argumentem operacji jest obiekt naszego typu. Jeśli to obiekt naszego typu Integer
to wartość mamy w składowej value
, a z innymi typami operujemy bezpośrednio. Dla przejrzystości pozostawiłem ten kod bez obsługi błędów (np. mnożenie liczby razy napis...).
Te operatory możemy zauważyć pracując z biblioteką numpy. Przykład:
>>> import numpy as np
>>> A = np.array([1, 2, 3])
>>> A
array([1, 2, 3])
>>> A * 7
array([ 7, 14, 21])
Ot, cała rzecz. To nie "magia" lecz programowanie, a programowanie to w zasadzie matematyka.
W C++ również możemy definiować operatory (przeciążanie operatorów /operator overloading/). Nie będę przytaczał całych przykładów, spójrzmy jedynie na składnię:
MyType operator+ (double other, const MyType& self);
MyType operator+ (const MyType& self, double other);
A jeśli definiujemy je jako metody wewnątrz klasy, to potrzebny nam jest już tylko "other":
struct MyType
{
MyType operator* (double other);
};
Operatory to jedno z kluczowych zagadnień OOP. Czasami bywają zaimplementowane źle (lub nie wszystkie).
$ nodejs
Welcome to Node.js v12.22.7.
Type ".help" for more information.
> 2 * "11"
22
> "11" * 2
22
> 2 + "11"
'211'
> "11" + 2
'112'
> "11" - 2
9
Solar
Na tym kończymy dzisiejsze omówienie. Wiele spraw pozostało - m.in. dziedziczenie, polimorfizm, mro, super()
, a w C++ choćby vtable
... Zostawimy to na inną okazję, żeby nie "przemóżdżyć". Nie jest nam to też potrzebne do przećwiczenia podstaw OOP ("czym są klasy i metody?") ani do napisania naszego "solar". Mieliśmy dopiero E
i S
. Do dzieła! Jeszcze się nie ruszały!
Do animacji punktów na "kartce papieru" w Python możemy użyć biblioteki matplotlib.
Czas płynie w jedną stronę, wartość w pionie (oś y
, sznurek w łuku) to sinus, a w poziomie (oś x
, strzała w łuku) to cosinus.
Wikipedia posiada w swoich zasobach ten piękny artykuł: History of trigonometry. Niestety nie ma on polskiego tłumaczenia.
Video
Plot twist: nie mogąc się oprzeć "pokusie" stworzyłem animację 3D.
Kod dostępny jest w repozytorium git.
Video: MP4, 1.6M, 1920x1080. Duration: 00:00:47 Link