Классы в Python: от основ ООП до продвинутых концепций. python.. python. атрибуты класса.. python. атрибуты класса. Блог компании Нетология.. python. атрибуты класса. Блог компании Нетология. инкапсуляция.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп. ооп python.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп. ооп python. полиморфизм.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп. ооп python. полиморфизм. Программирование.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп. ооп python. полиморфизм. Программирование. создание классов.. python. атрибуты класса. Блог компании Нетология. инкапсуляция. классы в питоне. метод в питоне. наследование в python. объекты python. ооп. ооп python. полиморфизм. Программирование. создание классов. Учебный процесс в IT.

Python часто начинают осваивать с простых скриптов и функций. Пока задачи простые, этого достаточно. Но когда в коде появляются сущности, например, пользователи, книги или машины, взаимодействие с ними строится по другим принципам. 

Для этого в Python используют классы. С их помощью описывают, какие данные есть у таких сущностей (объектов) и что с ними можно делать. Это и есть объектно-ориентированный подход — программа строится вокруг объектов и их взаимодействия.

В этой статье мы разберём основы работы с классами и объектами в Python: как они устроены, как их использовать и какие концепции вокруг них стоит знать, даже если вы пока не планируете углубляться в архитектуру.

Классы в Python: от основ ООП до продвинутых концепций - 1

Игорь Сверчков

Python-разработчик в компании BST-Digital. Помогал с написанием статьи.

Что такое класс и объект (ООП простыми словами)

Класс — это описание свойств и действий для будущих объектов.

Объект (экземпляр класса) хранит данные и свойства, определённые в классе.

Класс можно представить как чертёж, а объект как дом по этому чертежу. Один и тот же чертёж позволяет построить много домов с разной отделкой и наполнением, но с общей структурой. Точно так же класс в Python задаёт общую структуру, а каждый объект хранит свои конкретные значения.

Пример на бытовом уровне: класс User описывает, какие данные есть у пользователя (имя, email) и какие действия он может выполнять. Конкретный пользователь «Катя» — это объект класса User со своим именем и своим email.

С точки зрения Python:

  • класс — это объект специального типа type, созданный с помощью ключевого слова class;

  • объект класса создаётся, когда мы «вызываем» класс как функцию.

Давайте посмотрим, как это выглядит в коде.

Создаём первый класс: синтаксис и основы

Представим простую сущность — книгу. У книги есть данные (название и автор) и одно действие — вывести краткое описание. Всё это удобно собрать в одном месте, описав книгу с помощью класса.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def describe(self):
        print(f'"{self.title}", {self.author}')

Строка class Book: объявляет новый класс с именем Book. Всё, что написано ниже с отступом, относится к этому классу — так же, как тело функции относится к def.

Метод __init__ (магический метод, или dunder method — поясню дальше) вызывается автоматически при создании нового объекта. В нём задаётся начальное состояние объекта — данные, с которыми он будет работать, то есть происходит инициализация объекта.

Параметр self — это ссылка на конкретный объект, который создаётся в данный момент. Через него мы сохраняем данные внутрь объекта.

Метод describe описывает действие, которое можно выполнить с объектом книги.

Создание объекта класса выглядит так:

book = Book("Три товарища", "Эрих Мария Ремарк")
book.describe()

В этот момент Python создаёт объект класса Book, сохраняет в нём название и автора и позволяет вызывать его методы.

На этом этапе важно лишь увидеть общую идею: класс задаёт структуру, а конкретные данные и поведение появляются у объекта. Теперь разберём эти элементы по отдельности.

Атрибуты: данные, которые хранит объект

В примере с книгой мы уже использовали атрибуты, даже если не называли их этим словом напрямую. 

Когда внутри класса мы пишем self.title или self.author, мы сохраняем данные внутри объекта:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

Здесь title и author — атрибуты объекта Book. У каждого экземпляра класса они свои:

book1 = Book("Три товарища", "Эрих Мария Ремарк")
book2 = Book("Мастер и Маргарита", "Михаил Булгаков")

print(book1.title)  # Три товарища
print(book2.title)  # Мастер и Маргарита

Хотя обе переменные book1 и book2 относятся к одному классу Book, данные внутри них разные. Это и есть основная идея: класс задаёт форму, а объект хранит конкретные значения.

Итог: всё, что записывается через self.имя, становится атрибутом объекта.

Методы: как объект работает со своими данными

Атрибуты хранят данные объекта. Методы — это функции класса, которые работают с этими данными и определяют, какие действия может выполнять объект.

