Классы: class, init, self, методы

В Python всё - объекты. Числа, строки, функции, модули. Класс описывает шаблон, по которому создаются объекты определённого типа со своими атрибутами и поведением. В этом уроке - базовый синтаксис, конструктор __init__, что такое self и как работают методы экземпляра.

Зачем нужны классы

Когда логически связанные данные и операции встречаются повсюду, лучше упаковать их в один объект:

# Без класса - данные и функции раздельно
def make_user(name, age):
    return {"name": name, "age": age}

def greet_user(user):
    return f"Привет, {user['name']}!"

def is_adult(user):
    return user['age'] >= 18

u = make_user("Alice", 30)
print(greet_user(u))

# С классом - данные и поведение вместе
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"Привет, {self.name}!"

    def is_adult(self):
        return self.age >= 18

u = User("Alice", 30)
print(u.greet())

Класс инкапсулирует данные и операции - читать и поддерживать проще.

Объявление класса

class User:
    pass

u = User()
print(type(u))   # <class '__main__.User'>

class - ключевое слово. По PEP 8 имя класса в PascalCase (User, HttpClient, OrderItem). pass нужен потому что пустой блок недопустим.

init - конструктор

__init__ вызывается автоматически при создании инстанса:

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Создан User: {name}, {age}")

u = User("Alice", 30)   # печатает "Создан User: Alice, 30"
print(u.name)           # Alice
print(u.age)            # 30

Аргументы передаются как параметры: User("Alice", 30). Первый параметр - всегда self (ссылка на создаваемый объект), его передавать не нужно.

__init__ ничего не возвращает (return None или пустой return). Это инициализация уже созданного объекта, не creation.

Реальное создание объекта делает `__new__` (вызывается до `__init__`) - это один из [dunder-методов](./29-dunder-methods.md). Но в 99% случаев переопределять `__new__` не нужно - для пользовательских нужд достаточно `__init__`. `__new__` используется только для нестандартных случаев (singletons, кастомные метаклассы, immutable типы).

self - явная ссылка на инстанс

В отличие от языков с неявным this, в Python self указывается явно:

class Counter:
    def __init__(self, start=0):
        self.value = start

    def increment(self):
        self.value += 1     # обращение к атрибуту через self

    def get(self):
        return self.value

self - просто конвенция, имя любое (но менять не стоит - линтеры предупредят, читатели запутаются).

Когда вызываешь c.increment(), Python передаёт c как первый аргумент: Counter.increment(c). Поэтому в определении метода self первый параметр.

Атрибуты экземпляра

Создаются присваиванием через self:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p.x, p.y)   # 1 2

# Можно добавлять атрибуты и снаружи (но обычно не стоит)
p.z = 3
print(p.z)

Динамическое добавление атрибутов - особенность Python. В Go и большинстве других статически типизированных языков такого нет. Использовать с осторожностью - усложняет понимание.

Атрибуты хранятся в __dict__:

print(p.__dict__)   # {'x': 1, 'y': 2, 'z': 3}

Методы экземпляра

Обычные функции внутри класса:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def scale(self, factor):
        self.width *= factor
        self.height *= factor

r = Rectangle(3, 4)
print(r.area())          # 12
print(r.perimeter())     # 14
r.scale(2)
print(r.area())          # 48

r.area() это синтаксический сахар для Rectangle.area(r). Python сам передаёт r как self.

Методы с дополнительными параметрами

class Account:
    def __init__(self, balance=0):
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Недостаточно средств")
        self.balance -= amount

a = Account(100)
a.deposit(50)
print(a.balance)   # 150
a.withdraw(30)
print(a.balance)   # 120

После self идут обычные параметры. Можно с дефолтами, *args, **kwargs - всё работает.

repr для удобной отладки

По умолчанию объекты выводятся не очень информативно:

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

u = User("Alice", 30)
print(u)   # <__main__.User object at 0x7f...>

Добавь __repr__ для нормального вывода:

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age})"

u = User("Alice", 30)
print(u)         # User(name='Alice', age=30)
print(repr(u))   # User(name='Alice', age=30)

Конвенция: __repr__ должен выводить такое представление, которое (в идеале) можно было бы передать в eval() для воссоздания объекта. Если класс - просто набор полей, всю эту рутину сгенерирует @dataclass.

Объявление в теле класса vs в init

class User:
    species = "Human"        # class attribute - общий для всех

    def __init__(self, name):
        self.name = name     # instance attribute - свой у каждого

