У многих Junior Python-программистов возникают проблемы с классами. Сегодня подробно разберём применение классов в Python, а также затронем тему *args и **kwargs.
Почему эти темы трудные? На начальном уровне программирования нам не требуется строить громоздкие конструкции. Обычно мы ограничиваемся скриптами без собственных классов. Или же создаем объекты классов, не задумываясь о том, что внутри.
Сегодня научимся настраивать классы под себя, добавлять в них новый функционал.
Что такое ООП в Python простыми словами
Прежде чем писать классы, разберёмся зачем они нужны. ООП (объектно-ориентированное программирование) - подход, при котором программа строится из объектов. Объект объединяет данные и действия над ними в одном месте.
Представьте кота. У него есть данные (имя, возраст, цвет) и поведение (мяукать, спать, есть). В ООП Python мы описываем это классом - шаблоном, по которому создаются объекты-коты.
ООП в Python держится на четырёх принципах:
- Инкапсуляция - данные и методы спрятаны внутри объекта. Снаружи доступно только то, что мы разрешили.
- Наследование - один класс может перенять свойства другого. Кот и собака - оба животные, общее берём из родителя.
- Полиморфизм - объекты разных классов отвечают на один вызов по-своему. Команда "издать звук" у кота даст "мяу", у собаки "гав".
- Абстракция - прячем сложность за простым интерфейсом. Вызываем
cat.feed(), не вникая как именно внутри устроено кормление.
Зачем это нужно на практике: ООП в Python делает код структурированным, переиспользуемым и понятным. Вместо россыпи функций и переменных получаем логичные объекты, которые легко расширять и поддерживать. Особенно это заметно в больших проектах.
Дальше разберём как всё это устроено в Python: классы, объекты, self, init, наследование и продвинутые приёмы.
Классы в Python: основы ООП
Что потребуется:
- Компьютер или любая другая ЭВМ
- Редактор кода
- Python версии 3.9 и выше
- Соединение с интернетом (впрочем можно и без него)
Начнем разбираться: для чего нужен класс? Он нужен для создания объектов с определенными функциями и данными внутри. То есть класс упрощает нам жизнь. Если у нас есть функционал взаимодействия с чем-либо и для этого нужно вызывать много функций, будет проще объединить все функции в один объект.
Например, мы взаимодействуем с базой данных. Нам приходится выполнять ряд однотипных действий:
- Подключиться к базе
- Найти нужную нам таблицу
- Сделать поиск значений по каким-то параметрам
- Вывести полученные значения.
Таких действий может быть и больше. Скоро нам надоест руками перебирать все функции для работы с БД. Тут и вступают в игру классы.
Рассмотрим на примере кода:
class Person:
def __init__(self, sex: str):
self.sex = sex
self.gender = 'heterosexual'
self.age = 0
print('I was born')
def change_gender(self, preferred_gender: str):
self.gender = preferred_gender
def describe_the_action(self, action: str):
print('I do ', action)
def __del__(self):
print('I am dead')Сразу уточним, что функции в рамках класса будут называться методами, а переменные - полями. Обратите внимание - в Python принято записывать имя класса с большой буквы, а имена методов - с маленькой.
Метод __init__ - это конструктор, он автоматически вызывается при создании объекта. Как только мы создали новый объект, нам выведется ‘I was born’.
Все методы принимают обязательный элемент self - он указывает, что мы обращаемся именно к этому объекту, а не к какому-то другому.
Для пояснения self есть красочный пример со stackoverflow:

