Към съдържанието
wannacode

Класове в класове и данни в данни

Проблем

Клас, който има смисъл само на едно място

В урока за свързания списък 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 total
order = 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 defined
Order.Line

В Python вложени класове се срещат по-рядко, отколкото в Java. Често малкият клас е просто до големия, в същия файл, или вместо клас има речник. За това е следващата карта.

Идеята

Данни в данни: речници и списъци

Ако редът на поръчката само пази стойности и няма методи, в Python често няма клас. Поръчката е речник, в който има списък от речници:

order = {
"customer": "Иванова",
"lines": [
{"product": "Кафе на зърна", "quantity": 2, "price": 12.5},
{"product": "Чаша", "quantity": 4, "price": 3.0},
],
}
total = 0
for 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] * 3
grid[0][0] = 1
print(grid)
grid = []
for row in range(3):
grid.append([0] * 3)
grid[0][0] = 1
print(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 €
В Java има вътрешен клас (inner class): всеки обект от него пази скрита референция към обекта отвън. В Python такъв няма, затова референцията е видима: add подава self, а Item я пази в self.cart. final_price чете _discount_percent на количката, в която е. Долната черта казва „вътрешно“, а Item е вътре в Cart, затова може.

cart = Cart(10)
cart.add("Слушалки", 80)
cart.add("Калъф", 20)
cart.print_items()
Стъпка по стъпка, с думи
  1. Количката е обект Cart с отстъпка 10%.
  2. add създава Item и му подава количката, self. Освен name и price, всеки Item пази в cart стрелка към своята количка.
  3. Вторият Item сочи към същата количка.
  4. final_price на „Слушалки“ минава по стрелката self.cart и чете _discount_percent: 80 * (100 - 10) / 100 = 72.0.
  5. 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()
Отговор

Пробвай

Намери грешката

Редът трябва да покаже валутата на поръчката. При print(Order.Line("Кафе", 12.5).show()) програмата спира:
Грешка
NameError: name 'currency' is not defined

На кой ред е проблемът?

Избери реда с грешката.

Идеята

Клас във функция

Клас може да се обяви и в функция. Тогава се вижда само там и се казва локален клас:

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.0
100
  • Функцията е стойност. 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)
Отговор

Упражнение

Класове, които вървят заедно

Реши задачите в редактора си.

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.

Изход
Заявки: 5
200: 3
401: 1
500: 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.

Тест

Провери се

1. class Line е вътре в class Order. Вижда ли Line атрибутите на конкретна поръчка?
2. Защо show() на Order.Line спира с NameError: name 'currency' is not defined?
3. С какво Python заменя анонимен клас с един метод, като отстъпката?
4. Какво прави sorted(students, key=by_grade)?
5. Кое подрежда учениците по успех от най-високия надолу?
6. grid = [[0] * 3] * 3 и после grid[0][0] = 1. Какво има в grid?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·