Обектно-ориентирано програмиране#
Основни принципи#
Енкапсулация
Абстракция
Наследяване
Полиморфизъм
Обектно-ориентираното програмиране се основава на използването на класове от обекти, които обменят съобщения помежду си. В Python използваме ключовата дума class
, за да започнем дефиницията на клас:
class ExampleClass:
pass
(ключовата дума pass
обозначава празен блок (еквивалентно на {}
в езиците, използващи скоби за scope))
Инстанции на класа можем да създаваме чрез синтаксис подобен на извикването на функция със същото име. Новосъздадените обекти отиват в динамичната памет и по подразбиране когато искаме да print
-нем информация за тях Python ни показва адреса в паметта, на който се намират, както и типа им (класа):
example_object = ExampleClass()
print(example_object)
example_object_2 = ExampleClass()
print(example_object_2)
<__main__.ExampleClass object at 0x113bed6a0>
<__main__.ExampleClass object at 0x112dbbc40>
is
сравнява дали два обекта съвпадат, т.е. ще върне False
, когато адресите им в паметта са различни. Операторът ==
пък не е дефиниран за горния клас и затова и неговата стойност засега ще бъде False
(по премълчаване се дефинира чрез is
):
print(f"{example_object is example_object_2 = }")
print(f"{example_object == example_object_2 = }")
example_object is example_object_2 = False
example_object == example_object_2 = False
Горният клас с име ExampleClass
така дефиниран е празен - не притежава нито член-данни, нито методи. Добре е да се знае, обаче, че поради динамичния характер на езика, такива могат да бъдат добавяни (и отнемани) по всяко време (както като част от инстанцията, така и като част от класа). Достъпът до атрибути и методи става чрез точка .
:
example_object.example_property = "I am an attribute of this instance only"
print(f"{example_object.example_property = }")
ExampleClass.example_shared_property = "I am a shared/static attribute"
print(f"{ExampleClass.example_shared_property = }")
print(f"{example_object.example_shared_property = }")
print(f"{example_object_2.example_shared_property = }")
del example_object.example_property
print(f"{example_object.example_property = }") # 💥
example_object.example_property = 'I am an attribute of this instance only'
ExampleClass.example_shared_property = 'I am a shared/static attribute'
example_object.example_shared_property = 'I am a shared/static attribute'
example_object_2.example_shared_property = 'I am a shared/static attribute'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/980122267.py in <module>
8
9 del example_object.example_property
---> 10 print(f"{example_object.example_property = }") # 💥
AttributeError: 'ExampleClass' object has no attribute 'example_property'
Обикновено обаче искаме да знаем винаги какви член-данни да очакваме от един клас, както и да можем да ги задаваме при конструирането му. Това е възможно, чрез дефинирането на __init__
метода в класа:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
Това е първият от редица “dunder” (double underscore - двойна подчертавка) методи (още наричани магически методи), които ще разгледаме в лекцията. Този се нарича инициализатор и приема като първи аргумент новосъздадения обект (по конвенция го кръщаваме винаги self
, но на теория може да е всякак). Следващите параметри дефинираме и означаваме по наш избор.
Обърнете внимание, че нарочно не го наричаме контруктор, понеже истинският конструктор създава и връща нов обект от даден клас, докато инициализаторът приема този обект и задава начални стойности на член-данните му или изпълнява някакъв друг вид инициализация. Такъв конструктор има и в Python (__new__
), но на него няма да се спираме подробно.
p1 = Point(2, 3)
p2 = Point(5j, 3.14)
print(f"{p1.x = }, {p1.y = }")
print(f"{p2.x = }, {p2.y = }")
p1.x = 2, p1.y = 3
p2.x = 5j, p2.y = 3.14
Инициализаторът си е функция като всяка други и може да има стойности на параметрите по подразбиране, както и args
и kwargs
:
class Coordinate:
def __init__(self, la=0, lo=0, **kwargs):
self.latitude = la
self.longitude = lo
self.metadata = kwargs
sofia = Coordinate(42.69, 23.420, city="Sofia", country="Bulgaria")
null_island = Coordinate()
print(f"{sofia.latitude = }, {sofia.longitude = }, {sofia.metadata = }")
print(f"{null_island.latitude = }, {null_island.longitude = }, {null_island.metadata = }")
sofia.latitude = 42.69, sofia.longitude = 23.42, sofia.metadata = {'city': 'Sofia', 'country': 'Bulgaria'}
null_island.latitude = 0, null_island.longitude = 0, null_island.metadata = {}
Произволни методи дефинираме по същия начин:
class Path:
def __init__(self, start, end):
self.start = start
self.end = end
def length(self):
return ((self.start.x - self.end.x) ** 2 + (self.start.y - self.end.y) ** 2) ** 0.5
p1 = Point(0, 3)
p2 = Point(4, 0)
path = Path(p1, p2)
l = path.length()
print(f"{l = }")
l = 5.0
Липсата на self
като първи аргумент ще е грешка, понеже Python винаги ще се опита да предаде инстанцията на класа като първи параметър на метода:
class A:
def a():
pass
A().a()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/3247279650.py in <module>
3 pass
4
----> 5 A().a()
TypeError: a() takes 0 positional arguments but 1 was given
Енкапсулация#
В Python достъпа до всичко е публичен.
По конвенция е общоприето protected
имената да започват с една подчертавка (_name
), а private
- с две (__name
).
class Counter:
def __init__(self, value=0):
self._value = value
def get_value(self):
return self._value
def increment(self):
self._value += 1
def decrement(self):
self._value -= 1
c = Counter(41)
c.increment()
print(f"{c.get_value() = }")
print(f"{c._value = }")
c.get_value() = 42
c._value = 42
При двойните подчертавки обаче съществува една особеност, която ни улеснява в енкапсулирането на private член-данни. Нарича се name mangling и представлява добавянето на _
и името на класа в началото на името на тези член-данни. Тоа се прави с цел замаскиране на атрибута и премахването на достъпа чрез оригиналното му име.
class Counter:
def __init__(self, value=0):
self.__value = value # private access from within the class is OK
def get_value(self):
return self.__value
def increment(self):
self.__value += 1
def decrement(self):
self.__value -= 1
c = Counter(41)
c.increment()
print(f"{c.get_value() = }")
print(f"{c._Counter__value = }") # name has been mangled
print(f"{c.__value = }") # 💥 cannot be accessed at its original name
c.get_value() = 42
c._Counter__value = 42
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/3226284858.py in <module>
18 print(f"{c.get_value() = }")
19 print(f"{c._Counter__value = }") # name has been mangled
---> 20 print(f"{c.__value = }") # 💥 cannot be accessed at its original name
AttributeError: 'Counter' object has no attribute '__value'
Дълга тема за дискусия е колко често и кога да използваме public, private или protected член-данни и методи в Python. За разлика от повечето езици, които проповядват всичко по подразбиране да е максимално скрито, докато не ни се наложи друго, в Python не е толкова често срещана необходимостта от строгото забраняване на достъп, даже напротив, подходът най-често е по-скоро обратен.
Хубави отговори на този въпрос може да намерите тук.
Наследяване#
Както споменахме, всичко в Python е обект. По-точно object
.
object
е най-базовия клас в езика, който имплицитно бива наследяван от всички класове.
class A:
pass
a = A()
print(f"{isinstance(a, A) = }")
print(f"{isinstance(a, object) = }")
isinstance(a, A) = True
isinstance(a, object) = True
(с вградената функция isinstance
проверяваме дали даден обект е инстанция на даден клас или на негов базов такъв)
Дори и да изглежда празен класът А, в него в момента се съдържат доста член-методи. С вградената функция dir
можем да видим пълния набор от атрибути и методи, които даден обект притежава:
dir(a)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__']
Почти всички от тези магически методи и атрибути са наследени от object
:
dir(object())
['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
Ако искаме да наследим друг клас, записваме името му в скоби преди дефиницията на наследника:
class BaseClass:
x = 69
class SubClass(BaseClass):
pass
sub = SubClass()
print(f"{sub.x = }")
sub.x = 69
Виждаме, че няма нужда да пишем експлицитно, че наследяваме от object
. Важно е да се отбележи обаче, че това е задължително за Python 2.
Предефинирането на методи от базовия клас не изисква някаква специална ключова дума. Тяхната дефиниция просто бива замествана от новата.
class Foo:
def foo(self):
print("foo")
class Bar(Foo):
def foo(self):
print("foobar")
# (I'm sorry I couldn't come up with anything creative this time)
bar = Bar()
bar.foo()
foobar
Използването на super()
ни връща обект-прокси, който пренасочва дефиниции на методи към базов клас:
class Drink:
def __init__(self, name, alcoholic_percentage):
self.name = name
self.alcoholic_percentage = alcoholic_percentage
class SoftDrink(Drink):
def __init__(self, name):
super().__init__(name, 0)
drincc = SoftDrink("Coca-Cola")
print(f"{drincc.name = }, {drincc.alcoholic_percentage = }%")
drincc.name = 'Coca-Cola', drincc.alcoholic_percentage = 0%
Python е един от малкото езици, в които множественото наследяване е позволено. За избягване на проблема на диаманта и други подобни, които често това поражда, е хубаво базовите класове да са семпли и с ясно-дефинирана функционалност, по възможност непокриваща се.
Mixin-ите са най-честия пример за използване на множествено наследяване в Python. Те са базови класове, които предоставят “наготово” имплементация на конкретна допълнителна функционалност.
import json
class JSONSerializableMixin:
def to_json(self):
return json.dumps(self.__dict__)
class DebugMixin:
def __repr__(self):
arguments = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
return f"{self.__class__.__name__}({arguments})"
class Person(JSONSerializableMixin, DebugMixin):
def __init__(self, name, age):
self.name = name
self.age = age
class Employee(Person, JSONSerializableMixin, DebugMixin):
def __init__(self, name, age, salary):
self.salary = salary
super().__init__(name, age)
elon = Employee("Elon Musk", 51, 1_000_000_000)
print(f"{elon.to_json() = }")
print(f"{elon = }")
elon.to_json() = '{"salary": 1000000000, "name": "Elon Musk", "age": 51}'
elon = Employee(salary=1000000000, name=Elon Musk, age=51)
В горните примери дефинирахме два mixin-a: един, който дефинира как да бъде сериализиран до JSON всеки обект, а другия - как да бъде принтиран в дебъг конзолата. Класът Employee
наслодява както Person
, така и двата други класа, като придобива всичката функционалност. В този случай super()
знае към кой базов клас да се обърне чрез поредността на изпълнение на методи (Method Resolution Order), който зависи от реда, в който сме изброили базовите класове (може да се види чрез __mro__
)
Employee.__mro__
(__main__.Employee,
__main__.Person,
__main__.JSONSerializableMixin,
__main__.DebugMixin,
object)
Повече за super()
може да прочетете тук.
В примерът по-горе използвахме доста непознати dunder-и: __repr__
, __dict__
, __class__
, __name__
. За тях, както и други, ще разясним в следващата секция.
Магическите методи ✨#
Документация: https://docs.python.org/3/reference/datamodel.html
__repr__
#
От англ. “representation”.
Изиква се при изпълнението на repr(self)
. Трябва да върне низ с репрезентация на обекта, подходящ за принт в конзолата при дебъгване например. Хубаво е тя максимално много да наподобява начина, по който можем да пресъздадем обекта с изпълним код. Ако това не е възможно е хубаво да върне низ във вида <... някакво полезно инфо...>
.
Например за клас Point
(точка) с атрибути x
и y
, които инициализаторът приема, вместо (x, y)
е по-удачно да върнем Point(x, y)
, понеже това може директно да бъде изпълнено като Python код и да получим еквивалентен обект.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(2, 3)
print(p)
print(repr(p))
Point(2, 3)
Point(2, 3)
Конвертиране в други типове#
Можем да предефинираме начинът, по който нашият клас бива конвертиран в някои други вградени типове, чрез специалните dunder
методи с тяхното име, предназначени за това. Възможните типове са str
, bytes
, bool
, int
, float
, complex
.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
def __bool__(self):
"""Return `True` only for (0, 0)."""
return not (not self.x and not self.y)
p = Point(2, 3)
print(f"{str(p) = }")
print(f"The `__str__` method is called when using string interpolation: {p}")
print(f"If we want to use the `__repr__` instead we should write it like this: {p!r}")
print(f"{bool(p) = }")
# `__bool__` is useful when building conditions
if p:
print("p is truthy")
z = Point()
if not z:
print("z is falsey")
str(p) = '(2, 3)'
The `__str__` method is called when using string interpolation: (2, 3)
If we want to use the `__repr__` instead we should write it like this: Point(2, 3)
bool(p) = True
p is truthy
z is falsey
Note: За доста от класовете, които бихте срещнали, върнатите низове от repr
и str
съвпадат. Тук обаче умишлено сме избрали пример, в който има повече смисъл да са различни - repr
е debuggable репрезентация на обекта точка (валиден изпълним код), а str
- стандартен математически запис на точка.
class Fraction:
def __init__(self, numerator=0, denominator=1):
self.numerator = numerator
self.denominator = denominator
def __float__(self):
return self.numerator / self.denominator
def __int__(self):
return self.numerator // self.denominator # alternative is `return int(float(self))`
def __complex__(self):
return complex(float(self)) # alternative is `return float(self) + 0j`
def __str__(self):
return f"{self.numerator}/{self.denominator}"
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
def __bool__(self):
return self.numerator != 0
frac = Fraction(6, 9)
print(f"{float(frac) = }")
print(f"{int(frac) = }")
print(f"{complex(frac) = }")
print(f"{str(frac) = }")
print(f"{repr(frac) = }")
print(f"{bool(frac) = }")
float(frac) = 0.6666666666666666
int(frac) = 0
complex(frac) = (0.6666666666666666+0j)
str(frac) = '6/9'
repr(frac) = 'Fraction(6, 9)'
bool(frac) = True
Note 2: Обърнете внимание, че dunder(x)
в случая е същото като x.__dunder__()
. Обикновено нямаме причина да използваме втория запис.
Аритметични и логически оператори#
В Python (също както в С++ и повечето други ООП езици) можем да предефинираме поведението на аритметичните и логическите оператори от езика, когато биват извикани върху обект от нашия клас.
Съответните магически методи за това са:
оператор |
|
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Fraction:
def __init__(self, numerator=0, denominator=1):
self.numerator = numerator
self.denominator = denominator
def __add__(self, other):
return Fraction(
self.numerator * other.denominator + other.numerator * self.denominator,
self.denominator * other.denominator
)
def __sub__(self, other):
return Fraction(
self.numerator * other.denominator - other.numerator * self.denominator,
self.denominator * other.denominator
)
def __mul__(self, other):
return Fraction(
self.numerator * other.numerator,
self.denominator * other.denominator
)
def __truediv__(self, other):
return Fraction(
self.numerator * other.denominator,
self.denominator * other.numerator
)
def __iadd__(self, other):
self.numerator = self.numerator * other.denominator + other.numerator * self.denominator
self.denominator = self.denominator * other.denominator
return self
def __isub__(self, other):
self.numerator = self.numerator * other.denominator - other.numerator * self.denominator
self.denominator = self.denominator * other.denominator
return self
def __imul__(self, other):
self.numerator = self.numerator * other.numerator
self.denominator = self.denominator * other.denominator
return self
def __itruediv__(self, other):
self.numerator = self.numerator * other.denominator
self.denominator = self.denominator * other.numerator
return self
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
frac1 = Fraction(1, 2)
frac2 = Fraction(1, 3)
summ = frac1 + frac2
diff = frac1 - frac2
prod = frac1 * frac2
quot = frac1 / frac2
print(f"{summ = }")
print(f"{diff = }")
print(f"{prod = }")
print(f"{quot = }")
frac1 += frac2
print(f"{frac1 = }")
summ = Fraction(5, 6)
diff = Fraction(1, 6)
prod = Fraction(1, 6)
quot = Fraction(3, 2)
frac1 = Fraction(5, 6)
Вариантите на методите, започващи с r
, дефиниращи поведението на операторите, се извикват с разменени аргументи. Те се използват само в случая, когато левият операнд не поддържа съответната операция и операндите са от различни типове.
Нека например имаме израза x - y
и нека y
имплементира __rsub__
. Той ще се извика в случая, когато x
не поддържа __sub__
(няма го дефиниран или връща NotImplemented
) и x
и y
са от различни типове.
Хеширане#
Колекции като dict
, set
и frozenset
изискват елементите им да могат да се хешират. Това се осъществява чрез извикването на вградената функция hash()
, която връща цяло число - съответният хеш.
hash(1)
1
hash(420)
420
hash("a")
-5016004937874941484
hash((1, "a", False))
-5093137992435649037
hash([1, "a", False])
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/1812877005.py in <module>
----> 1 hash([1, "a", False])
TypeError: unhashable type: 'list'
Rule of thumb във всички езици за програмиране е с дефиниране на __hash__
винаги да има и дефиниция на __eq__
(обратното не е задължително да е вярно), понеже няма смисъл да се хешира обект, който не може да се сравнява с други.
В Python при дефиниране на клас, по премълчаване той получава готова default-на дефиниция на __eq__
и __hash__
. Тяхното поведение е такова, че две различни инстанции от този клас не са равни и не произвеждат един и същ хещ (равни са единствено на себе си, т.е. равенство настъпва само при сравняване на имена, сочещи към едно и също място в паметта).
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
p1 = Point(4, 2)
p2 = Point(4, 2)
print(f"{hash(p1) = }")
print(f"{hash(p2) = }")
visited = set()
visited.add(p1)
visited.add(p2)
print(f"{visited = }")
hash(p1) = 289920877
hash(p2) = 289920985
visited = {<__main__.Point object at 0x1147d7d90>, <__main__.Point object at 0x1147d76d0>}
В случая от примера може би не искаме две точки с едни и същи координати да се третират като различни. За да го постигнем, трябва да дефинираме __eq__
и __hash__
:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # the hash of the tuple of the coordinates is sufficient
p1 = Point(4, 2)
p2 = Point(4, 2)
print(f"{hash(p1) = }")
print(f"{hash(p2) = }")
visited = set()
visited.add(p1)
visited.add(p2)
print(f"{visited = }")
hash(p1) = -9131917124991031250
hash(p2) = -9131917124991031250
visited = {<__main__.Point object at 0x113d60790>}
По този начин (чрез предефиниране на ==
и евентуално и на hash()
) определяме еднаквост между инстанциите. След това единственият начин, по който можем да различим дали обектите съвпадат (т.е. са едни и същи от гледна точка на паметта) остава чрез използване на is
.
p1 is p2
False
p1 == p2
True
Контейнери и итератори#
Съществуват методи, които ни помагат в дефинирането на контейнери и мапинги:
__len__(self)
: трябва да върне дължината на контейнера (цяло число \( \ge 0 \)). Извиква се от вградената функцияlen()
.__getitem__(self, key)
: връща елемента с ключkey
(на позицияkey
). Извиква се при read-only достъп с[]
.__setitem__(self, key, value)
: задава стойността на елемента с ключkey
(на позицияkey
) да бъдеvalue
. Извиква се при присвояване с[]
.__delitem__(self, key)
: изтрива елемента с ключkey
(на позицияkey
). Извиква се от ключовата думаdel
.__iter__(self)
: връща итератор, който да се използва за обхождане на контейнера. При мапинги трябва да обхожда ключовете на контейнера. Извиква се от вградената функцияiter()
когато е необходимо. Итераторът трябва да имплементира__next__
, така че при обхождането му да връща следващия елемент при всяко извикване наnext()
.__reversed__(self)
: връща обратен итератор, който да се използва за обхождане на контейнера. Извиква се от вградената функцияreversed()
.__contains__(self, item)
: връщаbool
, казващ далиitem
се съдържа в контейнера. Извиква се при използване наin
за проверка на съдържанието.
class DefaultArray:
"""
A container that behaves like a list with a maximum size (array)
but returns a default value for missing indices.
"""
def __init__(self, limit, default_element_func):
self._limit = limit
self._default_element_func = default_element_func
self._list = [None] * limit
def __len__(self):
return self._limit
def __getitem__(self, index):
element = self._list[index]
return self._default_element_func() if element is None else element
def __setitem__(self, index, element):
self._list[index] = element
def __delitem__(self, index):
self._list[index] = None
def __contains__(self, item):
return item in self._list
arr = DefaultArray(10, int) # `int` can be viewed as a function that returns 0 upon calling
print(f"{arr[1] = }") # __getitem__
arr[1] = 42 # __setitem__
print(f"{arr[1] = }")
print(f"{42 in arr = }") # __contains__
del arr[1] # __delitem__
print(f"{arr[1] = }")
print(f"{42 in arr = }")
arr[1] = 0
arr[1] = 42
42 in arr = True
arr[1] = 0
42 in arr = False
Индексирането в Python е гъвкаво и позволява взимането на цели части (slice
s) от дадена колекция, както и броене отзад-напред.
Синтаксисът е iterable[start:stop:step]
, където:
iterable
е въпросната колекцияstart
е началния индекс (default = 0)stop
е крайният индекс (невключително) (default = len)step
е стъпката (default = 1), през която се взимат елементи. Ако е отрицателна, то обхождането е наобратно.
Индексът, както и start
/stop
могат да бъдат отрицателни числа. В такъв случай -1
означава последният елемент, -2
- предпоследният и т.н.
Също е хубаво да се отбележи, че при slicing не се хвърля IndexError
, в случай че някой от индексите е извън колекцията.
Най-добре всичко това се демонстрира чрез примери:
items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(f"{items[9] = }")
print(f"{items[-1] = }")
print()
print(f"{items[1:5] = }")
print(f"{items[1:] = }")
print(f"{items[:5] = }")
print(f"{items[:-1] = }")
print(f"{items[-6:-1] = }")
print()
print(f"{items[1:5:2] = }")
print(f"{items[1:5:-1] = }")
print(f"{items[5:1:-1] = }")
print()
print(f"{items[5::-1] = }")
print(f"{items[::-1] = }")
print(f"{items[:] = }")
items[9] = 9
items[-1] = 9
items[1:5] = [1, 2, 3, 4]
items[1:] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
items[:5] = [0, 1, 2, 3, 4]
items[:-1] = [0, 1, 2, 3, 4, 5, 6, 7, 8]
items[-6:-1] = [4, 5, 6, 7, 8]
items[1:5:2] = [1, 3]
items[1:5:-1] = []
items[5:1:-1] = [5, 4, 3, 2]
items[5::-1] = [5, 4, 3, 2, 1, 0]
items[::-1] = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
items[:] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Чрез имплементиране на __getitem__
нашият клас поддържа имплицитно slicing. Това е така, понеже горепоказаният начин за slicing е синтактична захар на създаването на slice
обект и предаването му чрез []
.
С други думи, следните две операции са екивалентни:
usa_weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
workdays1 = usa_weekdays[1:-1]
workdays2 = usa_weekdays[slice(1, -1, None)]
print(f"{workdays1 = }")
print(f"{workdays2 = }")
workdays1 = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
workdays2 = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
Това означава, че ако искаме (или пък нарочно и експлицитно не искаме) да поддържаме slicing по адекватен начин в нашия обект, в__getitem__(self, key)
, __setitem__(self, key)
и __delitem__(self, key)
може да е необходимо да проверяваме дали key
е от тип slice
в случаите, в които това има значение. Това може да стане с isinstance(key, slice)
например.
В горния пример с DynamicArray
няма да имаме проблем, понеже параметъра го предаваме директно на __getitem__
на листа който си пазим, а листовете както видяхме вече успешно се справят със slice
-ове.
arr[:]
[None, None, None, None, None, None, None, None, None, None]
Итерирането в Python става с помощта на итератори. Итераторите са обекти, които имплементират метода __next__
, който връща следващия елемент при всяко извикване. Когато няма повече елементи, трябва да се хвърли изключение StopIteration
. Итераторите се използват от for
-циклите.
iterable = [1, 2, 3]
# `for i in iterable: print(i)` is equal to the following calls:
iterator = iter(iterable) # the `for` loop uses the iterator of the object
print(f"{iterator = }") # printing here the iterator just for demo purposes
print(f"{next(iterator) = }") # first iteration
print(f"{next(iterator) = }") # second iteration
print(f"{next(iterator) = }") # third iteration
print(f"{next(iterator) = }") # this exception is caught by the `for` loop and is its exit condition
# let's not catch it and let it boom 💥
iterator = <list_iterator object at 0x118986490>
next(iterator) = 1
next(iterator) = 2
next(iterator) = 3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/920778728.py in <module>
10 print(f"{next(iterator) = }") # second iteration
11 print(f"{next(iterator) = }") # third iteration
---> 12 print(f"{next(iterator) = }") # this exception is caught by the `for` loop and is its exit condition
13 # let's not catch it and let it boom 💥
StopIteration:
С други думи, for
циклите правят следното:
def for_from_aliexpress(iterable, func):
iterator = iter(iterable)
while True:
try:
element = next(iterator)
except StopIteration:
break
func(element)
for_from_aliexpress([1, 2, 3], print)
1
2
3
Ако не разбирате част от кода горе, не се притеснявайте. В последваща лекция ще учим за грешки и изключения, както и в друга говорим за итериране и функции от по-висок ред в повече дълбочина.
За бъде итеруем (iterable) един клас, трябва да имплементира __iter__
, връщайки итератор. Възможно е да върне и собствената си инстанция, в случай, че има имплементиран __next__
.
class NotAnIterable:
pass
not_an_iterable = NotAnIterable()
for i in not_an_iterable:
print(i)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/2000024672.py in <module>
4 not_an_iterable = NotAnIterable()
5
----> 6 for i in not_an_iterable:
7 print(i)
TypeError: 'NotAnIterable' object is not iterable
class DefaultArray:
"""
A container that behaves like a list with a maximum size (array)
but returns a default value for missing indices.
"""
def __init__(self, limit, default_element_func):
self._limit = limit
self._default_element_func = default_element_func
self._list = [None] * limit
def __len__(self):
return self._limit
def __getitem__(self, index):
element = self._list[index]
return self._default_element_func() if element is None else element
def __setitem__(self, index, element):
self._list[index] = element
def __delitem__(self, index):
self._list[index] = None
def __contains__(self, item):
return item in self._list
def __iter__(self):
return iter(self._list)
arr = DefaultArray(10, int)
for i in range(10):
arr[i] = i*i*i # squares are overrated, let's print the cubes
for element in arr: # __iter__ is called here
print(element)
0
1
8
27
64
125
216
343
512
729
class FibonacciGenerator:
def __init__(self, max_index):
self.max_index = max_index
self._a = 1
self._b = 1
self._current_index = 0
def __iter__(self):
return self
def __next__(self):
if self._current_index >= self.max_index:
raise StopIteration
current = self._a
self._a, self._b = self._b, self._a + self._b
self._current_index += 1
return current
lateralus = FibonacciGenerator(16)
for i in lateralus:
print(i)
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
class YetAnotherCustomIterable:
class CustomIterator:
def __init__(self, iterable):
self._iterable = iterable
self._index = 0
def __next__(self):
if self._index >= len(self._iterable):
raise StopIteration
element = self._iterable[self._index]
self._index += 1
return element
def __init__(self, iterable):
self._iterable = iterable
def __iter__(self):
return YetAnotherCustomIterable.CustomIterator(self._iterable)
it = YetAnotherCustomIterable("strings are also iterables")
print("👏".join(i for i in it)) # __iter__ is also called by comprehensions
# more info on list/generator/dict comprehensions in another lecture
s👏t👏r👏i👏n👏g👏s👏 👏a👏r👏e👏 👏a👏l👏s👏o👏 👏i👏t👏e👏r👏a👏b👏l👏e👏s
Функции#
Искате обектът ви да бъде извикан като функция?
Няма проблеми.
class LinearBinomial:
def __init__(self, a, b):
self.a = a
self.b = b
def __call__(self, x):
return self.a * x + self.b
def __str__(self):
A = f"{self.a}x" if self.a != 1 else "x"
B = f"{self.b}" if self.b != 0 else ""
return f"{A} + {B}"
f = LinearBinomial(1, 2)
print(f"f(x) = {f}")
print(f"f(0) = {f(0)}")
print(f"f(1) = {f(1)}")
f(x) = x + 2
f(0) = 2
f(1) = 3
Pure witchcraft#
class ExampleClass:
"""This is an example class.
Note: The first multiline comment in a definition of a class, module or function
is called a 'docstring'.
"""
def __init__(self, var):
"""Take a variable and store it in the instance.
Note: It is good practice to document as many public methods and classes as possible.
"""
self.var = var
def __repr__(self):
"""Return repr(self).
Note: there are established strict conventions as to what good code documentation sould look like.
For example, such comments should always be imperative (e.g. 'return', not 'returns').
For more info check out `pydocstyle` (https://www.pydocstyle.org/en/stable/). It includes a linter
that can be configured inside your IDE to constantly check and give out warnings
regarding your code documentation and style.
Good thing to check out as well is PEP-8 but keep in mind that we will cover PEP-8 in details
in a following lecture.
"""
return f"ExampleClass({self.var})"
__str__ = __repr__ # after all, methods are objects too. They can be passed around and assigned.
instance = ExampleClass(42)
# now let the black magic begin
print(f"{instance.__doc__ = }") # the docstring of the object
print(f"{instance.__repr__.__doc__ = }") # the docstring of a method of the object
print(f"{ExampleClass.__name__ = }") # the name of the class as a string
print(f"{instance.__class__ = }") # the class of the instance as a class object
print(f"{instance.__module__ = }") # the name of the module in which the object exists. In this case, it's `__main__` because we are in the REPL.
print(f"{instance.__dict__ = }") # a `dict` containing all the attributes of the instance. It is returned by the `vars` builtin function:
print(f"{vars(instance) = }")
print(f"{instance.__repr__.__code__ = }") # the code object of the method. Don't expect to see the exact code though, this is some representation of the compiled code.
print(f"{instance.__repr__.__code__.co_code = }") # the actual compiled code. This is a `bytes` object.
print(f"{instance.__str__.__code__.co_code = }") # it's the same picture
instance.__doc__ = "This is an example class.\n \n Note: The first multiline comment in a definition of a class, module or function\n is called a 'docstring'.\n "
instance.__repr__.__doc__ = "Return repr(self).\n \n Note: there are established strict conventions as to what good code documentation sould look like.\n For example, such comments should always be imperative (e.g. 'return', not 'returns').\n For more info check out `pydocstyle` (https://www.pydocstyle.org/en/stable/). It includes a linter\n that can be configured inside your IDE to constantly check and give out warnings\n regarding your code documentation and style.\n Good thing to check out as well is PEP-8 but keep in mind that we will cover PEP-8 in details\n in a following lecture.\n "
ExampleClass.__name__ = 'ExampleClass'
instance.__class__ = <class '__main__.ExampleClass'>
instance.__module__ = '__main__'
instance.__dict__ = {'var': 42}
vars(instance) = {'var': 42}
instance.__repr__.__code__ = <code object __repr__ at 0x113d91df0, file "/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/2566046567.py", line 15>
instance.__repr__.__code__.co_code = b'd\x01|\x00j\x00\x9b\x00d\x02\x9d\x03S\x00'
instance.__str__.__code__.co_code = b'd\x01|\x00j\x00\x9b\x00d\x02\x9d\x03S\x00'
Някои полезни помощни класове и декоратори#
Но първо, какво е “декоратор”?#
Накратко (понеже в друга лекция ще обясним по-подробно), това е функция, обграждаща и “добавяща” функционалност на друга такава. Т.е. приема за параметър някоя функция и връща друга, която е модифицирана версия на първата. В Python може да се използва и чрез @
, което е удобен syntax sugar за декориране.
def logged(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} and {kwargs}")
return func(*args, **kwargs)
return wrapper
@logged
def fibonacci(n=10_000):
a = 0
b = 1
for i in range(n):
a, b = b, a + b
return a
lateralus = fibonacci(16) # here we observe the added side effect of logging the call
print(f"{lateralus = }") # as above, so below
# and beyond I imagine
fibonacci()
Calling fibonacci with (16,) and {}
lateralus = 987
Calling fibonacci with () and {}
33644764876431783266621612005107543310302148460680063906564769974680081442166662368155595513633734025582065332680836159373734790483865268263040892463056431887354544369559827491606602099884183933864652731300088830269235673613135117579297437854413752130520504347701602264758318906527890855154366159582987279682987510631200575428783453215515103870818298969791613127856265033195487140214287532698187962046936097879900350962302291026368131493195275630227837628441540360584402572114334961180023091208287046088923962328835461505776583271252546093591128203925285393434620904245248929403901706233888991085841065183173360437470737908552631764325733993712871937587746897479926305837065742830161637408969178426378624212835258112820516370298089332099905707920064367426202389783111470054074998459250360633560933883831923386783056136435351892133279732908133732642652633989763922723407882928177953580570993691049175470808931841056146322338217465637321248226383092103297701648054726243842374862411453093812206564914032751086643394517512161526545361333111314042436854805106765843493523836959653428071768775328348234345557366719731392746273629108210679280784718035329131176778924659089938635459327894523777674406192240337638674004021330343297496902028328145933418826817683893072003634795623117103101291953169794607632737589253530772552375943788434504067715555779056450443016640119462580972216729758615026968443146952034614932291105970676243268515992834709891284706740862008587135016260312071903172086094081298321581077282076353186624611278245537208532365305775956430072517744315051539600905168603220349163222640885248852433158051534849622434848299380905070483482449327453732624567755879089187190803662058009594743150052402532709746995318770724376825907419939632265984147498193609285223945039707165443156421328157688908058783183404917434556270520223564846495196112460268313970975069382648706613264507665074611512677522748621598642530711298441182622661057163515069260029861704945425047491378115154139941550671256271197133252763631939606902895650288268608362241082050562430701794976171121233066073310059947366875
Съществуват различни полезни декоратори, които ни улесняват с имплеменетирането на често-срещани ООП фунцкионалности.
@property
#
В някои езици наричано “computed property”, това е поле/атрибут, чиято стойност се изчислява при използването му и обикновено не се съхранява никъде. На практика това е метод, който се използва със синтаксиса атрибут. Достатъчно е да декорираме с @property
нормален метод, за да го превърнем в атрибут.
class Player:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
doncho = Player("Doncho")
print(doncho.name)
Doncho
@setter
#
Освен read-only computed property-та, можем да имаме и writable такива. Това се постига чрез декоратора @{propertyname}.setter
, който се използва върху метод, който да се изпълнява при присвояване на стойност на атрибута ({propertyname}
се замества с името на property-то).
class Player:
def __init__(self, xp):
self._xp = xp
@property
def level(self):
return self._xp // 1000 + 1
@level.setter
def level(self, value): # has to have the same name!
self._xp = (value - 1) * 1000
newb = Player(0)
print(f"{newb.level = }")
print("now the player buys something and advances some levels automatically...")
newb.level = 10 # ...cheater
print(f"{newb.level = }")
print(f"{newb._xp = }")
newb.level = 1
now the player buys something and advances some levels automatically...
newb.level = 10
newb._xp = 9000
@staticmethod
#
Ако дефинираме функция в блока за дефиниция на клас, тя става метод на класа и при извикването ѝ винаги имплицитно се предава като първи параметър съответната инстанция на класа. Но как да създадем метод, който не зависи от инстанцията?
class A:
def instance_method(self):
print(self)
def static_method():
print("I am free")
a = A()
a.instance_method()
A.static_method()
a.static_method()
<__main__.A object at 0x113d60be0>
I am free
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/666281317.py in <module>
9 a.instance_method()
10 A.static_method()
---> 11 a.static_method()
TypeError: static_method() takes 0 positional arguments but 1 was given
Ако в горния случай изпълним A.static_method()
няма да бъде направен опит да се предаде инстанция като първи параметър, понеже такава нямаме. Но не винаги можем да сме сигурни, че метода ще бъде извикан върху класа, а не върху конкретен обект. Начинът по който можем да подсигурим безпроблемното изпълнение и в двата случая е да декорираме метода със @staticmethod
:
class A:
@staticmethod
def static_method():
print("I am free")
a = A()
A.static_method()
a.static_method()
I am free
I am free
@classmethod
#
Понякога може да искаме като първи аргумент на метода да ни бъде подадена не инстанцията, а самият клас. Такъв метод трябва да декорираме с @classmethod
. Отново, също както със self
, името на аргумента може да е всякакво, но по конвенция е прието да се кръщава cls
(понеже class
е запазена дума).
class User:
def __init__(self, username, email, money):
self.username = username
self.email = email
self.money = money
@classmethod
def from_json_dict(cls, json):
username = json["username"]
email = json["email"]
money = json["money"]
return cls(username, email, money)
def __repr__(self):
return f"User({self.username}, {self.email}, {self.money})"
json_data = {
"username": "yalishanda",
"email": "yalishanda@example.com",
"money": 420
}
user = User.from_json_dict(json_data)
print(user)
User(yalishanda, yalishanda@example.com, 420)
ABC
(Abstract Base Class) и @abstractmethod
#
Модулът abc
ни предоставя начини, с които да дефинираме абстрактни базови класове. Те не могат да бъдат инстанцирани, когато съдържат един или повече от един абстрактни метода, чиято цел е да опишат общ интерфейс и да бъдат имплементирани от наследниците на класа.
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name):
self._name = name
@abstractmethod
def make_sound(self):
pass
def pet(self):
print(f"Petting {self._name}...")
self.make_sound()
class Dog(Animal):
def make_sound(self):
print("woof")
class Cat(Animal):
def make_sound(self):
print("purr")
class Snek(Animal):
def make_sound(self):
print("sss")
doge = Dog("Doge")
doge.pet()
catto = Cat("Мишо")
catto.pet()
snek = Snek("Съска")
snek.pet()
base = Animal("опа")
Petting Doge...
woof
Petting Мишо...
purr
Petting Съска...
sss
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_62173/3318180173.py in <module>
34 snek.pet()
35
---> 36 base = Animal("опа")
TypeError: Can't instantiate abstract class Animal with abstract method make_sound
Примери (задачи за решаване в час)#
Пример 1#
Напишете клас Rectangle
, описващ правоъгълник, със следните характеристики:
дължина
широчина
цвят (низ)
методи, връщащи стойностите на член-данните на класа
метод за изчисляване на лице
Напишете клас Circle
, описващ кръг, със следните характеристики:
радиус
цвят (низ)
методи, връщащи стойностите на член-данните на класа
метод за изчисляване на лице
Напишете клас Shapes
, със следните характеристики:
съдържа в себе си горе-дефинираните геометрични фигури
метод за добавяне на нов кръг към колекцията
метод за добавяне на нов правоъгълник към колекцията
метод, който връща сумата от всички лица на правоъгълниците
метод, който връща сумата от всички лица на кръговете
метод, който връща геометрична фигура по подаден индекс
Пример 2#
Съществуват различни видове броячи - някои могат само да увеличават бройката, която пазят, други могат и да я намалят, а трети имат ограничение до колко могат да отброяват.
Клас Counter
#
Най-простият брояч - само нагоре, без ограничение.
Инициализатор с параметри
initial=0
(начална стойност) иstep=1
(стъпка)increment()
: увеличава текущата стойност със стъпката на броячаget_total()
: връщаint
- текущата отброена стойностget_step()
: връщаint
- стъпката на брояча (не трябва да може да бъде променяна)
Клас TwowayCounter
#
Брояч, който може и да намалява отброяваната стойност.
Освен всичко изброено в Counter
, съдържа и:
decrement()
: намалява текущата стойност със стъпката на брояча
Клас LimitedCounter
#
Брояч, който отброява само до дадена максимална стойност.
Инициализатор с 3 параметъра
max
(максимална стойност),initial=0
(начална ст-т) иstep=1
(стъпка)increment()
: увеличава текущата стойност със стъпката на брояча само ако няма да надмине максималнатаget_max()
: връщаint
- максималната стойност на броячаget_total()
: същия като този наCounter
get_step()
: същия като този наCounter
Клас LimitedTwowayCounter
#
Той е и LimitedCounter, и TwowayCounter едновременно: може да отброява нагоре до определена максимална стойност и надолу до определена минимална стойност.
Инциализатор с 4 параметъра
min
(минимална ст-т),max
(максимална ст-т),initial=0
(начална ст-т) иstep=1
(стъпка)increment()
: същия като наLimitedCounter
decrement()
: намаля текущата стойност със стъпката на брояча само ако няма да стане по-ниска от минималнатаget_min()
: връща минималната стойност на броячаget_max()
: същия като този наLimitedCounter
get_total()
: същия като този наCounter
get_step()
: същия като този наCounter
Клас Semaphore
#
Най-простия бинарен семафор - това е LimitedTwowayCounter
, който има минимална стойност 0, максимална стойност 1 и стъпка 1. Използва се от процесите в операционнитe системи за синхронизационни цели. (За повече информация: https://skelet.ludost.net)
Инициализатор с 1 параметър
is_available=False
. ПриTrue
началната стойност на брояча е 1, а приFalse
е 0.is_available()
: връщаbool
, показващ дали стойността на брояча е над 0wait()
- прави същото катоdecrement()
наLimitedTwowayCounter
signal()
- прави същото катоincrement()
наLimitedTwowayCounter
Pythonic bonus: Use @property
instead of all the getters.
Двата примера достигат до вас благодарение на курса по ООП 2020/2021 за СИ.#
Задачи за упражнение (за вкъщи)#
Задача 0#
Дефинирайте клас PolarCoordinate
, който да моделира точка чрез двумерни полярни координати. Класът трябва да има:
инициализатор, който приема два аргумента - радиус \( r \) и ъгъл \( \phi \) (ще интерпретираме мерната му единица да е в радиани)
атрибути/методи
r
иangle
за достъп до съответните полета, но не и за тяхната промянаметод
to_cartesian()
, който връщаtuple
от две числа(x, y)
- координатите на точката, конвертирани в декартова система по формулата:\( x = r\cos(\phi) \)
\( y = r \sin(\phi) \)
клас-метод
from_cartesian(x, y)
, който приема два аргумента - координатите на точката в декартова система - и създава и връща инстанция наPolarCoordinate
, изчислена по формулата:\( r = \sqrt{x^2 + y^2} \)
\( \phi = \arctan(\frac{y}{x}) \)
дефинирана подходяща репрезентация на обекта
дефиниция на конвертиране в
str
във формат(r: {self.r}, angle: {self.angle})
дефиниране на хеширане
дефиниране на сравнение с
==
и!=
Важно: стойностите на полетата на класа не трябва да могат да бъдат променяни по никакъв начин, освен чрез инициализатора, с цел гарантиране на immutability.
from math import sqrt, sin, cos, atan # you will need these
from math import pi, isclose # the tests below will need these
class PolarCoordinate:
pass # your code here
p1 = PolarCoordinate(1, pi/6)
print(p1.r == 1)
print(p1.angle == pi/6)
p2 = PolarCoordinate.from_cartesian(3, 4)
print(isclose(p2.r, 5))
print(isclose(p2.angle, atan(4/3)))
x, y = p2.to_cartesian()
print(isclose(x, 3))
print(isclose(y, 4))
p3 = PolarCoordinate(1, 0)
print(str(p3) == "(r: 1, angle: 0)")
print(repr(p3) == "PolarCoordinate(1, 0)")
pp1, pp2, pp3 = PolarCoordinate(1, pi/6), PolarCoordinate.from_cartesian(3, 4), PolarCoordinate(1, 0)
print(p1 == pp1)
print(p2 == pp2)
print(p3 == pp3)
d = {p1: "A", p2: "B", p3: "C"}
print(d[pp1] == "A")
print(d[pp2] == "B")
print(d[pp3] == "C")
s = {p1, p2, p3, pp1, pp2, pp3, p1, p2, p3}
print(len(s) == 3)
Задача 1#
Дефинирайте клас Player
, който да моделира играч от RPG игра. Трябва да има следните атрибути и техните условия към тях:
name
- име на играча (read-only)hp
- жизнени точки на играча (да не могат да падат под 0, по подразбиране са 10)xp
- опит на играча (да може само да бъде увеличаван, по подразбиране е 0)level
- ниво на играча (read-only), зависи отxp
по формулата:\( level = 1 \), ако
xp
е по-малко от 300;\( level = 2 + \log_2 int(\frac{xp}{300}) \), иначе.
(т.е. 0-299 XP са ниво 1, 300-599 XP са ниво 2, 600-1199 XP са ниво 3, 1200-2399 са ниво 4 и т.н.)
Имате свобода при имплементацията на класа, стига горните атрибути да съществуват и условията им да са винаги изпълнени.
Note: за \( log_2 \) може да използвате math.log2
. Функцията int
в горната формула е аналогична на конструктора на int
в Python, както и на floor
функцията, т.е. закръглянето към цяло число е винаги надолу.
Задача 2#
Дефинирайте клас Vector3D
, който да репрезентира вектор в тримерното пространство. Класът трябва да има поне следните методи:
__init__(self, x, y, z)
: инициализира вектора със стойностиx
,y
иz
__repr__(self)
: връща репрезентация на обекта__str__(self)
: връща стрингово представяне на вектора във формат"(x, y, z)"
(например,"(1, 2, 3)"
)__add__(self, other)
: връща сумата на два вектора (или на вектор (вляво) с число (вдясно)) за целите на задачата, нека дефинираме:сума на вектор \( v = (v_x, v_y, v_z) \) с вектор \( u = (u_x, u_y, u_z) \) като \( u + v = (v_x + u_x, v_y + u_y, v_z + u_z) \)
сума на вектор \( v = (v_x, v_y, v_z) \) с число \( \lambda \) като \( u + \lambda = (v_x + \lambda, v_y + \lambda, v_z + \lambda) \)
__radd__(self, other)
: връща сумата на число (в ляво) с вектор (в дясно)__iadd__(self, other)
: прибавяother
къмself
и връщаself
__mul__(self, other)
: връща “произведение”, дефинирано по следния начин:произведението \( v * u \) на два вектора \( v = (v_x, v_y, v_z) \) и \( u = (u_x, u_y, u_z) \) като \( v * u = (v_x u_x, v_y u_y, v_z u_z) \)
произведението \( v * \lambda \) на вектора \( v = (v_x, v_y, v_z) \) и числото \( \lambda \) като \( v * \lambda = (v_x \lambda, v_y \lambda, v_z \lambda) \)
__rmul__(self, other)
: връща произведението (както е дефинирано горе) на число с вектор__imul__(self, other)
: умножаваother
къмself
и връщаself
__eq__(self, other)
: връщаTrue
, ако два вектора са поелементно равни, иFalse
в противен случай__ne__(self, other)
: връща обратното на__eq__
__abs__(self)
: връща дължината на вектора (по формулата \( \sqrt{x^2 + y^2 + z^2} \) )__getattr__(self, name)
: пренасочваX
,Y
иZ
къмx
,y
иz
(напримерvector.X
да е еквивалентно наvector.x
)__setattr__(self, name, value)
, аналогичен на горния__iter__(self)
: връща итератор на вектора, който да позволява итериране в редаx
,y
,z
.
Добавете още каквито методи смятате за нужни.
Note 1: нека в контекста на задачата за скалари смятаме int
, float
и complex
.
Note 2: корен квадратен може да изчислите с math.sqrt
или като повдигнете нещо на степен 0.5.
Note 3: понеже още не сме учили грешки и изключения, не се очаква да хвърляте такива в случай, че някой аргумент е невалиден. Очаква се обаче поне където се налага да проверите за това. Ако не ви се разучава как да raise
-нете TypeError
, AttributeError
или ValueError
, може вместо това да return
-нете None
за сега.
Задача 3#
В тази задача ще направим мини-framework за чертане на UI в конзолата.
С цел той да бъде декларативен и лесно композируем, всички елементи трябва да имат метод render()
, който да връща str
със съдържанието на елемента.
Различните елементи могат да биват:
Spacer(length=1)
: празно място с дадена дължина (length
на брой интервали)Line(length, symbol="-")
: ред от символи с дадена дължина (примерноLine(10).render()
трябва да върне"----------"
)Text(text)
: текст със съдържаниеtext
FancyText(text, symbol="=")
: текст със съдържаниеtext
, между всeки символ на който има сложенsymbol
, както и в началото и в края му (примерноFancyText("Hello").render()
трябва да върне"=H=e=l=l=o="
)HorizontalStack(*elements)
: елементи, подредени един до друг в ред (слепени на реда / разделени чрез “”) (примерноHorizontalStack(Text("Hello"), Line(3), Text("World")).render()
трябва да върне"Hello---World"
)VerticalStack(*elements)
: елементи, подредени един под друг (разделени чрез симвла за празен ред) (примерноVerticalStack(Text("Hello"), Line(3), Text("World")).render()
трябва да върне"Hello\n---\nWorld"
)Box(width, *elements)
: елемент, който е катоVerticalStack
, с разликата че:преди и след
elements
добавяHorizontalStack(Text("+"), Line(width - 2, symbol="="), Text("+"))
всеки ред е с дължина
width
и започва и завършва с"|"
, т.е. приrender()
-ването на всеки елемент отelements
добавяText("|")
в началото и в края на реда. В случай, че дължината на реда е повече отwidth
отрязва излишъка от подадения елемент, a в случай, че дължината е по-малка - добавяSpacer
след елемента с дължинаwidth - len(element.render()) - 2
.
ProgressBar(length, progress)
: прогрес бар с дължинаlength
символа и коефициент на запълванеprogress
(\( \in [0, 1] \)) (примерноProgressBar(10, 0.5).render()
трябва да върне"[====----]"
)
Добавяйте каквито прецените други класове и методи към тях.
Пример#
ui = Box(19,
FancyText("WELCOME!"),
Spacer(),
Text("Loading packages:THIS SHOULD NOT BE SHOWN IN THE BOX"),
HorizontalStack(
Line(3),
Spacer(),
Text("cowsay")
),
HorizontalStack(
Line(3),
Spacer(),
Text("lolcat")
),
HorizontalStack(
Line(3, symbol=">"),
Spacer(),
Text("whoami"),
Text("...")
),
Spacer(),
HorizontalStack(
Spacer(),
ProgressBar(15, 0.4),
Spacer()
)
)
print(ui.render())
Изход:
+=================+
|=W=E=L=C=O=M=E=!=|
| |
|Loading packages:|
|--- cowsay |
|--- lolcat |
|>>> whoami... |
| |
| [======-------] |
+=================+