Мы уже видели метод describe в примере с книгой:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def describe(self):
        print(f'"{self.title}", {self.author}')

В методах экземпляра первым параметром передаётся объект, с данными которого метод работает. Обычно его называют self. Благодаря этому метод может обращаться к данным именно этого объекта.

Создадим книгу и вызовем метод:

book = Book("Три товарища", "Эрих Мария Ремарк")
book.describe()

Вызов book.describe() работает с данными, которые хранятся внутри Book. Если создать другой объект, метод останется тем же самым, а данные будут другими:

another_book = Book("Мастер и Маргарита", "Михаил Булгаков")
another_book.describe()

Важно, что код метода не меняется — меняется объект, с которым он работает. В этом и состоит смысл методов: одно и то же действие применяется к разным объектам одного класса, используя их собственные данные.

Коротко: атрибуты — это состояние объекта, методы — операции над этим состоянием.

Три столпа ООП в Python

До этого момента мы смотрели на классы как на способ объединять данные и действия. Но в реальном коде классы связаны друг с другом и взаимодействуют между собой.

Для описания таких связей используют три базовых принципа объектно-ориентированного программирования: наследование, инкапсуляцию и полиморфизм. Ниже разберём их на простых примерах.

Наследование

Иногда в программе нужно работать с объектами, которые во многом похожи друг на друга. Например, у нас могут быть обычные книги и электронные книги. И те и другие имеют название и автора, но у электронной книги есть дополнительные свойства.

Сначала опишем обычную книгу — так же, как мы делали это раньше:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def describe(self):
        print(f'"{self.title}", {self.author}')

Этот класс описывает общую модель: у каждой книги есть название и автор, и у каждой можно вывести краткое описание.

Теперь представим, что нам нужна электронная книга. По сути, это всё та же книга, но с дополнительной информацией — например, с размером файла.

Чтобы не переписывать код заново, в Python можно создать новый класс на основе уже существующего:

class EBook(Book):
    def __init__(self, title, author, file_size):
        super().__init__(title, author)
        self.file_size = file_size

Запись class EBook(Book): означает, что класс EBook создаётся на основе класса Book. Такой класс называется дочерним, а Book — базовым.

Внутри __init__ мы сначала вызываем super().__init__(title, author). Этой строкой задаём: «сначала выполни конструктор базового класса Book и задай общие для книги данные». После этого добавляем новое поле file_size, которое есть только у электронной книги.

Теперь можно создать объект электронной книги:

ebook = EBook("Три товарища", "Эрих Мария Ремарк", 5)
ebook.describe()

Хотя метод describe был объявлен в классе Book, его можно вызывать и у объекта класса EBook. Это работает потому, что электронная книга унаследовала поведение обычной книги.

Наследование позволяет описывать общее поведение и данные в одном классе, а различия — выносить в отдельные классы, не дублируя код.

Инкапсуляция

Инкапсуляция отвечает на вопрос, как именно можно работать с данными объекта.

В Python к атрибутам объекта можно обращаться напрямую — читать их и изменять:

book.title = "Новое название"  

Язык это позволяет: в Python нет специальных ключевых слов private или protected, которые жёстко ограничивают доступ к данным. Если атрибут существует, к нему можно обратиться по имени.

На практике этого часто недостаточно. Например, у книги не может быть отрицательного количества страниц, и менять это значение нужно только по определённым правилам.
Поэтому в объектно-ориентированном коде принято отделять внешний интерфейс объекта от его внутреннего состояния.

Посмотрим на пример:

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def set_pages(self, pages):
        if pages > 0:
            self.pages = pages

Разберём этот код. 

title и author — обычные атрибуты, с ними можно работать напрямую. _pages хранит внутреннее состояние объекта — количество страниц.

set_pages — метод, через который предполагается менять это значение. Внутри метода есть проверка корректности данных.

Объект используют так:

book = Book("Три товарища", "Эрих Мария Ремарк", 300)
book.set_pages(350)

Внешний код не меняет _pages. Он вызывает метод, а объект сам отвечает за корректность своего состояния.

Подчёркивание в имени атрибута (_pages) используется, чтобы обозначить: это внутренняя часть объекта, и работать с ней предполагается через методы класса. Иногда используют два подчёркивания (__pages) — в этом случае Python дополнительно изменяет имя атрибута, чтобы его сложнее было использовать напрямую извне. Смысл остаётся тем же: отделить внутреннее устройство объекта от его внешнего интерфейса.

Полиморфизм

Мы уже видели, что разные объекты могут быть связаны между собой через наследование. Обычная книга и электронная книга — хороший пример: по сути, это всё ещё книги, но в деталях они могут вести себя по-разному.

