Как написать класс

Эксперименты будем ставить на коде, который решает очень важную и ответственную задачу — выводит на экран вес фруктов. Делает он это, разумеется, с использованием ООП. Куда же без него во "взрослых" проектах?! И пускай от объявленного класса 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)

Скопируйте код к себе, запустите его в консоли:

$ python3 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)