📦 Репозиторий занятия 68

Урок 68. Атрибуты объекта и класса

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции174 строк

Атрибут класса vs атрибут экземпляра: где создаётся, где виден, как экранируется

less_35__class_instance_attributes/theory_01__class_attribute__vs__instance_attribute.md

Пошаговый разбор различий на классе A с атрибутом класса general и атрибутом экземпляра x: атрибут класса создаётся вне __init__ и виден одинаково через любой экземпляр и через сам класс, атрибут экземпляра — только внутри своего объекта. Ключевая ловушка раздела 4: присваивание a1.general = 999 не меняет атрибут класса, а создаёт новый атрибут экземпляра, который экранирует видимость атрибута класса именно для a1 — это лечится del a1.general. Также показаны два способа менять сам атрибут класса (через A.general и через a1.__class__.general) и объяснено, почему PEP 8 не одобряет добавление атрибутов экземпляра вне __init__.

  • class A: general = 100 (атрибут класса) вне __init__, self.x внутри __init__
  • a1.general/a2.general/A.general — все три показывают 100 одновременно
  • A.general = 500 и a1.__class__.general = 777 — два способа изменить атрибут класса сразу для всех
  • a1.general = 999 — ловушка: создаёт свой атрибут экземпляра, экранирующий класс
  • del a1.general — атрибут класса снова становится виден
  • PEP 8: атрибуты экземпляра всегда объявлять внутри класса (в __init__ или методах)
Показать начало файла (174 строк всего)
### 1. Создание атрибута класса `general` и атрибута экземпляра класса

* **Атрибут класса** создаётся ВНЕ `__init__()`
* **Атрибут экземпляра класса** создаётся ВНУТРИ `__init__()`

```python
class A:
    general = 100   # атрибут класса

    def __init__(self, x):
        self.x = x   # атрибут экземпляра


a1 = A(5)
a2 = A(10)
```

### 2. Где "виден" каждый из атрибутов?

* **Атрибут класса** виден и в класса, и в каждом экземпляре класса
* **Атрибут экземпляра класса** виден ИСКЛЮЧИТЕЛЬНО внутри ТОЛЬКО этого экземпляра

```python
# class attribute
print(a1.general)  # 100
print(a2.general)  # 100
print(A.general)   # 100

# instance attribute
print(a1.x)  # 5
print(a2.x)  # 10
```

---

### 3. Как изменить атрибут класса `general`?

* 3.1. Через имя класса:

```python
A.general = 500


print(a1.general)  # 500
print(a2.general)  # 500
```

* 3.2. Через экземпляр класса с помощью метода `__class__`:

```python
a1.__class__.general = 777

print(a1.general)  # 777
print(a2.general)  # 777
```

### 4. Что будет, если написать `a1.general = ...`

Вот важная ловушка:
* это НЕ меняет атрибут класса.
…
Проверьте себя: После `a1.general = 999`, что покажет `a2.general` и почему оно НЕ равно 999?
2
MarkdownРазбор концепции58 строк

__dict__: как объект и класс хранят свои атрибуты

less_35__class_instance_attributes/theory_02__method__dict__.md

Объясняет магический атрибут __dict__ — словарь, в котором хранятся атрибуты объекта. У экземпляра Person __dict__ содержит только его собственные данные (name, age), а у самого класса Person.__dict__ — служебный mappingproxy с методами и служебными полями (__init__, __doc__, __module__ и т.д.). Показаны два практических применения: vars(p) как эквивалент p.__dict__ для отладки и прямое изменение атрибута через p.__dict__['age'] = 31.

  • p.__dict__ — словарь атрибутов экземпляра ({'name': ..., 'age': ...})
  • Person.__dict__ — mappingproxy класса с методами и служебными полями
  • vars(p) — эквивалент p.__dict__, удобно для отладки
  • p.__dict__['age'] = 31 — динамическое изменение атрибута через словарь
Показать начало файла (58 строк всего)
## Что такое `__dict__`?

`__dict__` — это магический атрибут, возвращающий словарь, 
в котором объект хранит свои атрибуты.

Пример:

```python
from pprint import pprint

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

p = Person("Alice", 30)

# ------ Для экземпляра класса: -------
print(p.__dict__)

# {'name': 'Alice', 'age': 30}


# ------  Для самого класса: -------
pprint(Person.__dict__)

# mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,
#               '__doc__': None,
#               '__init__': <function Person.__init__ at 0x7fe1cd16a2a0>,
#               '__module__': '__main__',
#               '__weakref__': <attribute '__weakref__' of 'Person' objects>})
```

---

## Для чего используется `__dict__`?

### 1. Инспекция атрибутов

Удобно для отладки:

```python
print(vars(p))
```

`vars(p)` = `p.__dict__`.

---

### 2. Динамическое изменение атрибутов

```python
p.__dict__['age'] = 31
print(p.age)  # 31
```


