EasyТеория7 min

Классы и объекты

class, __init__, self, @staticmethod, @classmethod, __str__/__repr__ и основы ООП

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

Создание классов

# Basic class definition
class User:
    """Represent a user in the system."""

    # Class attribute (shared by all instances)
    platform = "IT Crib"

    def __init__(self, name: str, age: int, email: str) -> None:
        """Initialize a new User instance."""
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
        self.email = email
        self.is_active = True

    def greet(self) -> str:
        """Return a greeting message."""
        return f"Привет! Я {self.name}, мне {self.age} лет."

    def deactivate(self) -> None:
        """Deactivate the user account."""
        self.is_active = False
        print(f"Аккаунт {self.name} деактивирован")

# Creating instances
user1 = User("Иван", 25, "[email protected]")
user2 = User("Мария", 30, "[email protected]")

print(user1.greet())        # Привет! Я Иван, мне 25 лет.
print(user2.name)           # Мария
print(user1.platform)       # IT Crib
print(User.platform)        # IT Crib (access via class)

# Instance attributes are independent
user1.deactivate()
print(user1.is_active)      # False
print(user2.is_active)      # True

self -- ссылка на экземпляр

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def distance_to(self, other: "Point") -> float:
        """Calculate distance to another point."""
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

    def translate(self, dx: float, dy: float) -> "Point":
        """Return a new translated point."""
        return Point(self.x + dx, self.y + dy)

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0

# self is just a convention - it's the first argument
# These are equivalent:
p1.distance_to(p2)
Point.distance_to(p1, p2)  # explicit class call

init и инициализация