У нашей персоны будет его пол, гендер (стандартный при рождении) и возраст (изначально он будет равен 0). Дальше передаем аргументы.
Теперь немного о полях: они обозначаются обращением к self через точку и название поля.
Метод __del__ - это деконструктор, который срабатывает когда объект уничтожается (вызов del object или завершение скрипта).
Также, мы добавляем свои собственные методы, которые будут выводить что делает человек и выполнять изменение гендера персоны.
Создаем объект на основе класса и передаем его пол "male":
person = Person('male')Давайте посмотрим что выведется и попробуем изменить пол:
print('gender:', person.gender)
person.change_gender('cat')
print('gender:', person.gender)Выводы в консоль:
I was born
gender: heterosexual
gender: cat
I am dead
Тут мы видим, что все работает корректно. Предлагаю вам самостоятельно поиграться с классом персон.
Атрибуты класса и экземпляра в Python
У классов Python есть два вида атрибутов (полей), и новички часто их путают.
Атрибуты экземпляра - принадлежат конкретному объекту. Задаются обычно в init через self:
class Cat:
def __init__(self, name):
self.name = name # атрибут экземпляра
cat1 = Cat("Барсик")
cat2 = Cat("Мурка")
print(cat1.name) # Барсик
print(cat2.name) # Мурка
У каждого кота своё имя - это атрибут экземпляра.
Атрибуты класса - общие для всех объектов. Объявляются прямо в теле класса, вне методов:
class Cat:
species = "кошка" # атрибут класса, общий для всех
def __init__(self, name):
self.name = name
print(Cat.species) # кошка
print(Cat("Барсик").species) # кошка
species один на всех котов. Если поменять Cat.species, изменится у всех экземпляров сразу.
Правило простое: данные уникальные для объекта (имя, возраст) - в self через init. Данные общие для всего класса (вид, константы) - атрибут класса.
Методы классов в Python: обычные, статические и классовые
Метод - это функция внутри класса. В Python есть три вида методов, у каждого своя роль.
Обычный метод - работает с конкретным объектом, первым аргументом принимает self:
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print(f"{self.name} говорит: Мяу!")
Статический метод (@staticmethod) - не зависит от объекта и класса, просто логически связан с классом. Без self:
class Cat:
@staticmethod
def is_cat_sound(sound):
return sound.lower() in ("мяу", "meow")
print(Cat.is_cat_sound("Мяу")) # True
Классовый метод (@classmethod) - работает с самим классом, первым аргументом принимает cls. Часто используется для альтернативных способов создания объекта:
class Cat:
def __init__(self, name):
self.name = name
@classmethod
def from_string(cls, data):
return cls(data.split(",")[0])
cat = Cat.from_string("Барсик,3,рыжий")
print(cat.name) # Барсик
Отдельно стоит property - метод, который ведёт себя как атрибут. Удобно для вычисляемых значений и контроля доступа:
class Cat:
def __init__(self, birth_year):
self.birth_year = birth_year
@property
def age(self):
return 2026 - self.birth_year
cat = Cat(2020)
print(cat.age) # 6 - обращаемся как к атрибуту, без скобок
Магические методы Python (dunder-методы)
Магические методы (их ещё зовут dunder - double underscore) - специальные методы с двойными подчёркиваниями. Python вызывает их автоматически в определённых ситуациях. Один из них вы уже знаете - это init.
Самые полезные магические методы:
init - конструктор, вызывается при создании объекта (мы его уже разбирали).
str - что показывать при print() объекта. Без него увидите невнятное <__main__.Cat object at 0x...>:
class Cat:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Кот по имени {self.name}"
print(Cat("Барсик")) # Кот по имени Барсик
repr - техническое представление объекта, для отладки. Показывается в консоли и логах.
eq - как сравнивать объекты через ==:
class Cat:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
print(Cat("Барсик") == Cat("Барсик")) # True
len - что возвращает len(объект). getitem - доступ по индексу объект[i]. call - позволяет вызывать объект как функцию.
Магические методы делают ваши классы "родными" для Python - они начинают работать со встроенными функциями (print, len, ==) как обычные типы данных.
Наследование классов в Python
Итак, мы хотим создать новый класс на базе Person. Для этого после названия класса в скобках пишем класс, от которого хотим наследоваться:
class Ivan(Person):
def __init__(self, sex: str, name: str):
super().__init__(sex)
self.name = nameКласс Ivan унаследует все методы и поля, которые есть у родителя Person.
[TGBLOCK]
Также, надо сделать инициализацию класса родителя. С этим нам поможет super().__init__(sex). После чего добавляем имя для Ivan, чего не было у Person. Теперь можем создать объект "man" и провести с ним некоторые операции:
man = Ivan('male', 'Ivan')
print('gender:', man.gender)
man.change_gender('cat')
print('gender:', man.gender)Вывод в консоль:
I was born
gender: heterosexual
gender: cat
I am dead
Не забывайте, что обращаться к полям класса можно через точку:
print(man.name)Вывод в консоль:
Ivan
Теперь вы убедились, что Ivan унаследовал все методы от Person + к ним добавилось поле "name".
super() и порядок наследования в Python
При наследовании классов в Python часто нужно вызвать метод родителя. Для этого есть функция super().
Типичный случай - дочерний класс расширяет init родителя:
class Animal:
def __init__(self, name):
self.name = name
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # вызываем __init__ родителя
self.color = color # добавляем своё поле
cat = Cat("Барсик", "рыжий")
print(cat.name, cat.color) # Барсик рыжий
super().__init__(name) берёт логику из родителя, не дублируя код. Это и есть сила наследования - общее пишем один раз.
Когда классов в иерархии много, Python определяет порядок поиска методов через MRO (Method Resolution Order). Посмотреть его можно так:
print(Cat.__mro__)
# (<class 'Cat'>, <class 'Animal'>, <class 'object'>)
Python ищет метод сначала в самом классе, потом у родителей по цепочке, и в конце - в базовом object. Знание MRO спасает при множественном наследовании, когда классов-родителей несколько.
Инкапсуляция: приватные поля в Python
Инкапсуляция - сокрытие внутренних данных объекта. В Python нет строгих private как в Java, но есть соглашения.
Одно подчёркивание _name - сигнал "это внутреннее, снаружи не трогай". Технически доступ остаётся, но по договорённости лезть не стоит:
class Cat:
def __init__(self, name):
self._secret = name # внутреннее поле
Два подчёркивания __name - Python запутывает имя (name mangling), доступ снаружи усложняется:
class Cat:
def __init__(self):
self.__chip_id = 12345 # почти приватное
cat = Cat()
# print(cat.__chip_id) # AttributeError
print(cat._Cat__chip_id) # 12345 - доступ только так
Зачем это нужно: инкапсуляция защищает данные от случайных изменений и прячет детали реализации. Снаружи работаем через публичные методы и property, а внутреннюю кухню класса не трогаем. Это делает код надёжнее.
dataclasses: современный способ писать классы
Если класс нужен в основном для хранения данных, писать init вручную утомительно. С Python 3.7 есть dataclasses - они генерируют init, repr и eq автоматически.
Сравните. Обычный класс:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
То же самое через dataclass:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p) # Point(x=1, y=2) - __repr__ уже есть
print(p == Point(1, 2)) # True - __eq__ тоже
Декоратор @dataclass сам создаёт конструктор по объявленным полям. Меньше шаблонного кода, меньше ошибок. Для классов-хранилищ данных (конфиги, DTO, точки, записи) - идеальный выбор в современном Python.
Секреты *args и **kwargs
Что это за звёздочки и зачем они нужны? Начнём с *args.
Неважно какое будет название у аргумента, важен сам оператор - звёздочка. В нашем примере звёздочка сообщает Python, что если пользователь укажет больше двух аргументов, все лишние аргументы передадутся в кортеж под названием other:
def arguments(first, second, *other):
print('first:', first)
print('second:', second)
print('other:', other)
print(type(other))
arguments(1, 2, 3, 4, 5, 6, 7, 8)Давайте посмотрим что выведется в консоль:
first: 1
second: 2
other: (3, 4, 5, 6, 7, 8)
<class 'tuple'>
Первые аргументы, поступающие в функцию, имеют свои названия - first и second. Все остальные аргументы попали в кортеж (tuple). К other нужно обращаться по индексу, как и к обычному кортежу (подробнее о tuple тут).
Что будет, если мы не передадим в other никаких аргументов?
arguments(1, 2)Вывод в консоль:
first: 1
second: 2
other: ()
<class 'tuple'>
Мы просто получим пустой кортеж - никаких ошибок не будет.
Теперь разберёмся с **kwargs.
def keyword_arguments(first, second, **other):
print('first:', first)
print('second:', second)
print('other:', other)
print(type(other))
keyword_arguments(1, 2, nums=[1, 2, 3, 3], string='text', dict={'key': 'value'})У ваc может возникнуть вопрос - что за новые аргументы nums, string и dict?
Давайте запустим и посмотрим:
first: 1
second: 2
other: {'nums': [1, 2, 3, 3], 'string': 'text', 'dict': {'key': 'value'}}
<class 'dict'>
Удивительно, но other стал словарем с ключами в виде названий аргументов. Как мы видим, туда можно передавать любое количество аргументов с названием и значением - они будут доступны в виде словаря.
Если мы задаём всего 2 аргумента - словарь просто останется пустым:
keyword_arguments(1, 2)Вывод в консоль:
first: 1
second: 2
other: {}
<class 'dict'>
Как видите, ошибок нет, мы получили пустой словарь.
FAQ: классы и ООП в Python
Что такое self в Python?
self - ссылка на текущий объект внутри методов класса. Через self обращаются к атрибутам и методам конкретного экземпляра. Python передаёт self автоматически при вызове метода, в самом вызове его писать не нужно.
Зачем нужен init в Python?
init - конструктор класса, метод который вызывается автоматически при создании объекта. В нём задают начальные значения атрибутов через self. Например def __init__(self, name): self.name = name.
Чем отличается атрибут класса от атрибута экземпляра?
Атрибут экземпляра уникален для каждого объекта (задаётся через self в init). Атрибут класса общий для всех объектов (объявляется в теле класса). Имя кота - атрибут экземпляра, вид "кошка" - атрибут класса.
Что такое ООП в Python?
ООП - объектно-ориентированное программирование, подход где код строится из объектов, объединяющих данные и поведение. Держится на четырёх принципах: инкапсуляция, наследование, полиморфизм, абстракция. В Python всё является объектом.
Когда использовать args и *kwargs?
args - когда метод принимает произвольное число позиционных аргументов, *kwargs - именованных. Часто применяются при наследовании, чтобы передать аргументы родителю через super() без жёсткой привязки к их числу.
Что лучше: обычный класс или dataclass?
Если класс в основном хранит данные - берите dataclass, он экономит код (сам генерирует init, repr, eq). Если в классе много логики и методов - обычный класс гибче.
Заключение
Темы классов и аргументов не просты в освоении. Надеюсь, мне удалось пролить больше света на эти удивительные возможности Python.
Чем крупнее будут становиться проекты, тем больше вам придётся иметь дело с классами. Без них просто невозможно управлять сложными объектами с большим количеством функций.
Ну а если вы не знаете заранее какие аргументы и в каком количестве вам нужны для функции, на помощь всегда придут *args и **kwargs.
Чтобы понять и закрепить материал, я рекомендую самостоятельно поиграться с классами, с *args и **kwargs. Лучше, если вы примените их на реальном проекте, чтобы как следует разобраться и усвоить детали.