Общото за всички: атрибути на класа
Проблем
Всеки продукт е №1
Магазинът иска всеки нов продукт да получава пореден номер: 1, 2, 3… Слагаш брояч _next_id в продукта. __init__ дава на продукта текущия номер и увеличава брояча:
class Product: def __init__(self, name): self._next_id = 1 self._name = name self._id = self._next_id self._next_id += 1
def __str__(self): return f"№{self._id} {self._name}"
bread = Product("Хляб")milk = Product("Мляко")coffee = Product("Кафе")print(bread)print(milk)print(coffee)№1 Хляб№1 Мляко№1 КафеВсички са №1. Причината е, че _next_id е атрибут на обекта, както името. Всеки нов продукт получава свой брояч, който започва от 1. Броячът на хляба никога не стига до млякото.
Идеята
Атрибут на класа, не на обекта
Броячът не е част от един продукт. Той е общ за всички продукти. Запиши го в самия клас, извън __init__:
class Product: _next_id = 1Атрибут, записан в класа, е атрибут на класа. Той е един, колкото и обекти да създадеш, и всички обекти го делят. В __init__ до него се стига през името на класа: Product._next_id.
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
def __str__(self): return f"№{self._id} {self._name}"
bread = Product("Хляб")milk = Product("Мляко")coffee = Product("Кафе")print(bread)print(milk)print(coffee)№1 Хляб№2 Мляко№3 КафеХлябът взима 1 и прави брояча 2. Млякото вижда същия брояч, взима 2 и го прави 3.
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
bread = Product("Хляб")milk = Product("Мляко")coffee = Product("Кафе")Стъпка по стъпка, с думи
- _next_id е атрибут на класа: един, а не във всеки обект.
- Product("Хляб"): нов обект, __init__ записва името.
- self._id = Product._next_id: обектът взима текущия номер, 1.
- Product._next_id += 1 сменя общия брояч: вече е 2.
- Мляко: същият брояч. Взима 2 и го прави 3.
- Кафе: взима 3 и го прави 4.
- Броячът е един за всички, затова номерата са поредни.
По-точно, за любопитните
Внимавай със self. Четенето self._next_id намира атрибута на класа, но записът self._next_id += 1 прави нов атрибут на обекта, а общият брояч не мърда:
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = self._next_id self._next_id += 1
def __str__(self): return f"№{self._id} {self._name}"
bread = Product("Хляб")milk = Product("Мляко")print(bread)print(milk)print(Product._next_id)№1 Хляб№1 Мляко1Затова общото се сменя само през името на класа: Product._next_id += 1.
Пробвай
Общо и свое
Какво ще отпечата програмата?
class Order: count = 0
def __init__(self): Order.count += 1 self.items = 0 self.items += 1
a = Order()b = Order()c = Order()print(Order.count, c.items)count е атрибут на класа: един брояч за всички поръчки, увеличен три пъти. items е атрибут на обекта: всяка поръчка има свой, увеличен веднъж.Идеята
@staticmethod: работа без обект
Същото важи за методите. Метод с @staticmethod отгоре е метод на класа. Няма self и се вика през името на класа, без да правиш обект:
class Calculator: @staticmethod def add(a, b): return a + b
@staticmethod def average(values): total = 0 for v in values: total += v return total / len(values)
print(Calculator.add(10, 20))grades = [5, 6, 4, 6]print(Calculator.average(grades))print(max(3, 8))305.258На Calculator.add не му трябва обект: всичко, от което има нужда, идва като параметри. В Python такава работа често е просто функция извън класа, като max и math.sqrt. В клас я слагаш, когато е тясно свързана с него и искаш да е под неговото име.
Python няма main в клас, от който да тръгва програмата: тя е редовете на файла отгоре надолу. Затова функциите от модул „Функции“ нямаха нужда от клас.
По-точно, за любопитните
Има и @classmethod. Той получава като първи параметър самия клас, cls, и стига до атрибутите на класа без обект:
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
@classmethod def created(cls): return cls._next_id - 1
bread = Product("Хляб")milk = Product("Мляко")print(Product.created())2@staticmethod не получава нищо, @classmethod получава класа, обикновеният метод получава обекта (self).
Пробвай
Намери грешката
Избери реда с грешката.
label() чете self._name, а името е на конкретен продукт. @staticmethod не получава self, затова не знае чие име да вземе:NameError: name 'self' is not defined@staticmethod и сложи self като параметър: етикетът е на продукта, затова е метод на обекта.Идеята
Константи
ДДС е 20% за всички продукти и не се променя, докато програмата работи. Такава стойност е константа:
class Prices: VAT_RATE = 0.20
@staticmethod def with_vat(price): return price * (1 + Prices.VAT_RATE)
print(Prices.with_vat(50))print(f"ДДС: {Prices.VAT_RATE * 100}%")60.0ДДС: 20.0%В Python няма константи. Има договорка: име с главни букви и долна черта, VAT_RATE, значи „това не се сменя“. Записано в класа, то е атрибут на класа.
Ако някой опита да я смени,
Prices.VAT_RATE = 0.25нищо не я пази и Prices.VAT_RATE вече е:
0.25Ползата от договорката е, че числото 0.20 е написано на едно място с ясно име. Главните букви казват на всеки, който чете кода, „не пипай“, но пазенето е твоя грижа.
По-точно, за любопитните
Final от модула typing изглежда като константа, но Python не го проверява. Предупреждава само редакторът или инструмент като mypy:
from typing import Final
class Prices: VAT_RATE: Final = 0.20
Prices.VAT_RATE = 0.25print(Prices.VAT_RATE)0.25Понякога стойността е различна за всеки обект, но след създаването не трябва да се мени. Такъв е номерът на продукта. Направи го свойство само за четене, @property без setter (урок „Капсулация“):
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
@property def id(self): return self._idcoffee.id чете номера, а coffee.id = 5 спира с AttributeError, защото свойството няма setter.
Пробвай
Кое липсва?
class Calendar:
= 12MONTHS: главните букви казват, че това е константа. Python не я пази, но всеки, който чете кода, знае да не я пипа.Идеята
Кога на класа и кога на обекта
Питай се: „Това на конкретен обект ли е?“
- Да (балансът на сметката, името на продукта, тегленето от сметка): атрибут на обекта в
__init__или метод съсself. - Не (броят на всички сметки, ДДС, сбор на две числа): атрибут на класа,
@staticmethodили@classmethod.
Честа грешка е списък като атрибут на класа. Той е един за всички обекти, затова това, което сложиш в едната количка, се появява и в другата:
class Cart: items = []
def add(self, name): self.items.append(name)
first = Cart()second = Cart()first.add("Хляб")print(second.items)['Хляб']Списъкът е на всяка количка, затова мястото му е в __init__: self.items = []. Иначе всички „обекти“ делят едни и същи данни и се връщаш при проблема с успоредните списъци от първия урок.
Упражнение
Общото за всички
Реши задачите в редактора си.
1. Поръчки. Клас Order с клиент и сума. Всяка нова поръчка получава пореден номер от брояч, атрибут на класа. Метод count() с @classmethod връща колко поръчки са създадени.
orders = [ Order("Мария", 45.5), Order("Петър", 120), Order("Елена", 18.9),]for o in orders: print(o)print(f"Поръчки: {Order.count()}")Поръчка №1: Мария, 45.5 €Поръчка №2: Петър, 120 €Поръчка №3: Елена, 18.9 €Поръчки: 32. Калкулатор. Клас Calculator със методи с @staticmethod за събиране, изваждане и умножение на цели числа и за деление на дробни.
print(Calculator.add(10, 20))print(Calculator.subtract(10, 15))print(Calculator.multiply(6, 7))print(Calculator.divide(5, 2))30-5422.53. Обмяна. Клас Converter с константи за курса: 1 € = 1,08 щатски долара и 1 € = 0,85 британски лири (курсовете са примерни). Методи с @staticmethod превръщат сума в евро в долари и в лири. За 20 €:
20 EUR = 21.6 USD20 EUR = 17.0 GBPОще задачи
- Сметки с номера. Към
Accountот урока „Капсулация“ добави пореден номер на сметката от брояч, атрибут на класа, и свойство само за четене за номера. - Обмяна в двете посоки. Добави към
Converterметоди от долари и от лири към евро. Превърни 20 € в долари и обратно в евро. Получава ли се точно 20? - Ограничение. Магазинът приема най-много 3 продукта в количка. Сложи константа за това и накарай метода за добавяне да отказва четвъртия.
Накратко
Модулът накратко
- Клас е твой тип, обект е конкретен екземпляр по него, създаден с
Product(). Променливата сочи към обекта. Свой тип __init__задължава обектът да се създаде цял.selfе текущият обект. Конструктори и self- Методите на обекта работят с неговите атрибути.
__str__казва как изглежда като текст. Поведение _nameказва „вътрешно“, а@propertyсъс setter пуска промяната само през проверка. Капсулация- Атрибутите на класа,
@staticmethodи@classmethodса общото за класа: броячи, помощни методи и константи.
Данни и правилата за тях на едно място, с ясно казано кое е вътрешно: това е обектно-ориентираното програмиране. В следващия модул класовете ще започнат да споделят код помежду си.
Тест