Различия:

u1 = User("Alice")
u2 = User("Bob")

# Class attribute
print(u1.species)   # Human
print(u2.species)   # Human
User.species = "Cyborg"
print(u1.species)   # Cyborg - изменение на классе видно всем

# Instance attribute
u1.name = "Alicia"
print(u1.name)      # Alicia
print(u2.name)      # Bob - не затронут

Подробно про class vs instance attributes - в следующем уроке.

Удаление атрибутов

u = User("Alice")
del u.name        # удалить атрибут
print(u.name)     # AttributeError

Редко нужно. Иногда используется для cleanup в кастомных классах.

Передача объектов

Объекты передаются по ссылке (как и всё в Python):

def rename(user):
    user.name = "Renamed"

u = User("Alice")
rename(u)
print(u.name)   # Renamed - изменился оригинал

Если хочешь не модифицировать, делай новый объект:

def with_new_name(user, new_name):
    return User(new_name)

Это уже разбирали в уроке про ссылки и изменчивость.

hasattr, getattr, setattr

Динамический доступ к атрибутам:

u = User("Alice", 30)

hasattr(u, "name")        # True
hasattr(u, "missing")     # False

getattr(u, "name")        # 'Alice'
getattr(u, "missing", "default")   # 'default'

setattr(u, "email", "a@b.c")
print(u.email)            # a@b.c

Полезно для сериализации, фабрик объектов, фреймворков. В обычном коде - редко.

Распространённые ошибки

1. Забыли self

class User:
    def __init__(name):    # ОШИБКА - забыли self
        self.name = name    # NameError - self не определён

Первый параметр всегда self. Без него Python всё равно передаст инстанс, но имя будет name, и присваивание self.name упадёт.

2. self при вызове не передавать

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value += 1

c = Counter()
c.increment()        # OK - Python передаёт c как self
Counter.increment()  # TypeError - нужен self

При вызове через инстанс self передаётся автоматически.

3. Mutable default в init

То же что в обычной функции (урок 9):

# Плохо
class Order:
    def __init__(self, items=[]):
        self.items = items   # все Order(items=[]) делят один список!

# Хорошо
class Order:
    def __init__(self, items=None):
        self.items = items if items is not None else []

Сравнение с Go и PHP

В Go нет классов, есть структуры и методы:

type User struct {
    Name string
    Age  int
}

func (u User) Greet() string {
    return "Привет, " + u.Name
}

u := User{Name: "Alice", Age: 30}
fmt.Println(u.Greet())

В PHP синтаксис ближе к Python, но с явными модификаторами доступа:

class User {
    public string $name;
    public int $age;

    public function __construct(string $name, int $age) {
        $this->name = $name;
        $this->age = $age;
    }

    public function greet(): string {
        return "Привет, {$this->name}";
    }
}

$u = new User('Alice', 30);
echo $u->greet();

В PHP это $this, в Python - self, в Go - receiver-имя на выбор. Семантика похожая.

Мини-задание

  1. Класс с базовыми операциями:
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Недостаточно средств")
        self.balance -= amount

    def __repr__(self):
        return f"BankAccount(owner={self.owner!r}, balance={self.balance})"

a = BankAccount("Alice", 100)
a.deposit(50)
a.withdraw(30)
print(a)   # BankAccount(owner='Alice', balance=120)
  1. Метод с параметрами:
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def scale(self, factor):
        self.radius *= factor

    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area())     # 78.53...
c.scale(2)
print(c.area())     # 314.15...
  1. Перепиши без класса и с классом:
# Без класса - словарь + функции
def make_book(title, author, pages=0):
    return {"title": title, "author": author, "pages": pages}

def book_summary(book):
    return f"{book['title']} by {book['author']} ({book['pages']}p)"

# С классом
class Book:
    def __init__(self, title, author, pages=0):
        self.title = title
        self.author = author
        self.pages = pages

    def summary(self):
        return f"{self.title} by {self.author} ({self.pages}p)"

    def __repr__(self):
        return f"Book({self.title!r}, {self.author!r}, {self.pages})"

b = Book("Code Complete", "Steve McConnell", 960)
print(b.summary())

Что дальше

Освоили основы классов. В следующем уроке - атрибуты класса vs инстанса, classmethod, staticmethod, property - детальный разбор разницы между методами и атрибутами разных типов.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.