При этом с точки зрения кода с ними удобно работать одинаково. Например, получать описание книги, не задумываясь о том, обычная она или электронная.

Сначала опишем обычную книгу:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def describe(self):
        print(f'"{self.title}", {self.author}')

Этот метод выводит базовое описание: название и автора.

Теперь создадим электронную книгу. Она по-прежнему остаётся книгой, но мы хотим, чтобы её описание выглядело иначе. Для этого достаточно переопределить метод describe:

class EBook(Book):
    def describe(self):
        print(f'Электронная книга: "{self.title}", {self.author}')

Мы не добавляем новых данных и не переписываем конструктор — меняем только поведение метода.

Создадим два объекта и вызовем у них один и тот же метод:

book = Book("Три товарища", "Эрих Мария Ремарк")
ebook = EBook("Мастер и Маргарита", "Михаил Булгаков")

book.describe()
ebook.describe()

Метод вызывается одинаково, но результат отличается, потому что объекты относятся к разным классам.

В этом суть полиморфизма: один и тот же метод используется одинаковым образом, но ведёт себя по-разному в зависимости от типа объекта.

Специальные методы: как Python взаимодействует с объектами

В Python есть методы, которые мы обычно не вызываем напрямую. Их запускает сам интерпретатор — в определённых ситуациях, когда объект используется тем или иным образом.

Такие методы называют специальными. С ними мы уже сталкивались в этой статье: метод __init__, который выполняется при создании объекта, — один из них.

Эти методы также называют магическими — потому что со стороны кажется, будто они «срабатывают сами». На самом деле за этим стоит вполне понятная логика. А в англоязычных источниках часто встречается название dunder-методы — от double underscore, так как имена этих методов начинаются и заканчиваются двумя подчёркиваниями.

Дальше разберём несколько самых распространённых специальных методов и посмотрим, как именно Python их использует.

Метод __init__

__init__ вызывается в тот момент, когда мы создаём новый объект класса. Его задача — задать начальное состояние объекта: сохранить данные, с которыми он будет дальше работать.

Представим, что мы описываем персонажа в игре. У него есть имя и количество здоровья. Когда персонаж появляется, эти значения должны быть заданы сразу:

class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

Теперь создадим персонажа:

hero = Character("Archer", 100)

В этот момент Python автоматически вызывает метод __init__. Значения "Archer" и 100 передаются в параметры name и health, а затем сохраняются внутри объекта в атрибутах self.name и self.health.

После этого объект hero уже содержит данные:

print(hero.name)    # Archer
print(hero.health)  # 100

Каждый раз, когда создаётся новый объект Character, метод __init__ выполняется заново и заполняет данные именно этого объекта, которые были переданы.

Метод __str__

После создания объекта часто возникает вопрос: как понять, что именно в нём хранится. Например, когда мы выводим объек�� в консоль или просто хотим быстро посмотреть его состояние.

Возьмём уже знакомый пример с персонажем, у которого есть имя и показатель здоровья. Если сейчас попробовать вывести объект через print (hero), Python выдаст техническую информацию о типе объекта и его адресе в памяти, вроде:

<__main__.Character object at 0x10c8f3d90>

Чтобы задать осмысленное текстовое представление объекта, в классе используют специальный метод __str__.

Добавим его в наш класс:

class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

    def __str__(self):
        return f"{self.name} (здоровье: {self.health})"

Здесь мы описали, какую строку должен вернуть объект, когда Python пытается представить его в текстовом виде.

Теперь, если снова выполнить print (hero), мы получим результат:

Archer (здоровье: 100)

Таким образом, метод __str__ позволяет один раз описать, как объект должен выглядеть в тексте, и после этого Python будет использовать это описание автоматически — в print, логах и других похожих ситуациях.

Иногда рядом с __str__ можно встретить метод __repr__. Он тоже возвращает строку, но используется в других ситуациях.

__str__ отвечает за то, как объект выглядит при обычном выводе — например, когда мы используем print.

__repr__ используется, когда Python показывает объект в служебных контекстах: в отладке, в интерактивной консоли или внутри коллекций.

Если метод __str__ не определён, Python использует __repr__ вместо него.

На практике __str__ делают коротким и удобным для чтения, а __repr__ — более точным описанием объекта.

Метод __len__

В Python есть встроенная функция len(), которая возвращает длину строки, списка или словаря. Она может работать и с объектами пользовательских классов, если мы явно зададим это поведение.

