Ha geometriai feladatokkal van dolgunk, vagy grafikus felhasználói felületet készítünk, szükségünk lehet egy geometriai pontot modellező osztályra, illetve annak példányaira. A kérdés, hogy ezt hogyan valósítsuk meg?
Ha csak így tesszük fel a kérdést, akkor a válasz persze az, hogy „attól függ”. Ahhoz, hogy érdemben lehessen válaszolni, tudni kell, hogy mit várunk el egy pont példánytól. E követelmények legyenek a következők:
– Legyen a pont példány megváltoztathatatlan objektum, vagyis a létrehozás után az x és y koordináták értékét ne lehessen módosítani.
– Lehessen az x és y koordináták érvényes típusát típusutalással, típusannotációval jelezni.
– Lehessen egyenlőségvizsgálatot végezni két pont példány között. Akkor legyenek egyenlőek, ha mind az x, mind az y koordináták egyenlők az összehasonlított példányokban, és a példányok ugyanazon típusúak.
– Lehessen a pont példány halmaznak eleme, szótárnak kulcsa, más szóval legyen hashelhető.
– Legyen iterálható. Az iteráció eredményeképpen először az x, majd az y értékét adja ki.
– Lehessen egyéni igényeknek megfelelően definiált metódusokat meghívni a példányon. (pl. az x vagy y tengelyre vett tükörképpont előállítása, az origó körül adott szöggel elforgatott pont előállítása)
Több megvalósítási lehetőség közül választhatunk, amelyeket akkor is érdemes megvizsgálni, ha a fenti követelménylista nem mindegyik tételét lehet velük teljesíteni, mert a lista elég szigorú elvárásokat támaszt, amiből a gyakorlatban lehet némi engedményeket tenni.
A következőkben vizsgált megvalósítási opciók ezek: normál osztálydefiníció, továbbá dataclass, namedtuple vagy NamedTuple alapú osztálydefiníciók.
Mindegyik esetben két, eltérő nevű osztályt definiálunk az egyenlőségi relációra tett kitétel miatt, hogy annak teljesülését is tudjuk vizsgálni.
Normál osztálydefinícióval való megvalósítást láthatjuk alább.
|
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
from __future__ import annotations from math import cos, sin, pi class Point: def __init__(self, x: int | float, y: int | float): self._x = x self._y = y def __repr__(self): return f'{type(self).__name__}{self.x, self.y}' @property def x(self): return self._x @property def y(self): return self._y def __eq__(self, point: Point): if not isinstance(point, Point): return False return (self.x, self.y) == (point.x, point.y) def __hash__(self): return hash((self.x, self.y)) def __iter__(self): yield from (self.x, self.y) def reflect_across_axis_x(self) -> Point: """Az x tengelyre történő tükrözés után előálló új Point példánnyal tér vissza.""" return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point: """Az y tengelyre történő tükrözés után előálló új Point példánnyal tér vissza.""" return type(self)(-self.x, self.y) def rotate(self, angle: int | float) -> Point: """Az angle szöggel történő forgatás után előálló új Point példánnyal tér vissza.""" return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) class Point2D: def __init__(self, x: int | float, y: int | float): self._x = x self._y = y def __repr__(self): return f'{type(self).__name__}{self.x, self.y}' @property def x(self): return self._x @property def y(self): return self._y def __eq__(self, point: Point2D): if not isinstance(point, Point2D): return False return (self.x, self.y) == (point.x, point.y) def __hash__(self): return hash((self.x, self.y)) def __iter__(self): yield from (self.x, self.y) def reflect_across_axis_x(self) -> Point2D: """Az x tengelyre történő tükrözés után előálló új Point példánnyal tér vissza.""" return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point2D: """Az y tengelyre történő tükrözés után előálló új Point példánnyal tér vissza.""" return type(self)(-self.x, self.y) def rotate(self, angle: int | float) -> Point2D: """Az angle szöggel történő forgatás után előálló új Point példánnyal tér vissza.""" return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) # TESZT p1, p2, p3 = Point(3, 4), Point(3, 4), Point(5.5, 6.6) print(p1 == p2, p1 is p2) # True False print(p1 == p3) # False print({p1, p2, p3}) # {Point(3, 4), Point(5.5, 6.6)} print({p1: 'p1', p3: 'p3'}) # {Point(3, 4): 'p1', Point(5.5, 6.6): 'p3'} x, y = p1 print(x, y) # 3 4 print(p1.reflect_across_axis_x()) # Point(3, -4) print(p1.reflect_across_axis_y()) # Point(-3, 4) print(Point(1, 1).rotate(pi / 4)) # Point(0.0, 1.4142135623730951) # Két azonos struktúrájú, de különböző osztály példányának összehasonlítása: p, p2d = Point(7, 8), Point2D(7, 8) print(p == p2d) # False |
A tesztsorok mellett feltüntetett eredményekből látható, hogy az összes követelményt tudjuk teljesíteni, viszont elég sok munka árán. A tulajdonságokat a megváltoztathatatlanság miatt kell definiálni. Az egyenlőségvizsgálathoz és a hashelhetőséghez az __eq__ és __hash__ metódusok kellenek. Az __iter__ pedig az iterálhatósághoz.
Adatosztállyal történő megvalósítás így néz ki:
|
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
from __future__ import annotations from dataclasses import dataclass from math import cos, sin, pi @dataclass(frozen=True) class Point: x: int | float y: int | float def __iter__(self): yield from (self.x, self.y) def reflect_across_axis_x(self) -> Point: return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point: return type(self)(-self.x, self.y) def rotate(self, angle) -> Point: return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) @dataclass(frozen=True) class Point2D: x: int | float y: int | float def __iter__(self): yield from (self.x, self.y) def reflect_across_axis_x(self) -> Point2D: return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point2D: return type(self)(-self.x, self.y) def rotate(self, angle) -> Point2D: return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) # TESZT p1, p2, p3 = Point(3, 4), Point(3, 4), Point(5.5, 6.6) print(p1 == p2, p1 is p2) # True False print(p1 == p3) # False print({p1, p2, p3}) # {Point(3, 4), Point(5.5, 6.6)} print({p1: 'p1', p3: 'p3'}) # {Point(3, 4): 'p1', Point(5.5, 6.6): 'p3'} x, y = p1 print(x, y) # 3 4 print(p1.reflect_across_axis_x()) # Point(3, -4) print(p1.reflect_across_axis_y()) # Point(-3, 4) print(Point(1, 1).rotate(pi / 4)) # Point(0.0, 1.4142135623730951) # Két azonos struktúrájú, de különböző osztály példányának összehasonlítása: p, p2d = Point(7, 8), Point2D(7, 8) print(p == p2d) # False |
Itt a követelmények majdnem mindegyike automatikusan teljesül, kivéve az iterálhatóságot. Ezért kell definiálni az __iter__ metódust.
A következő kódsorokban láthatók a NamedTuple alapú osztályok, amelyek szerkezete egyszerű és példányai alapból iterálhatók.
|
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
from __future__ import annotations from typing import NamedTuple from math import cos, sin, pi class Point(NamedTuple): x: int | float y: int | float def reflect_across_axis_x(self) -> Point: return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point: return type(self)(-self.x, self.y) def rotate(self, angle) -> Point: return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) class Point2D(NamedTuple): x: int | float y: int | float def reflect_across_axis_x(self) -> Point2D: return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point2D: return type(self)(-self.x, self.y) def rotate(self, angle) -> Point2D: return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) # TESZT p1, p2, p3 = Point(3, 4), Point(3, 4), Point(5.5, 6.6) print(p1 == p2, p1 is p2) # True False print(p1 == p3) # False print({p1, p2, p3}) # {Point(3, 4), Point(5.5, 6.6)} print({p1: 'p1', p3: 'p3'}) # {Point(3, 4): 'p1', Point(5.5, 6.6): 'p3'} x, y = p1 print(x, y) # 3 4 print(p1.reflect_across_axis_x()) # Point(3, -4) print(p1.reflect_across_axis_y()) # Point(-3, 4) print(Point(1, 1).rotate(pi / 4)) # Point(0.0, 1.4142135623730951) # Két azonos struktúrájú, de különböző osztály példányának összehasonlítása: p, p2d = Point(7, 8), Point2D(7, 8) print(p == p2d) # True # Tuple konténerrel való összehasonlítás. print(p == (7, 8)) # True |
Láthatjuk, hogy az egyenlőségvizsgálatra előírt kitétel – vagyis, hogy azonos koordináták esetén az egyenlőségvizsgálat csak akkor adjon True értéket, ha azonos osztály példányait hasonlítjuk össze – nem teljesül. Nem csak azonos szerkezetű más osztály példányával kapunk egyenlőséget egyező x, y koordináták esetén, hanem egy tuple konténerrel való összehasonlításkor is.
Az előbbiek igazak a namedtuple-t használó, alább látható megoldás esetében is, de itt nem teljesül a koordináták érvényes típusának típusutalással történő jelezhetőségére vonatkozó követelmény sem.
|
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 40 41 42 43 44 45 46 |
from __future__ import annotations from collections import namedtuple from math import cos, sin, pi Point = namedtuple('Point', 'x y') Point.reflect_across_axis_x = lambda self: type(self)(self.x, -self.y) Point.reflect_across_axis_y = lambda self: type(self)(-self.x, self.y) Point.rotate = lambda self, angle: type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) class Point2D(namedtuple('NTPoint', 'x y')): def reflect_across_axis_x(self) -> Point2D: return type(self)(self.x, -self.y) def reflect_across_axis_y(self) -> Point2D: return type(self)(-self.x, self.y) def rotate(self, angle) -> Point2D: return type(self)(self.x * cos(angle) - self.y * sin(angle), self.x * cos(angle) + self.y * sin(angle)) # TESZT p1, p2, p3 = Point(3, 4), Point(3, 4), Point(5.5, 6.6) print(p1 == p2, p1 is p2) # True False print(p1 == p3) # False print({p1, p2, p3}) # {Point(3, 4), Point(5.5, 6.6)} print({p1: 'p1', p3: 'p3'}) # {Point(3, 4): 'p1', Point(5.5, 6.6): 'p3'} x, y = p1 print(x, y) # 3 4 print(p1.reflect_across_axis_x()) # Point(3, -4) print(p1.reflect_across_axis_y()) # Point(-3, 4) print(Point(1, 1).rotate(pi / 4)) # Point(0.0, 1.4142135623730951) # Két azonos struktúrájú, de különböző osztály példányának összehasonlítása: p, p2d = Point(7, 8), Point2D(7, 8) print(p == p2d) # True # Tuple konténerrel való összehasonlítás. print(p == (7, 8)) # True |
Ami az egyéni metódusokat illeti, ezeket a négy megvalósítási változat mindegyikében tudjuk definiálni, ezért ebben a tekintetben nem lesz különbség. Legfeljebb a kivitelezés lehet egy kicsit furcsa, a megszokottól eltérő a namedtuple esetén.
Nos, melyik megvalósítást válasszuk?
Bár a normál osztálydefiníció minden igényt teljesít, de sok munkával jár, ezért érdemes a többi, gyorsabb kivitelezést lehetővé tevő alternatív megoldást elemezni.
Amennyiben a specifikáció azon részéhez ragaszkodunk, hogy azonos koordináták esetén az egyenlőségvizsgálat csak akkor adjon True értéket, ha ugyanazon osztály példányait hasonlítjuk össze, akkor az adatosztállyal történő megvalósítás a megfelelő választás.
Ha az egyenlőségvizsgálatra vonatkozó fenti kitétel nincs, viszont az x és y koordináták érvényes típusának típusutalással történő jelezhetősége követelmény, akkor a NamedTuple alapú megoldás is jó választás.
Ha a típusannotálás lehetősége sem fontos, mert egy pont esetén közismert, hogy a koordináták értéke csak valós szám lehet, akkor a namedtuple alapú osztálydefiniálás lesz a megfelelő az egyszerűsége okán, különösen, ha egyéni metódusokat sem kívánunk definiálni.
E bejegyzés témája viszonylag széleskörű ismeretet igényel. A Python tudásépítés lépésről lépésre című e-könyvben mindezekről a következő fejezetekben találjuk a szükséges tudnivalókat: „Mágikus metódusok egyedi igényre szabott osztályokban”, „Attribútumműveletek kontrollált végrehajtása”, „Különleges osztálydefiníciók”, valamint a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezeten belül a „Speciális konténer típusok” és a „Típusutalások és statikus típusellenőrzés támogatása” alfejezetek.