Проверьте себя: Чем Person.__dict__ принципиально отличается от p.__dict__ — почему у класса это mappingproxy, а не обычный dict?
3
MarkdownРазбор концепции92 строк

@classmethod: методы, работающие с классом, а не с экземпляром

less_35__class_instance_attributes/theory_03__classmethod.md

Объясняет декоратор @classmethod — метод получает первым аргументом сам класс (cls), а не объект, и потому может работать с состоянием класса целиком. Разбирает два типовых сценария: счётчик экземпляров population (метод get_population(cls) читает атрибут класса) и альтернативный конструктор from_birthday(cls, name, birth_date), который вычисляет возраст по дате рождения и возвращает cls(name, age) — паттерн, который работает корректно и для подклассов, в отличие от жёстко прописанного имени класса.

  • @classmethod get_population(cls): return cls.population — счётчик экземпляров
  • Person.population увеличивается в __init__ на каждый новый объект
  • @classmethod from_birthday(cls, name, birth_date) — альтернативный конструктор
  • Вычисление возраста: разница годов с поправкой на текущий день/месяц
  • Вопрос на закрепление: почему нельзя жёстко писать Person(...) вместо cls(...)
Показать начало файла (92 строк всего)
## Что такое `@classmethod`?

`@classmethod` — это декоратор, который превращает метод класса в «метод класса», а не объекта.

Главное отличие от обычного метода:
* **первым аргументом получает сам класс** (`cls`), а не объект (`self`).

То есть метод может работать с атрибутами и состоянием **класса**, а не конкретного экземпляра.

---

## Пример: `population` — счётчик созданных экземпляров класса `Person`.

```python
class Person:
    population = 0

    def __init__(self, name):
        self.name = name
        Person.population += 1

    @classmethod
    def get_population(cls):
        return cls.population

    
print(Person.get_population())  # 0
p = Person('Anna')
print(Person.get_population())  # 1
```

---

## Когда необходимо использовать `@classmethod`?

### 1. Когда нужно создать альтернативный конструктор

Наиболее чАстая причина использования.

```python
from datetime import datetime, date

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

    @classmethod
    def from_birthday(cls, name: str, birth_date: datetime):
        """Создание объекта Person по дате рождения (datetime)"""
        today = date.today()
        birthday = birth_date.date()   # Берём только дату

        age = today.year - birthday.year
        if (today.month, today.day) < (birthday.month, birthday.day):
            age -= 1

        return cls(name, age)

    def __repr__(self):
…
Проверьте себя: Почему в from_birthday используется `return cls(name, age)`, а не `return Person(name, age)` — в чём разница, если у Person появится подкласс?
4
MarkdownРазбор концепции57 строк

@staticmethod: функция внутри класса без self и cls

less_35__class_instance_attributes/theory_04__staticmethod.md

Объясняет @staticmethod как способ поместить обычную функцию внутрь класса логически, без доступа к состоянию объекта или класса — метод не получает ни self, ни cls и не может их менять. На примере Person.get_10_days_before_now() показано, что статический метод можно вызывать и через класс, и через экземпляр одинаково. В конце — сравнительная таблица трёх типов методов: staticmethod, classmethod и обычный метод — по тому, что они получают и когда уместны.

  • @staticmethod get_10_days_before_now() — не принимает self/cls
  • Person.get_10_days_before_now() и p.get_10_days_before_now() — оба варианта вызова работают
  • Назначение: утилитные функции, логически связанные с классом, но не зависящие от него
  • Сравнительная таблица: staticmethod / classmethod / обычный метод
Показать начало файла (57 строк всего)
### Что такое `@staticmethod`?

`@staticmethod` — это декоратор в Python, который превращает метод класса  
в обычную функцию, помещённую внутрь класса.   

Такой метод:

* не получает `self` (экземпляр)
* не получает `cls` (сам класс)
* не может изменять состояние объекта или класса
* используется для вспомогательных функций, логически связанных с классом, но не зависящих от него

---

#### Пример

```python
from datetime import datetime, timedelta

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

    @staticmethod
    def get_10_days_before_now():
        """Получить дату на 10 дней меньше текущей"""
        return datetime.now().date() - timedelta(days=10)


p = Person("Ivan", 25)