По умолчанию Python не знает, что считать длиной такого объекта, поэтому для этого в классе определяют специальный метод __len__.

Допустим, у персонажа есть инвентарь:

class Character:
    def __init__(self, name, items):
        self.name = name
        self.items = items

Логично ожидать, что len(hero) будет возвращать количество предметов в инвентаре. Чтобы это работало, добавим метод __len__:

class Character:
    def __init__(self, name, items):
        self.name = name
        self.items = items

    def __len__(self):
        return len(self.items)

Метод __len__ должен возвращать целое число. Здесь мы просто используем длину списка items.

Теперь вызов len() работает:

hero = Character("Archer", ["лук", "стрелы", "зелье"])
print(len(hero))  # 3

После __len__ логично перейти к сравнению объектов — потому что это следующий типичный сценарий взаимодействия с ними.

Метод __eq__

Иногда нам нужно понять, равны ли два объекта. Например, представим, что у двух персонажей в игре одинаковые характеристики, и мы хотим это проверить.

Если сравнить их напрямую через hero1 == hero2, Python по умолчанию считает объекты разными, даже если данные внутри одинаковые. Он сравнивает не содержимое, а то, являются ли это одни и те же объекты в памяти.

Чтобы объяснить Python, по каким признакам объекты считаются равными, в классе определяют специальн��й метод __eq__.

Например, описываем, что два персонажа считаются равными, если у них совпадают имя и здоровье:

class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

    def __eq__(self, other):
        return (
            self.name == other.name and
            self.health == other.health
        )

Теперь сравнение будет корректным:

hero1 = Character("Archer", 100)
hero2 = Character("Archer", 100)

print(hero1 == hero2) # True

Таким образом, можем проверять равенство не «по месту в памяти», а по важным для нас характеристикам.

Итог

Мы посмотрели на самые распространённые специальные методы, но в Python их десятки — каждый отвечает за отдельный сценарий взаимодействия с объектами.

Вот ещё несколько, с которыми вы наверняка столкнётесь:

  • __iter__ — позволяет проходить по объекту в цикле for;

  • __getitem__ — отвечает за доступ к элементам по индексу (obj[i]);

  • __enter__ и __exit__ — позволяют объекту работать с конструкцией with (например, как файл);

  • __add__, __sub__, __mul__ — переопределяют арифметические операции между объектами.

Полный список специальных методов есть в официальной документации.

Например, если вы делаете класс, который должен вести себя как коллекция, смотрите раздел Emulating container types. Для поддержки арифметики и операторов сравнения — Emulating numeric types.

Продвинутые концепции

Мы разобрались, как устроены классы, какие данные они хранят и как объекты ведут себя в типичных для Python ситуациях. Этого уже достаточно, чтобы использовать классы в прикладном коде.

Дальше имеет смысл хотя бы обозначить несколько инструментов, которые часто встречаются в реальных проектах. Мы не будем разбирать их подробно — это отдельные темы, каждая из которых тянет на собственную статью. Здесь важно другое: знать, что такие возможности существуют и зачем они вообще нужны, чтобы узнавать их в коде и понимать общую картину.

Ниже — краткий обзор двух таких механизмов: абстрактных классов и классов данных.

Абстрактные классы

При проектировании программы часто нужно не просто описать отдельные объекты, а задать общие правила для группы похожих классов. 

Например, в игре может быть много разных персонажей — лучник, маг, воин. Они отличаются, но у всех есть базовые свойства: имя, здоровье и умение атаковать.

Мы можем создать общий класс Character, чтобы описать, какие данные и методы должны быть у всех персонажей. Но что, если этот класс сам по себе не должен существовать? Нам нужен шаблон, от которого будут наследоваться другие, но который нельзя использовать напрямую.

Для этого в Python есть абстрактные классы.

Они задаются с помощью модуля abc (от abstract base class — «абстрактный базовый класс»). В нём есть специальный декоратор @abstractmethod, которым отмечают методы, обязательные для переопределения в наследниках.

Как это выглядит: 

from abc import ABC, abstractmethod

class Character(ABC):
    def __init__(self, name, health):
        self.name = name
        self.health = health

    @abstractmethod
    def attack(self):
        pass

Теперь, если попробовать создать Character напрямую, Python не позволит:

hero = Character("Archer", ["лук", "стрелы", "зелье"])
print(len(hero))  # 3

Но если мы создаём наследника, который реализует метод attack, всё работает:

class Archer(Character):
    def attack(self):
        print(f"{self.name} стреляет из лука!")

hero = Archer("Archer", 100)
hero.attack()
# Archer стреляет из лука!

