Python: Курс продвинутого Программирования
Часть вторая
Александр Ольшевски

Оформление обложки https://stabledifffusion.com

© Александр Ольшевски, 2025

ISBN 978-5-0067-9690-4 (т. 2)

ISBN 978-5-0067-9441-2

Создано в интеллектуальной издательской системе Ridero

Глава 1: Наследование – Построение Иерархий и Расширение Функциональности

В первой части нашего учебника мы познакомились с основами Python, включая переменные, типы данных, структуры данных (списки, кортежи, словари, множества), функции, работу с файлами, обработку исключений и базовые концепции объектно-ориентированного программирования (ООП).

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

1.1 Что такое наследование?

1.1 Что такое наследование?

Сущность наследования в объектно-ориентированном программировании

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

Базовые термины и понятия

Родительский класс (базовый класс, суперкласс) – существующий класс, чьи свойства и поведение наследуются.

Дочерний класс (производный класс, подкласс) – новый класс, который наследует от родительского класса.

Иерархия наследования – структура отношений «родитель-потомок» между классами.

Концептуальная модель наследования

Наследование реализует отношение «is-a» (является). Если класс B наследует от класса A, это означает, что «B является A». Например:

Кошка является животным (Cat is an Animal)

Круг является фигурой (Circle is a Shape)

Менеджер является сотрудником (Manager is an Employee)

Эта семантическая связь – ключевой критерий правильного применения наследования.

Практическая необходимость наследования

1. Устранение дублирования кода

Без наследования нам пришлось бы копировать одинаковые методы и атрибуты в разные классы, что нарушает принцип DRY (Don’t Repeat Yourself).

2. Создание иерархий понятий

Наследование позволяет моделировать сложные предметные области, отражая естественные иерархии: Transport → Vehicle → Car → ElectricCar.

3. Полиморфизм и единообразие интерфейсов

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

4. Постепенное усложнение функциональности

Можно создавать простые базовые классы и постепенно добавлять функциональность в производных классах.

Базовый пример наследования в Python

Рассмотрим простейший пример, демонстрирующий сущность наследования:

python

class Animal:

«„„Родительский класс Животное““»

def __init__ (self, name):

self.name = name

def speak (self):

return «Издает звук»

def move (self):

return f"{self.name} двигается»

class Cat (Animal): # Наследование от Animal

«„„Дочерний класс Кошка““»

def speak (self): # Переопределение метода

return «Мяу!»

def purr (self): # Новый метод

return «Муррр…»

# Использование

animal = Animal («Неизвестное животное»)

print (animal. speak ()) # Издает звук

cat = Cat («Барсик»)

print (cat. speak ()) # Мяу! (переопределенный метод)

print(cat.move ()) # Барсик двигается (унаследованный метод)

print (cat. purr ()) # Муррр… (новый метод)

Что именно наследуется?

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

Все методы родительского класса (как обычные, так и статические, классовые)

Все атрибуты класса (но не экземпляра!)

Специальные методы (init, str и другие)

Возможность переопределения любого унаследованного метода

Типы наследования

1. Простое наследование

Один дочерний класс наследует от одного родительского класса (как в примере выше).

2. Многоуровневое наследование

Цепочка наследования: класс C наследует от B, который наследует от A.

python

class A:

pass

class B (A):

pass

class C (B):

pass

3. Множественное наследование

Класс наследует от нескольких родительских классов одновременно.

python

class Flyer:

def fly (self):

return «Летит»

class Swimmer:

def swim (self):

return «Плывет»

class Duck (Flyer, Swimmer):

pass

duck = Duck ()

print (duck. fly ()) # Летит

print (duck. swim ()) # Плывет

4. Иерархическое наследование

Несколько классов наследуют от одного родительского класса.

python

class Shape:

pass

class Circle (Shape):

pass

class Square (Shape):

pass

class Triangle (Shape):

pass

Принцип подстановки Барбары Лисков

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

Нарушение этого принципа – признак неправильно спроектированной иерархии наследования.

Когда следует использовать наследование?

Когда существует четкое отношение «is-a»

Когда нужно расширить функциональность существующего класса

Когда несколько классов имеют общую функциональность

Когда нужно обеспечить полиморфное поведение

