Класове в класове и данни в данни
Проблем
Клас, който има смисъл само на едно място
В урока за свързания списък Node беше отделен клас. Всеки в програмата може да прави възли, а те имат смисъл само в списъка. Същото е с реда от поръчката:
class OrderLine: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
class Order: def __init__(self): self.lines = []OrderLine без Order е безсмислен. Името е дълго, за да не се сблъска с реда от фактурата, InvoiceLine, или с реда от склада. А и нищо не казва, че двата класа вървят заедно. Може ли редът да живее вътре в поръчката?
Идеята
Вложен клас
Може. Клас, обявен вътре в друг клас, се казва вложен клас (nested class):
class Order: class Line: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
def total(self): return self.quantity * self.price
def __init__(self): self._lines = []
def add(self, line): self._lines.append(line)
def total(self): total = 0 for line in self._lines: total += line.total() return totalorder = Order()order.add(Order.Line("Кафе на зърна", 2, 12.5))order.add(Order.Line("Чаша", 4, 3.0))print(f"Общо: {order.total()} €")Общо: 37.0 €- Отвън името е
Order.Line.Order.Line(...)се прави без обектOrder, като обикновен клас. - И вътре в методите на
Orderсе пишеOrder.Line. СамоLineтам не се вижда: класът не е обхват за методите си. - Вложеният клас в Python не е вързан за конкретна поръчка и не вижда атрибутите ѝ. В Java такъв клас се казва статичен.
- Ако е
_Line, по споразумение казва „само за вътрешна употреба“, както_lines.
class Order: class Line: pass
def make(self): return Line()
try: Order().make()except NameError as e: print(f"NameError: {e}")print(type(Order.Line()).__qualname__)NameError: name 'Line' is not definedOrder.LineВ Python вложени класове се срещат по-рядко, отколкото в Java. Често малкият клас е просто до големия, в същия файл, или вместо клас има речник. За това е следващата карта.
Идеята
Данни в данни: речници и списъци
Ако редът на поръчката само пази стойности и няма методи, в Python често няма клас. Поръчката е речник, в който има списък от речници:
order = { "customer": "Иванова", "lines": [ {"product": "Кафе на зърна", "quantity": 2, "price": 12.5}, {"product": "Чаша", "quantity": 4, "price": 3.0}, ],}
total = 0for line in order["lines"]: total += line["quantity"] * line["price"]print(order["lines"][0]["product"])print(f"Общо: {total} €")Кафе на зърнаОбщо: 37.0 €order["lines"]е списъкът,order["lines"][0]е първият ред, аorder["lines"][0]["product"]е продуктът в него. Всяко[...]влиза едно ниво навътре.- Стойност на речник може да е всичко: число, текст, списък, друг речник.
- Клас е по-добрият избор, когато данните имат поведение (метод
total) или правила, които трябва да се пазят.
Речник от списъци събира нещата в групи:
people = [("Иван", "София"), ("Мария", "Варна"), ("Ана", "София")]by_city = {}for name, city in people: if city not in by_city: by_city[city] = [] by_city[city].append(name)print(by_city)print(by_city["София"][1]){'София': ['Иван', 'Ана'], 'Варна': ['Мария']}АнаПо-точно, за любопитните
- Капан с вложени списъци:
* 3повтаря референцията, не прави три списъка. Трите реда са един и същ списък:
grid = [[0] * 3] * 3grid[0][0] = 1print(grid)
grid = []for row in range(3): grid.append([0] * 3)grid[0][0] = 1print(grid)[[1, 0, 0], [1, 0, 0], [1, 0, 0]][[1, 0, 0], [0, 0, 0], [0, 0, 0]]Идеята
Вложеният обект пази външния
Количката има отстъпка за всички продукти в нея. Всеки продукт трябва да знае отстъпката на своята количка. В Python вложеният клас не вижда обекта отвън, затова количката се подава изрично:
class Cart: class Item: def __init__(self, cart, name, price): self.cart = cart self.name = name self.price = price
def final_price(self): return self.price * (100 - self.cart._discount_percent) / 100
def __init__(self, discount_percent): self._discount_percent = discount_percent self._items = []
def add(self, name, price): self._items.append(Cart.Item(self, name, price))
def print_items(self): for item in self._items: print(f"{item.name}: {item.final_price()} €")cart = Cart(10)cart.add("Слушалки", 80)cart.add("Калъф", 20)cart.print_items()Слушалки: 72.0 €Калъф: 18.0 €add подава self, а Item я пази в self.cart. final_price чете _discount_percent на количката, в която е. Долната черта казва „вътрешно“, а Item е вътре в Cart, затова може.cart = Cart(10)cart.add("Слушалки", 80)cart.add("Калъф", 20)cart.print_items()Стъпка по стъпка, с думи
- Количката е обект Cart с отстъпка 10%.
- add създава Item и му подава количката, self. Освен name и price, всеки Item пази в cart стрелка към своята количка.
- Вторият Item сочи към същата количка.
- final_price на „Слушалки“ минава по стрелката self.cart и чете _discount_percent: 80 * (100 - 10) / 100 = 72.0.
- final_price на „Калъф“ минава по стрелката self.cart и чете _discount_percent: 20 * (100 - 10) / 100 = 18.0.
По-точно, за любопитните
- Отвън обект
Itemсе създава с количката като първи аргумент. Обикновено външният клас сам създава вътрешните, катоaddтук:
cart = Cart(10)cable = Cart.Item(cart, "Кабел", 10)print(cable.final_price())9.0- Полето
cartе точно това, което в Java прави скритотоCart.this. Тук то просто се вижда. - Ако вложеният клас не ползва полетата на външния, не му давай външния обект. Така не пази излишна референция.
Пробвай
Две колички
Класът Cart е същият. Едната количка е без отстъпка, а другата е с 20% за членове на клуба.
Какво ще отпечата програмата?
regular = Cart(0)member = Cart(20)regular.add("Книга", 15)member.add("Книга", 15)regular.print_items()member.print_items()Всеки Item вижда отстъпката на количката, която го е създала. Книгата в regular е с 0%, а тази в member е с 20%.
Пробвай
Намери грешката
print(Order.Line("Кафе", 12.5).show()) програмата спира:NameError: name 'currency' is not definedНа кой ред е проблемът?
Избери реда с грешката.
currency е атрибут на обект Order, а Line няма поръчка. Само име currency Python търси в метода и в целия файл и не го намира. Грешката излиза чак когато show се извика, не по-рано. Поправката е Line да получи поръчката, Order.Line(order, "Кафе", 12.5), и да чете self.order.currency. Друг начин е currency да е атрибут на класа, Order.currency, ако валутата е една за всички поръчки.Идеята
Клас във функция
Клас може да се обяви и в функция. Тогава се вижда само там и се казва локален клас:
def print_best_seller(names, sold): class Sale: def __init__(self, name, count): self.name = name self.count = count
best = Sale(names[0], sold[0]) for i in range(1, len(names)): if sold[i] > best.count: best = Sale(names[i], sold[i]) print(f"Най-продаван: {best.name} ({best.count} бр.)")names = ["Кафе", "Чай", "Вода"]sold = [120, 45, 300]print_best_seller(names, sold)Най-продаван: Вода (300 бр.)Sale държи заедно името и броя. Извън print_best_seller никой не го ползва, затова е там. Такива класове се срещат рядко, но е добре да ги познаваш.
Идеята
Функция вместо клас
Отстъпките в магазина може да са абстрактен клас с един метод, както в модул „Наследяване“:
class Discount(ABC): @abstractmethod def apply(self, price): passИ за всяка отстъпка клас: class BlackFriday(Discount): .... В Java за отстъпка, която се ползва само на едно място, има анонимен клас: клас без име, написан точно там. В Python анонимни класове няма. Нужни не са: когато „класът“ има един метод, в Python се подава функция:
def black_friday(price): return price * 70 / 100
def no_discount(price): return price
def checkout(price, discount): return discount(price)
print(checkout(100, black_friday))print(checkout(100, no_discount))70.0100- Функцията е стойност.
checkout(100, black_friday)подава функцията, без скоби, аcheckoutя вика сdiscount(price). checkoutне пита какво еdiscount. Стига да може да се извика с цена.no_discount(100)връща числото, което е получило, затова е100, а не100.0.
По-точно, за любопитните
- В модул „Модерен Python“ ще видиш по-кратък запис на малки функции като
no_discount, направо на мястото, където се подават. - Функция, обявена в друга функция, вижда променливите ѝ. Това прави функциите още по-удобни за такива случаи.
Идеята
key: подреждане по свое правило
sorted подреди текст и числа. Ученици обаче не може да подреди:
class Student: def __init__(self, name, grade): self.name = name self.grade = grade
def __repr__(self): return f"{self.name} {self.grade}"students = [ Student("Иван", 5.50), Student("Мария", 6.00), Student("Петър", 4.75), Student("Ана", 5.50),]sorted(students)TypeError: '<' not supported between instances of 'Student' and 'Student'Python не знае по какво да подреди ученици: по име, по успех, по възраст? < между два Student не е определено.
Правилото се дава като функция в key. Тя получава един елемент и връща стойността, по която да се сравнява. sorted я вика за всеки ученик и подрежда по резултата:
def by_grade(student): return student.grade
print(sorted(students, key=by_grade, reverse=True))[Мария 6.0, Иван 5.5, Ана 5.5, Петър 4.75]key=by_gradeподава функцията, без скоби.reverse=Trueобръща реда, затова най-високият успех е пръв.- Иван и Ана са с еднакъв успех и остават в реда, в който бяха.
sortedне разменя равни елементи, и сreverse=True. printна списък показва елементите с__repr__, затоваStudentго има.- За текст и числа в обратен ред стига
reverse=True:
cities = ["Варна", "София", "Пловдив"]print(sorted(cities, reverse=True))['София', 'Пловдив', 'Варна']По-точно, за любопитните
students.sort(key=by_grade, reverse=True)подрежда самия списък, аsortedвръща нов.- Текстът и числата се подреждат без
key, защото имат<. И твоят клас може да има естествен ред, ако напишеш__lt__.keyе за случаите, в които редовете са няколко. - Функция за сравнение като
compare(a, b)в Java също може, сfunctools.cmp_to_key. В Python обачеkeyе обичайният начин: по-кратък е и по-бърз.
Пробвай
Кое е първо?
Какво ще отпечата програмата?
products = [ Product("Монитор", 220), Product("Мишка", 25), Product("Клавиатура", 60),]
def by_price(product): return product.price
for p in sorted(products, key=by_price): print(p.name)by_price връща цената, а sorted подрежда от най-малката: 25, 60, 220. За най-скъпото отпред добави reverse=True.Упражнение
Класове, които вървят заедно
Реши задачите в редактора си.
1. Количка за пазаруване. Клас ShoppingCart с вложен клас Item (име и цена) и методи add_item(item), get_total() и count(). Добави ShoppingCart.Item("Мляко", 2.5), хляб за 1,20 € и кафе за 7,50 €. Сумата печатай с два знака след точката.
Продукти: 3Общо: 11.20 €2. Монитор на уеб сървър. Клас WebServer с вложен клас Request (адрес и статус код) и методи add_request(r) и print_stats(). Статистиката казва колко са заявките, колко са от всеки статус код (подредени) и какъв процент са успешни, тоест с код от 200 до 299. Броевете пази в речник. Заявките са /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Заявки: 5200: 3401: 1500: 1Успешни: 60%3. Сграден мениджър. Клас Building с вложен клас Apartment (номер, брой стаи, собственик) и методи add_apartment(a) и get_apartment_info(number). Пази апартаментите в речник номер → апартамент. Добави 101 с 3 стаи на Иванова и 102 с 2 стаи на Петров. Потърси 101 и 999.
Апартамент 101: 3 стаи, собственик ИвановаНяма апартамент 999.4. Класиране. Подреди учениците от урока по успех, от най-високия надолу. При равен успех ги подреди по име, по азбучен ред. Всичко е в една key функция: нека връща кортеж (-успех, име). Кортежите се сравняват по първия елемент, а при равен по втория. Минусът обръща реда само на успеха.
Мария 6.0Ана 5.5Иван 5.5Петър 4.75Още задачи
- Конфигуратор на компютър. Клас
Computerс вложени класовеCPU(марка, честота в GHz),RAM(GB) иStorage(GB) и методиset_cpu,set_ram,set_storage.__str__описва конфигурацията:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Организатор на пътуване, отново. Направи задачата от урока за речниците веднъж с вложен клас
TripPlanner.Destination, който пази името и списъка от дейности, и веднъж само с речник от списъци. Кое е по-удобно? - Мрежа от контакти, отново. Премести
Contactот задачата в урока за речниците вътре вContactNetwork. Трябва лиContactда знае мрежата си? - Смятане наум, отново. Премести
Questionот задачата в урока за списъците вътре вMathGame. - Продукти по два начина. Подреди списък с продукти по цена от най-скъпия, а при еднаква цена по име. После подреди същия списък само по име в обратен ред.
- Своя отстъпка. Напиши три функции-отстъпки: 10% над 100 €, фиксирани 5 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка с една и съща
checkout.
Тест