# Методы можно вызвать и с помощью класса, и с помощью экземпляра
print(Person.get_10_days_before_now())
print(p.get_10_days_before_now())
```


---

## Зачем нужен `@staticmethod`?

Используется, когда:

* метод логически относится к классу, но не работает с его атрибутами
* нужно сгруппировать функции внутри класса для удобства

---

## Отличия от других типов методов

| Тип метода      | Получает | Доступ к        | Когда использовать   |
| --------------- | -------- | --------------- | -------------------- |
| `@staticmethod` | —        | нет             | утилитные функции    |
| `@classmethod`  | `cls`    | атрибуты класса | фабрики, настройки   |
| обычный метод   | `self`   | объект          | работа с экземпляром |
Проверьте себя: Почему get_10_days_before_now() можно вызвать и как Person.get_10_days_before_now(), и как p.get_10_days_before_now(), хотя внутри метода нет ни self, ни cls?
5
MarkdownРазбор концепции106 строк

__str__ и __repr__: строковое представление для пользователя и для разработчика

less_35__class_instance_attributes/theory_05__str__and__repr__.md

Разбирает разницу между __repr__ (однозначное, техническое представление для разработчика, используется repr() и интерактивной консолью) и __str__ (дружелюбное представление для пользователя, используется print()/str()). Показано правило приоритета: если __str__ отсутствует, print(obj) использует __repr__; если оба метода есть, print берёт __str__, а сама консоль/repr() — __repr__. Финальный пример объясняет, зачем нужны оба метода одновременно: список объектов points печатается через __repr__ каждого элемента, даже если у класса определён красивый __str__ — без __repr__ список печатает служебные <...object at 0x...>.

  • __repr__(self) — техническое представление, repr(obj) и консоль
  • __str__(self) — читаемое представление, str(obj) и print(obj)
  • Правило: без __str__ используется __repr__; при обоих print берёт __str__
  • print(points) — список печатает элементы через __repr__ каждого, а не через __str__
  • Без __repr__: [<__main__.Point object at 0x...>, ...] вместо [Point(2, 5), Point(3, 4)]
Показать начало файла (106 строк всего)
Методы `__str__` и `__repr__` — это специальные (магические) методы в Python,  
которые определяют, как объект представляется в виде строки.

### `__repr__(self)`

**Назначение:** выдать однозначное, точное строковое представление объекта, предназначенное для **разработчиков**.

* Должен возвращать строку, которая по возможности может быть использована для воссоздания объекта (идеал, но не обязательно).
* Используется функцией `repr(obj)` и когда объект выводится в интерактивной консоли.
* Если `__str__` отсутствует, `print(obj)` тоже будет использовать `__repr__`.

**Пример:**

```python
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})"


x = Point(1, 2)
print(x)

# Point(x=1, y=2)
```

### `__str__(self)`

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

* Используется функцией `str(obj)` и командой `print(obj)`.
* Можно сделать более дружелюбным и неформальным, чем `__repr__`.

**Пример:**

```python
class Point:
    def __str__(self):
        return f"({self.x}, {this.y})"
```

### Взаимодействие

Если у класса **нет** `__str__`, Python использует `__repr__`.
Если у класса есть оба метода:

* `print(obj)` → `__str__`
* `obj` в интерпретаторе → `__repr__`

### Пример класса с обоими методами

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

…
Проверьте себя: Почему print(points) для списка объектов Point выводит представление через __repr__ каждого элемента, даже если у класса определён __str__?
6
MarkdownРазбор концепции67 строк

Два способа вызвать метод: a.method() и A.method(a)

less_35__class_instance_attributes/theory_06__two_ways_to_call_method.md

Показывает, что привычный вызов a.method() — это синтаксический сахар над A.method(a): интерпретатор сам находит method в классе A и автоматически подставляет объект a как self. Явный вызов A.method(a) делает ту же подстановку вручную, без магии — оба варианта приводят к одинаковому исполнению кода, различается только форма записи.

  • class A: __init__(self, name), method(self) — печатает self
  • a.method() — интерпретатор ищет method в классе A и подставляет a как self
  • A.method(a) — та же подстановка self, но вручную
  • Итоговая таблица: кто задаёт self в каждом из двух синтаксисов
Показать начало файла (67 строк всего)
## Два способа вызова метода

У нас есть класс `A`:
* с атрибутом `name`
* и методом `method()`

```python
class A:
    def __init__(self, name):
        self.name = name
        
    def method(self):
        print("Вызван method() у объекта", self)
```

---

### 1. Вызов через объект (стандартный)

```python
a = A()
a.method()
```

**Что происходит на самом деле?**

Когда мы вызываем `a.method()`, интерпретатор автоматически:

1. ищет метод `method` в классе `A`,
2. обнаруживает, что это **метод экземпляра класса A**, ожидающий первый параметр `self`,
3. **подставляет объект `a` в качестве первого аргумента**.

То есть:

```
a.method()  →  A.method(a)
```

Таким образом, `a.method()` — это просто синтаксический сахар.

В реальности выполняется:

### 2. Вызов метода через класс

```python
A.method(a)
```

Здесь мы вручную вызываете функцию `method`, описанную внутри класса `A`,
и в качестве аргумента явно передаём `self`, в качестве экземпляра класса.

То есть здесь нет автоматической подстановки — всё делается вручную.

Таким образом, об способа `a.method()` и `A.method(a)` — одно и то же действие.  
Разница — только в форме записи.

---

### Резюме

…
Проверьте себя: Что произойдёт, если вызвать A.method() без аргумента a — почему это не сработает так же, как a.method()?