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

Общото за всички: атрибути на класа

Проблем

Всеки продукт е №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.

shop.py
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("Кафе")
Стъпка по стъпка, с думи
  1. _next_id е атрибут на класа: един, а не във всеки обект.
  2. Product("Хляб"): нов обект, __init__ записва името.
  3. self._id = Product._next_id: обектът взима текущия номер, 1.
  4. Product._next_id += 1 сменя общия брояч: вече е 2.
  5. Мляко: същият брояч. Взима 2 и го прави 3.
  6. Кафе: взима 3 и го прави 4.
  7. Броячът е един за всички, затова номерата са поредни.
По-точно, за любопитните

Внимавай със 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)
Отговор

Идеята

@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))
Изход
30
5.25
8

На 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).

Пробвай

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

tea.label() спира програмата. На кой ред е грешката?

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

Идеята

Константи

ДДС е 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.25
print(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._id

coffee.id чете номера, а coffee.id = 5 спира с AttributeError, защото свойството няма setter.

Пробвай

Кое липсва?

Месеците в годината са 12 и това няма да се промени. Какво име пише на празното място?
class Calendar:
     = 12

Идеята

Кога на класа и кога на обекта

Питай се: „Това на конкретен обект ли е?“

  • Да (балансът на сметката, името на продукта, тегленето от сметка): атрибут на обекта в __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 €
Поръчки: 3

2. Калкулатор. Клас Calculator със методи с @staticmethod за събиране, изваждане и умножение на цели числа и за деление на дробни.

Програмата
print(Calculator.add(10, 20))
print(Calculator.subtract(10, 15))
print(Calculator.multiply(6, 7))
print(Calculator.divide(5, 2))
Изход
30
-5
42
2.5

3. Обмяна. Клас Converter с константи за курса: 1 € = 1,08 щатски долара и 1 € = 0,85 британски лири (курсовете са примерни). Методи с @staticmethod превръщат сума в евро в долари и в лири. За 20 €:

Изход
20 EUR = 21.6 USD
20 EUR = 17.0 GBP

Още задачи

  • Сметки с номера. Към Account от урока „Капсулация“ добави пореден номер на сметката от брояч, атрибут на класа, и свойство само за четене за номера.
  • Обмяна в двете посоки. Добави към Converter методи от долари и от лири към евро. Превърни 20 € в долари и обратно в евро. Получава ли се точно 20?
  • Ограничение. Магазинът приема най-много 3 продукта в количка. Сложи константа за това и накарай метода за добавяне да отказва четвъртия.

Накратко

Модулът накратко

  • Клас е твой тип, обект е конкретен екземпляр по него, създаден с Product(). Променливата сочи към обекта. Свой тип
  • __init__ задължава обектът да се създаде цял. self е текущият обект. Конструктори и self
  • Методите на обекта работят с неговите атрибути. __str__ казва как изглежда като текст. Поведение
  • _name казва „вътрешно“, а @property със setter пуска промяната само през проверка. Капсулация
  • Атрибутите на класа, @staticmethod и @classmethod са общото за класа: броячи, помощни методи и константи.

Данни и правилата за тях на едно място, с ясно казано кое е вътрешно: това е обектно-ориентираното програмиране. В следващия модул класовете ще започнат да споделят код помежду си.

Тест

Провери се

1. Създадени са 5 обекта от клас с атрибут на класа count = 0. Колко броя count има в паметта?
2. Кое НЕ може @staticmethod?
3. Какво получава @classmethod като първи параметър?
4. В класа има _next_id = 1. Какво прави self._next_id += 1 в __init__?
5. Какво става при Prices.VAT_RATE = 0.25?
6. Кое е добър кандидат за @staticmethod?

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