Создание класса в Python
Эксперименты будем ставить на коде, который решает очень важную и ответственную задачу — выводит на экран вес фруктов. Делает он это, разумеется, с использованием ООП. Куда же без него во “взрослых” проектах?! И пускай от объявленного класса Fruit
нет никакого проку, без него код выглядел бы слишком просто, а теперь в самый раз:
class Fruit():
pass
apple = Fruit()
apple.fruit_name = 'Яблоко'
apple.weight_kg = 0.1
orange = Fruit()
orange.fruit_name = 'Апельсин'
orange.weight_kg = 0.3
print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000)
print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Скопируйте код к себе, запустите его в консоли:
$ python script.py
Яблоко | вес в граммах 100.0
Апельсин | вес в граммах 300.0
Как это работает
Разберем что написано в этом коде. Первые две строки объявят новый класс Fruit
:
class Fruit():
pass
Команда pass
ничего не делает. Это заглушка, она нужна лишь потому, что Python требует внутри объявления класса написать хотя бы какой-то код. Написать здесь нечего, поэтому pass
.
Следующая строка кода создаст новый объект — яблоко “apple”:
apple = Fruit()
Класс Fruit
выступает шаблоном, который описывает свойства общие сразу для всех фруктов: и для яблок, и для апельсинов. Пока что шаблон пуст, и экземпляр apple
ничего полезного от вызова Fruit
не получит.
Следующие две строки добавят пару атрибутов к яблоку — название фрукта и его вес:
apple.fruit_name = 'Яблоко'
apple.weight_kg = 0.1
Затем аналогичным образом по тому же шаблону — классу Fruit
— будет создан еще один фрукт, на этот раз апельсин:
orange = Fruit()
orange.fruit_name = 'Апельсин'
orange.weight_kg = 0.3
Последние строчки кода пересчитают вес фрукта в граммах и выведут в консоль название и вес:
print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000)
print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Обязательные атрибуты
Можно заметить, что оба фрукта в программе имеют свойства с одинаковыми названиями — название fruit_name
и вес weight_kg
. Доработаем класс Fruit
таким образом, чтобы все экземпляры созданные по этому шаблону обязательно имели свойства fruit_name
и weight_kg
.
Идея следующая. Пусть Python каждый раз после создания нового экземпляра фрукта сразу добавляет все нужные атрибуты. Добавим для этого новую функцию init_attrs
:
# ... объявление класса Fruit ...
def init_attrs(fruit, fruit_name='неизвестный фрукт', weight_kg=None):
fruit.fruit_name = fruit_name
fruit.weight_kg = weight_kg
apple = Fruit()
init_attrs(apple, 'Яблоко', 0.1)
orange = Fruit()
init_attrs(orange, 'Апельсин', 0.3)
# ... вызовы print ...
Кода стало больше, но он стал надежнее. Добавляя новые фрукты: груши, вишню или абрикосы — вы точно не забудете указать вес и название. Вызовы функции init_attrs
гарантирует, что все фрукты получат одинаковый набор обязательных атрибутов.
В Python коде функции похожие на init_attrs
встречаются настолько часто, что для них есть стандартное название и специальный синтаксис. Переименуем функцию init_attrs
в __init__
и переместим внутрь класса Fruit
.
class Fruit():
def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None):
fruit.fruit_name = fruit_name
fruit.weight_kg = weight_kg
apple = Fruit('Яблоко', 0.1)
orange = Fruit('Апельсин', 0.3)
print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000)
print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Обратите внимание, что в программе нигде нет явного вызова __init__
. Python сам его вызовет выполняя эти строки кода:
apple = Fruit('Яблоко', 0.1)
orange = Fruit('Апельсин', 0.3)
Теперь создавать фрукты по шаблону стало еще проще. Не надо писать вызовы функций, достаточно передать все аргументы для __init__
в класс Fruit
.
Часто метод __init__
называют конструктором класса по аналогии с другими языками программирования. Это не совсем верно. Подробнее читайте на StackOverflow.
Добавим метод
Можно заметить что в коде пару раз встречается одинаковое выражение для расчета веса в граммах:
print(..., apple.weight_kg * 1000)
print(..., orange.weight_kg * 1000)
Пересчет веса можно вынести в отдельную функцию:
# ... объявление класса, __init__ для Fruit
def get_weight_gr(fruit):
return fruit.weight_kg * 1000
apple = Fruit('Яблоко', 0.1)
orange = Fruit('Апельсин', 0.3)
print(apple.fruit_name, "| вес в граммах", get_weight_gr(apple))
print(orange.fruit_name, "| вес в граммах", get_weight_gr(orange))
Функция get_weight_gr
требует единственный аргумент — объект описывающий фрукт. Ей нет разницы, с каким именно фруктом работать, яблоком или апельсином. Главное, это чтобы фрукт был создан по стандартному шаблону — на основе класса Fruit
. Python позволяет спрятать функцию get_weight_gr
внутрь класса Fruit
, чтобы эту связь между ними было не разорвать.
class Fruit():
def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None):
fruit.fruit_name = fruit_name
fruit.weight_kg = weight_kg
def get_weight_gr(fruit):
return fruit.weight_kg * 1000
Функции, объявленные внутри класса называют методами и Python предлагает специальный набор инструментов для работы с ними.
По аналогии с функциями метод можно вызвать так:
Fruit.get_weight_gr(apple)
Однако, не вдаваясь в детали, принято делать так:
apple.get_weight_gr()
Python всегда помнит от какого класса был создан тот или иной объект, и знает что яблоко apple
принадлежит к классу Fruit
. От класса Fruit
яблоко apple
получило все его методы, включая get_weight_gr
. Доступ к методам объекта получают через точку apple.get_weight_gr
, а при вызове указывают первый аргумент функции, потому что его автоматически подставляет Python.
Теперь программа выглядит так:
class Fruit():
def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None):
fruit.fruit_name = fruit_name
fruit.weight_kg = weight_kg
def get_weight_gr(fruit):
return fruit.weight_kg * 1000
apple = Fruit('Яблоко', 0.1)
orange = Fruit('Апельсин', 0.3)
print(apple.fruit_name, "| вес в граммах", apple.get_weight_gr())
print(orange.fruit_name, "| вес в граммах", orange.get_weight_gr())
Кто такой self
В Python есть стандартное название для первого атрибута метода. Вернемся к коду с фруктами и увидим там такое объявление методов:
class Fruit():
def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None):
...
def get_weight_gr(fruit):
...
В методах принято первый аргумент fruit
называть self
. Ход исполнения программы от этого не меняется, но другим программистам будет проще разобраться в коде:
class Fruit():
def __init__(self, fruit_name='неизвестный фрукт', weight_kg=None):
self.fruit_name = fruit_name
self.weight_kg = weight_kg
def get_weight_gr(self):
return self.weight_kg * 1000
apple = Fruit('Яблоко', 0.1)
orange = Fruit('Апельсин', 0.3)
print(apple.fruit_name, "| вес в граммах", apple.get_weight_gr())
print(orange.fruit_name, "| вес в граммах", orange.get_weight_gr())
Когда __init__
не нужен
Часто новый класс создается не на пустом месте, а наследуется от другого класса, предоставленного библиотекой. Например, так в Django выглядит добавление на сайт нового типа объектов — статей для блога:
from django.db.models import Model
class Article(Model):
pass
Сразу после объявления нового класса Article
указан класс-предок Model
. Из него будут позаимствованы все методы, включая готовый к использованию __init__
. Теперь внутри класса Article
будет достаточно описать те методы, что отличают его от стандартного класса Model
.
Когда аргументов много
Подобно другим функциям метод может принимать больше одного аргумента. Например:
class Fruit():
...
def get_title(self, upper_case, max_length):
title = self.fruit_name
if upper_case:
title = title.upper()
if max_length:
title = title[:max_length]
return title
Метод get_title
принимает три аргумента. Так как это не просто функция, а метод, то первым аргументом обязан быть self
. Его значение автоматически подставит Python. Остальные два аргумента upper_case
и max_length
должны быть вручную указаны при вызове метода:
apple.get_title(True, 20)