Когда наследование не подходит?

Для простого повторного использования кода без семантической связи

Для реализации отношения «has-a» (имеет) – вместо наследования используйте композицию

Когда дочерний класс не является полноценным подтипом родительского

Преимущества наследования

Повторное использование кода – избегаем дублирования

Расширяемость – легко добавлять новую функциональность

Организация кода – четкая структура и иерархия

Полиморфизм – единообразие работы с разными типами объектов

Потенциальные pitfalls (подводные камни)

Чрезмерное усложнение – слишком глубокие иерархии сложны для понимания

Жесткая связность – изменения в родительском классе влияют на всех потомков

Нарушение инкапсуляции – дочерние классы зависят от реализации родителя

Неправильное использование – применение наследования там, где нужна композиция

Выводы: Наследование – это механизм, который позволяет новому классу (называемому дочерним, производным или подклассом) наследовать атрибуты и методы другого класса (называемого родительским, базовым или суперклассом). Дочерний класс получает все атрибуты и методы родительского класса, что позволяет:

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

Создавать иерархии: Организовывать классы в логические структуры, отражающие отношения «является» (is-a). Например, «собака является животным», «автомобиль является транспортным средством».

Расширять функциональность: Добавлять новые атрибуты и методы в дочерние классы, не изменяя родительский класс.

Переопределять поведение: Изменять реализацию унаследованных методов в дочерних классах, чтобы адаптировать их поведение.

1.2 Синтаксис наследования в Python

1.2 Синтаксис наследования в Python

Базовый синтаксис объявления наследования

Синтаксис наследования в Python отличается лаконичностью и простотой. Для указания родительского класса достаточно перечислить его в круглых скобках после имени дочернего класса. Если родительских классов несколько, они разделяются запятыми.

Объявление простого наследования

Простое наследование предполагает наличие одного родительского класса. Синтаксис демонстрирует минималистичный подход Python к реализации ООП-концепций.

Синтаксис переопределения методов

Переопределение методов – фундаментальная возможность наследования. Дочерний класс может полностью заменить реализацию метода родительского класса, сохраняя его сигнатуру и семантику.

Конструктор наследования

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

Множественное наследование

Python поддерживает множественное наследование, позволяя классу наследовать от нескольких родительских классов одновременно. Синтаксис остается единообразным, но семантика усложняется due to необходимости разрешения конфликтов имен.

Порядок разрешения методов (MRO)

Механизм MRO (Method Resolution Order) определяет порядок поиска методов в иерархии наследования. Python использует алгоритм C3 linearization для обеспечения предсказуемого и consistent порядка разрешения методов при множественном наследовании.

Специальные методы и наследование

Наследование распространяется на все методы класса, включая специальные методы (magic methods). Дочерние классы могут переопределять специальные методы родительского класса, обеспечивая customized поведение.

Доступ к родительским методам

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

Абстрактные базовые классы

Синтаксис включает поддержку абстрактных методов и классов через модуль abc. Абстрактные базовые классы определяют интерфейс, который должны реализовать дочерние классы.

Декораторы и наследование

Декораторы классов и методов взаимодействуют с механизмом наследования. Некоторые декораторы влияют на поведение наследования, изменяя порядок разрешения методов или добавляя метаинформацию.

Метаклассы и наследование

Наследование метаклассов follows отдельным правилам. Когда класс наследует от класса с метаклассом, Python обеспечивает consistency в создании классов-потомков.

Проверка наследования

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

Особенности синтаксиса

Синтаксис наследования в Python отличается от многих других языков программирования отсутствием ключевых слов типа «extends» или «implements». Вместо этого используется минималистичный подход с указанием родительских классов в скобках.

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

Наследование в Python указывается в скобках при определении дочернего класса:

python

# Родительский класс (или базовый класс)

class Vehicle:

def __init__ (self, brand, model):

self.brand = brand

self.model = model