class BankAccount:
    """Bank account with balance and transaction history."""

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self.owner = owner
        self._balance = balance  # convention: _ means "protected"
        self._transactions: list[tuple[str, float]] = []

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

    def withdraw(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        if amount > self._balance:
            raise ValueError("Недостаточно средств")
        self._balance -= amount
        self._transactions.append(("withdraw", amount))

    def get_balance(self) -> float:
        return self._balance

# Usage
account = BankAccount("Иван", 1000.0)
account.deposit(500)
account.withdraw(200)
print(account.get_balance())  # 1300.0

# Validation in __init__
class Temperature:
    def __init__(self, celsius: float) -> None:
        if celsius < -273.15:
            raise ValueError("Температура не может быть ниже абсолютного нуля")
        self.celsius = celsius

    @property
    def fahrenheit(self) -> float:
        return self.celsius * 9/5 + 32

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

class Dog:
    # Class attributes - shared by ALL instances
    species = "Canis familiaris"
    count = 0  # track number of instances

    def __init__(self, name: str, breed: str) -> None:
        # Instance attributes - unique to each instance
        self.name = name
        self.breed = breed
        Dog.count += 1  # modify class attribute via class name

# Class attribute is shared
d1 = Dog("Бобик", "Овчарка")
d2 = Dog("Шарик", "Лабрадор")
print(Dog.count)      # 2
print(d1.species)     # Canis familiaris
print(d2.species)     # Canis familiaris

# TRAP: mutable class attributes
class BadExample:
    items = []  # shared between ALL instances!

    def add(self, item):
        self.items.append(item)

a = BadExample()
b = BadExample()
a.add("hello")
print(b.items)  # ['hello'] - BUG! Both share the same list

# CORRECT: initialize mutable attributes in __init__
class GoodExample:
    def __init__(self):
        self.items = []  # each instance gets its own list

@staticmethod и @classmethod

@staticmethod

class MathUtils:
    """Collection of math utility functions."""

    @staticmethod
    def is_prime(n: int) -> bool:
        """Check if a number is prime."""
        if n < 2:
            return False
        for i in range(2, int(n**0.5) + 1):
            if n % i == 0:
                return False
        return True

    @staticmethod
    def gcd(a: int, b: int) -> int:
        """Calculate Greatest Common Divisor."""
        while b:
            a, b = b, a % b
        return a

    @staticmethod
    def factorial(n: int) -> int:
        """Calculate factorial iteratively."""
        result = 1
        for i in range(2, n + 1):
            result *= i
        return result

# Called via class (no instance needed)
print(MathUtils.is_prime(17))     # True
print(MathUtils.gcd(48, 18))     # 6
print(MathUtils.factorial(10))    # 3628800

# Can also call via instance (but class is preferred)
m = MathUtils()
print(m.is_prime(7))  # True

@classmethod

import json
from datetime import datetime

class User:
    """User with alternative constructors."""

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age
        self.created_at = datetime.now()

    @classmethod
    def from_dict(cls, data: dict) -> "User":
        """Create User from a dictionary."""
        return cls(data["name"], data["age"])

    @classmethod
    def from_json(cls, json_str: str) -> "User":
        """Create User from a JSON string."""
        data = json.loads(json_str)
        return cls(data["name"], data["age"])

    @classmethod
    def from_birth_year(cls, name: str, birth_year: int) -> "User":
        """Create User from birth year."""
        age = datetime.now().year - birth_year
        return cls(name, age)

# Alternative constructors
user1 = User("Иван", 25)
user2 = User.from_dict({"name": "Мария", "age": 30})
user3 = User.from_json('{"name": "Пётр", "age": 28}')
user4 = User.from_birth_year("Анна", 2001)

print(user2.name)  # Мария
print(user3.name)  # Пётр
print(user4.age)   # 25 (in 2026)

# classmethod and inheritance
class Admin(User):
    def __init__(self, name: str, age: int) -> None:
        super().__init__(name, age)
        self.role = "admin"

# cls refers to Admin, not User!
admin = Admin.from_dict({"name": "Суперадмин", "age": 35})
print(type(admin))  # <class 'Admin'>
print(admin.role)   # admin

str и repr

class Product:
    """Product with string representations."""

    def __init__(self, name: str, price: float, quantity: int = 0) -> None:
        self.name = name
        self.price = price
        self.quantity = quantity

    def __repr__(self) -> str:
        """Unambiguous representation for developers (debugging)."""
        return f"Product({self.name!r}, {self.price!r}, quantity={self.quantity!r})"

    def __str__(self) -> str:
        """Readable representation for users."""
        return f"{self.name}: {self.price:,.2f} руб. (в наличии: {self.quantity})"

p = Product("Python Book", 1500.0, 10)

# __str__ is used by print() and str()
print(p)       # Python Book: 1,500.00 руб. (в наличии: 10)
print(str(p))  # same

# __repr__ is used in REPL, containers, and debugging
print(repr(p))      # Product('Python Book', 1500.0, quantity=10)
print([p])          # [Product('Python Book', 1500.0, quantity=10)]
print(f"{p!r}")     # Product('Python Book', 1500.0, quantity=10)

# If only __repr__ is defined, it's used for both
class Simple:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f"Simple({self.value!r})"

s = Simple(42)
print(s)       # Simple(42) (__repr__ used as fallback for __str__)
print(repr(s)) # Simple(42)

Правила для repr и str

class Coordinate:
    def __init__(self, lat: float, lon: float) -> None:
        self.lat = lat
        self.lon = lon

    def __repr__(self) -> str:
        # Goal: eval(repr(obj)) should recreate the object
        return f"Coordinate({self.lat}, {self.lon})"

    def __str__(self) -> str:
        # Goal: human-friendly output
        lat_dir = "N" if self.lat >= 0 else "S"
        lon_dir = "E" if self.lon >= 0 else "W"
        return f"{abs(self.lat):.4f}°{lat_dir}, {abs(self.lon):.4f}°{lon_dir}"

moscow = Coordinate(55.7558, 37.6173)
print(str(moscow))    # 55.7558°N, 37.6173°E
print(repr(moscow))   # Coordinate(55.7558, 37.6173)

# You can recreate the object from repr (ideal)
moscow_copy = eval(repr(moscow))
print(moscow_copy)    # 55.7558°N, 37.6173°E

Соглашения об именовании

class MyClass:
    def __init__(self) -> None:
        self.public = "доступно всем"           # public attribute
        self._protected = "конвенция: не трогай" # protected (convention)
        self.__private = "name mangling"          # private (name mangling)

obj = MyClass()
print(obj.public)       # 'доступно всем'
print(obj._protected)   # 'конвенция: не трогай' (still accessible!)
# print(obj.__private)  # AttributeError!
print(obj._MyClass__private)  # 'name mangling' (name mangling)

# Name mangling is NOT security - it's to avoid name clashes in inheritance
class Parent:
    def __init__(self):
        self.__value = "parent"

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__value = "child"  # doesn't override Parent's __value

c = Child()
print(c._Parent__value)  # 'parent'
print(c._Child__value)   # 'child'

Практический пример: TaskManager

from datetime import datetime
from enum import Enum

class Priority(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task:
    """Represent a task in a project."""

    _next_id = 1  # class-level counter

    def __init__(self, title: str, priority: Priority = Priority.MEDIUM) -> None:
        self.id = Task._next_id
        Task._next_id += 1
        self.title = title
        self.priority = priority
        self.created_at = datetime.now()
        self.completed = False

    @classmethod
    def urgent(cls, title: str) -> "Task":
        """Create an urgent task."""
        return cls(title, Priority.CRITICAL)

    def complete(self) -> None:
        self.completed = True

    def __repr__(self) -> str:
        status = "done" if self.completed else "pending"
        return f"Task(#{self.id}, {self.title!r}, {self.priority.name}, {status})"

    def __str__(self) -> str:
        icon = "x" if self.completed else " "
        return f"[{icon}] #{self.id} {self.title} ({self.priority.name})"

# Usage
tasks = [
    Task("Написать тесты"),
    Task("Исправить баг", Priority.HIGH),
    Task.urgent("Падение продакшена"),
]

tasks[0].complete()

for task in sorted(tasks, key=lambda t: t.priority.value, reverse=True):
    print(task)
# [ ] #3 Падение продакшена (CRITICAL)
# [ ] #2 Исправить баг (HIGH)
# [x] #1 Написать тесты (MEDIUM)

Итоги

  • class определяет шаблон для создания объектов
  • __init__ -- инициализатор (не конструктор), self -- ссылка на экземпляр
  • Атрибуты класса разделяются, атрибуты экземпляра уникальны
  • @staticmethod -- обычная функция в пространстве класса
  • @classmethod -- получает cls, используется для альтернативных конструкторов
  • __repr__ -- для разработчиков, __str__ -- для пользователей
  • _protected -- конвенция, __private -- name mangling
  • Всегда определяйте как минимум __repr__ для отладки

Проверь себя

Что такое self в методах Python-класса?

В чём опасность изменяемых атрибутов класса (не экземпляра)?

Чем @classmethod отличается от @staticmethod?

Что делает двойное подчёркивание (__) перед именем атрибута?