Теперь мы получим такой результат:

hero = Character("Archer", ["лук", "стрелы", "зелье"])
print(len(hero))  # 3

Итог: с помощью абстрактных классов можно заранее задать, что обязательно должно быть в будущих классах, и не бояться, что кто-то забудет про нужный метод. Такой подход помогает держать код в порядке, особенно когда проект растёт.

Классы данных

Бывает, нам нужен класс, который просто хранит данные — без сложной логики и десятков методов. 

Например, в игре может быть множество предметов: зелья, оружие, доспехи. У каждого есть несколько свойств: название, цена, вес.

Такой класс обычно состоит только из конструктора и пары атрибутов:

class Item:
    def __init__(self, name, price, weight):
        self.name = name
        self.price = price
        self.weight = weight

Проблема в том, что подобный шаблон приходится повторять снова и снова. И чтобы этого избежать, в Python добавили декоратор @dataclass — он создаёт стандартные методы автоматически.

С его помощью тот же класс можно записать так:

from dataclasses import dataclass

@dataclass
class Item:
    name: str
    price: int
    weight: float

Теперь Python сам создаёт конструктор (__init__), методы сравнения (__eq__), представление в виде строки (__repr__) и другие полезные детали.

Класс становится короче, но полностью рабочим:

sword = Item("Меч", 1200, 3.5)
potion = Item("Зелье", 250, 0.5)

print(sword)
# Item(name='Меч', price=1200, weight=3.5)

print(sword == potion)
# False

Итог: dataclass удобен, когда нужно хранить много однотипных данных. Вместо длинных повторяющихся конструкторов вы описываете только поля — остальное Python делает сам.

Практический пример: моделируем реальную систему

Попробуем посмотреть на классы не как на синтаксис Python, а как на способ описывать реальные вещи. Возьмём простую и знакомую ситуацию — поездку на поезде.

У нас есть поезд, вагоны и пассажиры. Каждый из этих объектов что-то хранит и что-то умеет делать.

Начнём с самого простого — пассажира:

class Passenger:
    def __init__(self, name, seat):
        self.name = name
        self.seat = seat

Здесь мы просто фиксируем данные. Каждый объект Passenger — это конкретный человек с конкретным местом.

Создадим пассажира:

p1 = Passenger("Анна", 12)

Теперь у объекта p1 внутри есть свои данные:

print(p1.name)   # Анна
print(p1.seat)   # 12

Теперь добавим вагон. 

Вагон — это не просто набор данных. Он содержит пассажиров и должен уметь с ними работать.

class Carriage:
    def __init__(self, number):
        self.number = number
        self.passengers = []

Мы явно храним список пассажиров внутри вагона. Пока вагон пустой.

Добавим метод, который сажает пассажира в вагон:

class Carriage:
    def __init__(self, number):
        self.number = number
        self.passengers = []

    def add_passenger(self, passenger):
        self.passengers.append(passenger)

Посадим пассажира в вагон:

carriage = Carriage(5)
carriage.add_passenger(p1)

Теперь пассажир стал частью вагона. Это важная идея ООП: объекты могут содержать другие объекты.

Чтобы понять, что происходит внутри вагона, добавим удобный вывод:

class Carriage:
    def __init__(self, number):
        self.number = number
        self.passengers = []

    def add_passenger(self, passenger):
        self.passengers.append(passenger)

    def __str__(self):
        return f"Вагон {self.number}, пассажиров: {len(self.passengers)}"

Теперь Python сам знает, как показывать объект:

print(carriage)
# Вагон 5, пассажиров: 1

На этом этапе у нас есть простая, но рабочая модель. Эту структуру можно расширять — добавлять новые классы и правила, не переписывая уже существующий код.

Заключение

В работе с классами важно не столько знание синтаксиса, сколько понимание того, зачем они вообще нужны. Классы позволяют описывать программу не как последовательность шагов, а как модель предметной области (по сути мир программы) — с сущностями, их состоянием и правилами взаимодействия.

Именно с этой точки зрения имеет смысл смотреть на ООП в Python: как на способ структурировать код так, чтобы его было проще расширять, читать и поддерживать по мере роста проекта. Поэтому дальше стоит изучать не новые приёмы сами по себе, а то, как классы применяют в реальных системах.


Чтобы расти, нужно выйти из привычной зоны и сделать шаг к переменам. Можно изучить новое, начав с бесплатных занятий:

Или можно стать востребованным сотрудником и открыть открыть бóльшие перспективы в карьере с профессиональным обучением:

Автор: kirakirap

Источник

Rambler's Top100