print (f"Vehicle initialized: {self.brand} {self.model}»)

def move (self):

print(f"{self.brand} {self.model} is moving.»)

# Дочерний класс, наследующий от Vehicle

class Car (Vehicle):

def __init__ (self, brand, model, num_doors):

# Важно: Явный вызов конструктора родительского класса

super ().__init__ (brand, model)

self.num_doors = num_doors

print (f"Car initialized with {self.num_doors} doors.»)

def honk (self):

print («Beep beep!»)

# Создаем объект дочернего класса

my_car = Car («Toyota», «Camry», 4)

# Вывод при создании:

# Vehicle initialized: Toyota Camry

# Car initialized with 4 doors.

# Доступ к унаследованным атрибутам и методам

print (f"Brand: {my_car.brand}») # Атрибут из Vehicle

my_car.move () # Метод из Vehicle

my_car. honk () # Собственный метод Car

print (f"Number of doors: {my_car.num_doors}») # Собственный атрибут Car

1.2.1 Использование super ()

Функция super () используется для обращения к методам родительского класса. Это особенно важно при переопределении конструктора (__init__) или других методов, чтобы сохранить функциональность родительского класса.

super ().__init__ (аргументы_родителя): Вызывает конструктор родительского класса.

super().method_name (аргументы): Вызывает любой другой метод родительского класса.

1.3 Переопределение методов (Method Overriding)

Дочерний класс может предоставить свою собственную реализацию метода, который уже существует в родительском классе. Это позволяет изменять поведение унаследованных функций.

python

class Animal:

def __init__ (self, name):

self.name = name

def speak (self):

# Этот метод должен быть реализован в дочерних классах

# Можно либо оставить его пустым, либо вызвать ошибку, если он не реализован

# raise NotImplementedError («Subclass must implement abstract method»)

print(f"{self.name} makes a generic sound.»)

class Dog (Animal):

def speak (self): # Переопределяем метод speak

print («Woof!»)

class Cat (Animal):

def speak (self): # Переопределяем метод speak

print («Meow!»)

generic_animal = Animal («Generic Creature»)

dog = Dog («Buddy»)

cat = Cat («Whiskers»)

generic_animal. speak () # Вывод: Generic Creature makes a generic sound.

dog. speak () # Вывод: Woof! (переопределенная версия)

cat. speak () # Вывод: Meow! (переопределенная версия)

1.3.1 Расширение методов родителя с помощью super ()

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

python

class Employee:

def __init__ (self, name, employee_id):

self.name = name

self. employee_id = employee_id

def work (self):

print(f"{self.name} (ID: {self. employee_id}) is working.»)

class Manager (Employee):

def __init__ (self, name, employee_id, department):

# Сначала инициализируем родительские атрибуты

super ().__init__ (name, employee_id)

# Затем добавляем собственный атрибут

self.department = department

def manage (self): # Собственный метод Manager

print(f"{self.name} is managing the {self.department} department.»)

def work (self): # Переопределяем метод work

super ().work () # Вызываем родительский метод work

self.manage () # Добавляем специфичную для Manager работу

# Создание объектов

emp = Employee («John Doe», «E12345»)

mgr = Manager («Jane Smith», «M98765», «Sales»)

emp. work () # Вывод: John Doe (ID: E12345) is working.

mgr. work () # Вывод: Jane Smith (ID: M98765) is working.

# Jane Smith is managing the Sales department.

1.4 Добавление атрибутов и методов в дочерний класс

Дочерний класс может иметь свои собственные атрибуты и методы, которые не существуют в родительском классе.

python

class Vehicle:

def __init__ (self, brand, model):

self.brand = brand

self.model = model

def move (self):

print(f"{self.brand} {self.model} is moving.»)

class Car (Vehicle):

def __init__ (self, brand, model, num_doors):

super ().__init__ (brand, model)

self.num_doors = num_doors # Новый атрибут

def honk (self): # Новый метод

print («Beep beep!»)

# Создание объекта

my_car = Car («Ford», «Focus», 4)

# Доступ к унаследованным атрибутам и методам

print(my_car.brand) # Вывод: Ford

my_car.move () # Вывод: Ford Focus is moving.

# Доступ к новым атрибутам и методам

print(my_car.num_doors) # Вывод: 4

my_car. honk () # Вывод: Beep beep!

1.5 Множественное наследование

Python поддерживает множественное наследование, когда класс может наследовать от нескольких родительских классов.

python

class Flyer:

def fly (self):

print («Flying…»)

class Swimmer:

def swim (self):

print («Swimming…»)

# DuckFlyer наследует методы fly () и swim ()

class DuckFlyer (Flyer, Swimmer):

def quack (self):

print («Quack!»)

duck = DuckFlyer ()

duck. fly () # Метод из Flyer. Вывод: Flying…

duck. swim () # Метод из Swimmer. Вывод: Swimming…

duck. quack () # Собственный метод. Вывод: Quack!

1.5.1 Проблема ромбовидного наследования и MRO

Множественное наследование может привести к «проблеме ромба», когда класс наследует от двух классов, имеющих общего предка. Python решает эту проблему с помощью Method Resolution Order (MRO) – порядка, в котором Python ищет методы в иерархии. MRO определяет, в каком порядке будут вызываться методы при множественном наследовании.

python

class A:

def process (self): print («Processing in A»)

class B (A):

def process (self): print («Processing in B»); super().process ()

class C (A):

def process (self): print («Processing in C»); super().process ()

class D (B, C): # Наследует от B и C, оба из которых наследуют от A

def process (self): print («Processing in D»); super().process ()

obj_d = D ()

obj_d.process ()

# Порядок выполнения (MRO):

# Processing in D

# Processing in B

# Processing in C

# Processing in A

# Можно посмотреть MRO:

# print (D.__mro__)

# или

# print(D.mro ())

1.6 Проверка типов: isinstance () и issubclass ()

isinstance (object, classinfo): Проверяет, является ли объект экземпляром указанного класса или любого из его подклассов.

python

print (isinstance (duck, DuckFlyer)) # True

print (isinstance (duck, Flyer)) # True (так как DuckFlyer наследует от Flyer)

print (isinstance (duck, Swimmer)) # True (так как DuckFlyer наследует от Swimmer)

print (isinstance (duck, Animal)) # False (если Animal нет в иерархии)

issubclass (class, classinfo): Проверяет, является ли один класс подклассом другого.

python

print (issubclass (DuckFlyer, Flyer)) # True

print (issubclass (DuckFlyer, Swimmer)) # True

print (issubclass (Flyer, DuckFlyer)) # False

1.7 Наследование и Полиморфизм

Наследование тесно связано с полиморфизмом. Когда мы имеем иерархию классов, и вызываем один и тот же метод для объектов разных классов (например, speak () для Dog и Cat), каждый объект реагирует по-своему.

python

def make_animal_speak (animal):

# Функция принимает любой объект, у которого есть метод speak ().

# Она не заботится о точном типе объекта, только о наличии метода.

print(f"{animal.name} says: {animal. speak ()}»)

class Animal:

def __init__ (self, name):

self.name = name

def speak (self):

return «Generic animal sound.»

class Dog (Animal):

def speak (self):

return «Woof!»

class Cat (Animal):

def speak (self):

return «Meow!»

animals = [Dog («Buddy»), Cat («Whiskers»), Animal («Unknown»)]

for animal in animals:

make_animal_speak (animal)

# Вывод:

# Buddy says: Woof!

# Whiskers says: Meow!

# Unknown says: Generic animal sound.

Это пример полиморфизма, когда функция make_animal_speak работает с различными типами животных, вызывая их специфичные реализации метода speak ().

Резюме главы:

В этой главе мы начали углубленное изучение ООП, сосредоточившись на наследовании:

Поняли, как создавать дочерние классы, наследуя от родительских, и как использовать super ().

Изучили переопределение методов и его использование для расширения функциональности.

Рассмотрели добавление собственных атрибутов и методов в дочерние классы.

Познакомились с множественным наследованием и проблемой ромба (MRO).

Научились использовать isinstance () и issubclass () для проверки типов и иерархий.

Увидели, как наследование является основой для полиморфизма.

Наследование – мощный инструмент для построения структурированного и повторно используемого кода. В следующей главе мы продолжим развивать наши знания ООП, погружаясь в абстракцию и другие важные концепции.

Глава 2: Полиморфизм, Абстракция и Специальные Методы

В прошлой главе мы заложили основу ООП, познакомившись с классами, объектами, атрибутами, методами и наследованием. Сегодня мы углубимся в другие краеугольные камни ООП: полиморфизм и абстракцию, а также изучим специальные методы, которые позволяют нашим объектам «вести себя» как стандартные типы данных Python.

2.1 Полиморфизм (Polymorphism)

Полиморфизм, как мы уже упоминали, означает «много форм». В ООП это способность объектов разных классов реагировать на один и тот же вызов метода по-разному. Это позволяет писать общий код, который может работать с объектами различных, но связанных типов.

2.1.1 «Утиная типизация» (Duck Typing)

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

Правило: «Если что-то ходит как утка и крякает как утка, то это, вероятно, утка.»

python

class Duck:

def quack (self):

print («Quack!»)

def fly (self):

print («Flap flap!»)

class Person:

def quack (self):

print («I’m quacking like a duck!») # Человек может имитировать кряканье

def fly (self):

print («I’m flying… by plane!») # Человек «летает» иначе

def make_it_quack_and_fly (thing):

«"»

Эта функция принимает любой объект, у которого есть методы quack () и fly ().

Ей неважно, какой это класс, главное – наличие нужных методов.

«"»

print (f» – - Trying with {thing.__class__.__name__} – -»)

thing. quack ()

thing. fly ()

my_duck = Duck ()

random_person = Person ()

make_it_quack_and_fly (my_duck)

# Вывод:

# – - Trying with Duck – —

# Quack!

# Flap flap!

make_it_quack_and_fly (random_person)

# Вывод:

# – - Trying with Person – —

# I’m quacking like a duck!

# I’m flying… by plane!

Благодаря утиной типизации, функция make_it_quack_and_fly работает корректно как с объектом Duck, так и с объектом Person, потому что оба объекта поддерживают необходимый интерфейс (quack () и fly ()).

2.1.2 Полиморфизм через наследование

Переопределение методов в дочерних классах – это еще одна форма полиморфизма.

python

from abc import ABC, abstractmethod # Для абстрактных классов

class Animal (ABC): # Абстрактный базовый класс

def __init__ (self, name):

self.name = name

@abstractmethod # Абстрактный метод

def speak (self):

pass

class Dog (Animal):

def speak (self): # Реализация для Dog

return «Woof!»

class Cat (Animal):

def speak (self): # Реализация для Cat

return «Meow!»

# Список объектов разных подклассов Animal

animals = [Dog («Buddy»), Cat («Whiskers»)]

for animal in animals:

# Мы вызываем один и тот же метод speak ()

# Но результат зависит от конкретного типа объекта (Dog или Cat)

print(f"{animal.name} says: {animal. speak ()}»)

# Вывод:

# Buddy says: Woof!

# Whiskers says: Meow!

2.2 Абстракция (Abstraction)

Абстракция – это процесс выделения существенных характеристик объекта и игнорирования второстепенных деталей. В ООП абстракция достигается через:

Абстрактные классы (Abstract Classes): Классы, которые не могут быть напрямую инстанцированы (созданы как объекты). Они служат как «чертежи» для других классов и определяют интерфейс (набор методов), который должны реализовать их подклассы.

Абстрактные методы (Abstract Methods): Методы, объявленные в абстрактном классе, но не имеющие реализации. Подклассы обязаны предоставить свою реализацию.

2.2.1 Использование abc для абстрактных классов

Модуль abc (Abstract Base Classes) в Python позволяет создавать абстрактные классы.

ABC: Базовый класс для создания абстрактных классов.

@abstractmethod: Декоратор для обозначения абстрактных методов.

python

from abc import ABC, abstractmethod

# Абстрактный класс Shape – определяет, что такое «фигура» в контексте нашего приложения

class Shape (ABC):

def __init__ (self, name):

self.name = name

@abstractmethod # Обязательный метод для всех производных классов

def area (self):

pass

@abstractmethod # Обязательный метод для всех производных классов

def perimeter (self):

pass

def describe (self): # Обычный метод, который могут использовать подклассы

print (f"This is a shape named {self.name}.»)

# Попытка создать экземпляр абстрактного класса приведет к ошибке

# try:

# shape = Shape («Generic»)

# except TypeError as e:

# print (e) # Выведет: Can’t instantiate abstract class Shape with abstract methods area, perimeter

# Конкретный подкласс, реализующий абстрактные методы

class Circle (Shape):

def __init__ (self, name, radius):

super ().__init__ (name)

if radius <= 0:

raise ValueError («Radius must be positive.»)

self. radius = radius

def area (self): # Реализация абстрактного метода

return 3.14159 * (self. radius ** 2)

def perimeter (self): # Реализация абстрактного метода

return 2 * 3.14159 * self. radius

# Создаем экземпляр конкретного подкласса

my_circle = Circle («MyCircle», 5)

my_circle.describe () # Вызов унаследованного метода

print (f"Area: {my_circle.area ()}»)

print (f"Perimeter: {my_circle.perimeter ()}»)

Абстракция помогает создать четкий контракт для вашего кода, гарантируя, что все объекты, которые должны вести себя как «фигура», будут иметь методы area () и perimeter ().

2.3 Специальные Методы (Magic Methods / Dunder Methods)

Специальные методы, имена которых начинаются и заканчиваются двумя символами подчеркивания (например, __init__, __str__), позволяют нашим классам взаимодействовать со встроенными функциями и операторами Python. Они называются «dunder» (double underscore) методами.

2.3.1 Строковое представление: __str__ и __repr__

__str__ (self): Определяет, что выводится при использовании print () или str () для объекта. Должно возвращать «пользовательское» строковое представление.

__repr__ (self): Определяет «официальное» строковое представление объекта, которое должно быть однозначным и, по возможности, воспроизводимым (т.е., eval (repr (obj)) == obj). Часто используется для отладки. Если __str__ не определен, print () будет использовать __repr__.

python

class Point:

def __init__ (self, x, y):

self. x = x

self. y = y

def __str__ (self): # Более читаемое представление для пользователя

return f»({self. x}, {self. y})»

def __repr__ (self): # Более техническое представление для разработчика

return f"Point (x= {self. x}, y= {self. y})»

p = Point (10, 20)

print (p) # Использует __str__. Вывод: (10, 20)

print (str (p)) # Использует __str__. Вывод: (10, 20)

print (repr (p)) # Использует __repr__. Вывод: Point (x=10, y=20)

# В интерактивной сессии (REPL) без print () обычно вызывается __repr__

#>>> p

# Point (x=10, y=20)

2.3.2 Перегрузка операторов

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

__add__ (self, other): Для оператора +.

__sub__ (self, other): Для оператора -.

__mul__ (self, other): Для оператора *.

__len__ (self): Для функции len ().

__getitem__ (self, key): Для доступа по индексу [].

__eq__ (self, other): Для оператора ==.

python

class Vector:

def __init__ (self, x, y):

self. x = x

self. y = y

def __str__ (self):

return f»({self. x}, {self. y})»

def __add__ (self, other): # Перегрузка оператора сложения +

if not isinstance (other, Vector): # Проверка типа входных данных

return NotImplemented # Указываем, что операция не поддерживается для данного типа

new_x = self. x + other. x

new_y = self. y + other. y

return Vector (new_x, new_y) # Возвращаем новый объект Vector

def __len__ (self): # Перегрузка len ()

# Можно вернуть, например, количество элементов или что-то другое

# Здесь вернем как бы «размер» вектора, но это скорее пример

return 2 # У нас всегда 2 компоненты

v1 = Vector (2, 3)

v2 = Vector (5, 1)

v3 = v1 + v2 # Используем перегруженный оператор +

print (f»{v1} + {v2} = {v3}») # Вывод: (2, 3) + (5, 1) = (7, 4)

print (f"Length of v1: {len (v1)}») # Используем перегруженный len (). Вывод: Length of v1: 2

# Проверка типа с помощью `isinstance`

if isinstance (v1, Vector):

print («v1 is a Vector object.»)

2.4 Абстрактные базовые классы (ABC) и @abstractmethod (Более подробно)

Мы кратко касались этого в контексте абстракции, но важно понять, как это работает на практике. Абстрактные классы служат для определения общего интерфейса, который должны реализовать все подклассы.

abc. ABC: Делает класс абстрактным.

@abstractmethod: Декоратор, который указывает, что метод является абстрактным и должен быть реализован в подклассах.

python

from abc import ABC, abstractmethod

class Shape (ABC): # Абстрактный класс

def __init__ (self, name):

self.name = name

@abstractmethod

def area (self):

«„„Возвращает площадь фигуры.““»

pass # Отсутствие реализации

@abstractmethod

def perimeter (self):

«„„Возвращает периметр фигуры.““»

pass # Отсутствие реализации

def describe (self): # Неабстрактный метод, который может быть унаследован

print (f"This is a shape named {self.name}.»)

class Square (Shape):

def __init__ (self, name, side):

super ().__init__ (name)

self.side = side

def area (self): # Реализация абстрактного метода

return self.side ** 2

def perimeter (self): # Реализация абстрактного метода

return 4 * self.side

class Rectangle (Shape):

def __init__ (self, name, width, height):

super ().__init__ (name)

self. width = width

self. height = height

def area (self): # Реализация абстрактного метода

return self. width * self. height

def perimeter (self): # Реализация абстрактного метода

return 2 * (self. width + self. height)

# Создаем объекты

my_square = Square («MySquare», 5)

my_rect = Rectangle («MyRectangle», 4, 6)

shapes = [my_square, my_rect]

for shape in shapes:

shape.describe () # Унаследованный метод

print (f» Area: {shape.area ()}»)

print (f» Perimeter: {shape.perimeter ()}»)

print (» -" * 10)

# Пример: Если подкласс не реализует все абстрактные методы

# class Triangle (Shape): # ОШИБКА, если не реализовать area () и perimeter ()

# def __init__ (self, name, base, height):

# super ().__init__ (name)

# self.base = base

# self. height = height

# def area (self): # Реализован только один метод

# return 0.5 * self.base * self. height

#

# try:

# t = Triangle («MyTriangle», 3, 4)

# except TypeError as e:

# print (e) # Can’t instantiate abstract class Triangle with abstract methods perimeter

2.5 Композиция и Агрегация (Composition vs. Aggregation)

Это два способа построения отношений «имеет» (has-a) между классами, отличающиеся от наследования («является» is-a).

Композиция: Одноклассник «состоит из» другого класса. Жизненный цикл зависимого объекта полностью контролируется родительским объектом. Если родительский объект уничтожается, зависимый объект тоже уничтожается.

python

class Engine:

def __init__ (self, horsepower):

self. horsepower = horsepower

print («Engine created.»)

def start (self):

print (f"Engine started with {self. horsepower} HP.»)

def __del__ (self): # Магический метод для финальной очистки, вызывается при удалении объекта

print («Engine destroyed.»)

class Car:

def __init__ (self, brand, model, horsepower):

self.brand = brand

self.model = model

# Car «имеет» Engine как свою часть (композиция)

self. engine = Engine (horsepower)

print (f"Car created: {self.brand} {self.model}»)

def start_car (self):

print (f"Starting the car…»)

self.engine.start ()

def __del__ (self):

print (f"Car {self.brand} {self.model} destroyed.»)

# self. engine будет уничтожен автоматически при уничтожении car

my_car = Car («Ford», «Focus», 150)

my_car.start_car ()

print (» -" * 10)

del my_car # Уничтожаем объект car

# Вывод:

# Engine created.

# Car created: Ford Focus

# Starting the car…

# Engine started with 150 HP.

# – — – — —

# Car Ford Focus destroyed.

# Engine destroyed. (Уничтожен Engine, потому что он был частью Car)

Агрегация: Класс содержит ссылку на объект другого класса, но эти объекты существуют независимо. Жизненный цикл зависимого объекта не связан с жизненным циклом родительского.

python

class Address:

def __init__ (self, street, city):

self.street = street

self.city = city

print (f"Address created: {self.street}, {self.city}»)

def __del__ (self):

print (f"Address destroyed: {self.street}, {self.city}»)

class Person:

def __init__ (self, name, address):

self.name = name

# Person «имеет» Address, но Address может существовать и отдельно (агрегация)

self.address = address

print (f"Person created: {self.name}»)

def __del__ (self):

print (f"Person destroyed: {self.name}»)

# Создаем Address отдельно

addr = Address («123 Main St», «Anytown»)

# Создаем Person, передавая Address

person = Person («Alice», addr)

print (» -" * 10)

print(f"{person.name} lives at {person.address.street}, {person.address.city}»)

# Вывод: Alice lives at 123 Main St, Anytown

# Уничтожаем person

del person

# Вывод:

# Address created: 123 Main St, Anytown

# Person created: Alice

# – — – — —

# Person destroyed: Alice

# Address destroyed: 123 Main St, Anytown (Address все еще существует, т.к. он не был частью person)

2.6 Статические методы (@staticmethod) и методы класса (@classmethod)

@staticmethod:

Не получает неявного первого аргумента (self или cls).

Является просто функцией, логически связанной с классом, но не работающей с состоянием ни класса, ни его экземпляров.

Может вызываться как через класс, так и через экземпляр.

python

class MathHelper:

@staticmethod

def add (a, b):

return a + b

print(MathHelper.add (5, 3)) # Вывод: 8

helper = MathHelper ()

print(helper.add (10, 2)) # Вывод: 12

@classmethod:

Получает неявный первый аргумент cls – ссылку на сам класс.

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

python

class Employee:

raise_percent = 1.10 # Атрибут класса

def __init__ (self, name, salary):

self.name = name

self.salary = salary

@classmethod

def from_string (cls, emp_str): # Альтернативный конструктор

# emp_str = «John-50000»

name, salary_str = emp_str. split (» -»)

salary = int (salary_str)

return cls (name, salary) # cls – это сам класс Employee

@classmethod

def get_raise_amount (cls): # Метод, работающий с атрибутом класса

return cls. raise_percent

emp_data = «Jane-60000»

emp1 = Employee.from_string (emp_data) # Создаем объект через метод класса

print(f"{emp1.name}’s salary: {emp1.salary}») # Вывод: Jane’s salary: 60000

print (f"Raise amount: {Employee.get_raise_amount ()}») # Вывод: Raise amount: 1.1

Резюме главы:

Сегодня мы углубили наше понимание ООП, рассмотрев:

Полиморфизм: Способность объектов разных классов реагировать на один и тот же вызов метода по-разному, включая «утиную типизацию» и переопределение методов.

Абстракцию: Использование абстрактных классов (abc, @abstractmethod) для определения интерфейсов и контрактов.

Специальные Методы: Магические методы (__str__, __repr__, __add__, __len__ и др.) для настройки поведения объектов и перегрузки операторов.

Композицию и Агрегацию: Способы построения отношений «имеет» между классами.

Статические Методы (@staticmethod) и Методы Класса (@classmethod): Различия и сферы применения.

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

Глава 3: Продвинутые Приемы работы с Данными – Генераторы и Декораторы

В этой главе мы рассмотрим два мощных механизма Python, которые позволяют писать более эффективный и лаконичный код: генераторы и декораторы.

3.1 Генераторы (Generators)

Генераторы – это особый тип итераторов, которые создаются с помощью функций-генераторов (использующих yield) или выражений-генераторов (аналогичных списковым выражениям, но в круглых скобках). Они позволяют создавать последовательности элементов «на лету», не храня всю последовательность в памяти одновременно. Это делает их идеальными для работы с большими объемами данных.

3.1.1 Функции-генераторы (yield)

Вместо return функция-генератор использует ключевое слово yield для возврата значения. При каждом вызове yield функция приостанавливает свое выполнение, запоминая свое состояние, и возвращает указанное значение. При следующем запросе значения (например, в цикле for) выполнение функции возобновляется с того места, где оно было приостановлено.

python

def count_up_to (n):

«„„Генерирует числа от 0 до n-1.““»

i = 0

while i <n:

print (f» -> Yielding {i}»)

yield i

i += 1

print (f» -> Resumed at {i}»)

# Создаем объект генератора

counter = count_up_to (5)

print («Starting iteration…»)

# Итерируемся по генератору

for number in counter:

print (f"Received: {number}»)

print («Iteration finished.»)

# Пример вывода:

# Starting iteration…

# -> Yielding 0

# Received: 0

# -> Resumed at 1

# -> Yielding 1

# Received: 1

# -> Resumed at 2

# -> Yielding 2

# Received: 2

Конец ознакомительного фрагмента.

Текст предоставлен ООО «Литрес».

Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.