Классы: 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.
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-имя на выбор. Семантика похожая.
Мини-задание
- Класс с базовыми операциями:
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)
- Метод с параметрами:
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...
- Перепиши без класса и с классом:
# Без класса - словарь + функции
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 - детальный разбор разницы между методами и атрибутами разных типов.