Главная страница

ООП_на_Python_Учебное пособие_var5a. Учебнометодическое пособие по дисциплине Введение в компьютерные технологии Москва Физический факультет мгу имени М. В. Ломоносова 2022


Скачать 59.5 Kb.
НазваниеУчебнометодическое пособие по дисциплине Введение в компьютерные технологии Москва Физический факультет мгу имени М. В. Ломоносова 2022
Дата21.01.2023
Размер59.5 Kb.
Формат файлаdocx
Имя файлаООП_на_Python_Учебное пособие_var5a.docx
ТипУчебно-методическое пособие
#897314
страница4 из 5
1   2   3   4   5

18. Композиция
Еще одной особенностью объектно-ориентированного программирования является

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

Чтобы понять, зачем нужна композиция в программировании, проведем аналогию с
реальным миром. Большинство биологических и технических объектов состоят из более
простых частей, также являющихся объектами. Например, животное состоит из различных
органов (сердце, желудок), компьютер — из различного "железа" (процессор, память).

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

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

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

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

Можно выделить три типа объектов – окна, двери и комнаты. Получается три класса.
Окна и двери являются частями комнаты, поэтому пусть они входят в состав объекта-
помещения.

Для данной задачи существенное значение имеют только два свойства – длина и
ширина. Поэтому классы «окна» и «двери» можно объединить в один. Если бы были важны
другие свойства (например, толщина стекла, материал двери), то следовало бы для окон
создать один класс, а для дверей – другой. Пока обойдемся одним, и все что нам нужно от
него – площадь объекта:
class WinDoor:
def __init__(self, x, y):
self.square = x * y

Класс "комната" – это класс-контейнер для окон и дверей. Он должен содержать
экземпляры класса WinDoor.

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

30

добавлять вызовом специально предназначенного для этого метода, который будет
добавлять объекты-компоненты в список.
class Room:
def __init__(self, x, y, z):
self.square = 2 * z * (x + y)
self.wd = []
def add_wd(self, w, h):
self.wd.append(WinDoor(w, h))
def work_surface(self):
new_square = self.square
for i in self.wd:
new_square -= i.square
return new_square
#-----------------------------------
r1 = Room(6, 3, 2.7)
print(r1.square) # вывод: 48.6
r1.add_wd(1, 1)
r1.add_wd(1, 1)
r1.add_wd(1, 2)
print(r1.work_surface()) #вывод: 44.6

19. Статические методы
Ранее было сказано, с определенным допущением классы можно рассматривать как

модули, содержащие переменные со значениями и функции. Только здесь переменные
называются полями или свойствами, а функции – методами. Вместе поля и методы
называются атрибутами. Когда метод применяется к объекту, этот экземпляр передается в
метод в качестве первого аргумента:
class A:
def meth(self):
print('meth')

a = A()
a.meth() # для объкта a (экземпляра класса A) вызываем метод meth(a)
A.meth(a) # вызываем метод принадлежащий классу A и передаем ему экземпляр a

Т.е. a.meth() на самом деле преобразуется к A.meth(a), то есть мы идем к "модулю A"
и в его пространстве имен ищем атрибут meth. Там оказывается, что meth это функция,
принимающая один обязательный аргумент. Тогда ничего не мешает сделать так:
class A:
def meth(self):
print('meth')
A.meth(10)

В таком "модульном формате" вызова методов передавать объект-экземпляр именно
класса A совсем не обязательно. Что делать, если возникает необходимость в методе,
который не принимал бы объект данного класса в качестве аргумента? Да, мы можем
объявить метод вообще без параметров и вызывать его только через класс:
class A:
def meth():
print('meth')
a = A()
A.meth() # вызываем метод без параметров принадлежащий классу A
a.meth() # Ошибка, в метод без параметров передается аргумент self

Получается странная ситуация. Ведь meth() вызывается не только через класса, но и
через порожденные от него объекты. Однако в последнем случае всегда будет возникать
ошибка. Кроме того, может понадобиться метод с параметрами, но которому не надо
передавать экземпляр данного класса.

31

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

Статические методы в Python реализуются с помощью специального декоратора
@staticmethod:
class A:
@staticmethod
def meth():
print('meth')
A.meth() # вызываем статический метод (без параметров) принадлежащий классу A
a = A()
a.meth() # вызываем статический метод без параметров через экземпляр класса A

Вообще, если в теле метода не используется self, то есть ссылка на конкретный объект,
следует задуматься, чтобы сделать метод статическим.

Пусть у нас будет класс "Цилиндр". При создании объектов от этого класса у них
заводятся поля высота и диаметр, а также площадь поверхности. Вычисление площади
можно поместить в отдельную статическую функцию. Она вроде и относится к цилиндрам,
но, с другой стороны, само вычисление объекта не требует и может быть использовано где
угодно.
from math import pi
class Cylinder:
@staticmethod
def make_area(d, h):
circle = pi * d**2 / 4
side = pi * d * h
return circle*2 + side
def __init__(self, diameter, high):
self.dia = diameter
self.h = high
self.area = self.make_area(diameter, high)
a = Cylinder(1, 2)
print(a.area)
print(a.make_area(2, 2))

В примере вызов make_area() за пределами класса возможен в том числе через
экземпляр. При этом понятно, в данном случае свойство area самого объекта a не меняется.
Мы просто вызываем функцию, находящуюся в пространстве имен класса.

20. Примеры объектно-ориентированных программ на Python
В ООП очень важно предварительное проектирование. В общей сложности можно

выделить следующие этапы разработки объектно-ориентированной программы:
1. Формулирование задачи.
2. Определение объектов, участвующих в ее решении.
3. Проектирование классов, на основе которых будут создаваться объекты. В случае
необходимости установление между классами наследственных связей.
4. Определение ключевых для данной задачи свойств и методов объектов.
5. Создание классов, определение их полей и методов.
6. Создание объектов.
7. Решение задачи путем организации взаимодействия объектов.

32

Далее приведены примеры классов в порядке возрастания сложности. Сначала
простые классы. Далее – классы демонстриующие наследование, полиморфизм и
композицию.

• Класс рациональных дробей
Простой класс, представляющий рациональную дробь (num – числитель, den

знаменатель). Класс содержит конструктор и перегруженные методы умножения и деления
(дроби на дробь и дроби на целое число). Метод создания случайной дроби из заданного
диапазона целых чисел объявлен как статический.

Следует отметить, что в языке имеется готовый тип Fraction в модуле fractions. И
данный пример нужно рассматривать только как образец для создания собственных
классов.
from math import gcd
from random import randint

class My_Fraction:
def __init__(self, num, den):
if num != 0 and den != 0:
k = gcd(num, den) # находим НОД
self.num = num // k # числитель
self.den = den // k # знасенатель
else:
raise ValueError

@staticmethod
def generate(num_min, num_max, den_min, den_max):
return My_Fraction(randint(num_min, num_max), randint(den_min, den_max))

def __str__(self): # Метод преобразования дроби в строку
return f'{self.num}/{self.den}'

def __mul__(self, other): # Умножение дробей
if isinstance(other,My_Fraction): # перегрузка умножения на дробь
return My_Fraction(self.num * other.num, self.den * other.den)
if isinstance(other,int): # перегрузка умножения на целое число
return My_Fraction(self.num * other, self.den)
return self # для остальных типов возвращаем значение самого
объекта

def __truediv__(self, other): # Деление дробей
if isinstance(other,My_Fraction): # перегрузка деления на дробь
return My_Fraction(self.num * other.den, self.den * other.num)
if isinstance(other,int): # перегрузка деления на целое число
return My_Fraction(self.num, self.den*other)
raise TypeError # для остальных типов вызываем исключение
#----------------------------------------------------------------------
# Список из 5 случайных дробей:
a = [My_Fraction.generate(1, 9, 1, 9) for i in range(5)]
for f in a:
b = My_Fraction.generate(1, 9, 1, 9) # дробь для правого операнда
cm = f * b
print(f'{f} * {b} = {cm}') # пример умножения на дробь
cd = f / b
print(f'{f} / {b} = {cd}') # пример деления на дробь
n=randint(1, 9) # число для правого операнда
cm = f * n
print(f'{f} * {n} = {cm}') # пример умножения на число
cd = f / n
print(f'{f} / {n} = {cd}') # пример деления на число

33

• Класс «Студент»
Класс содержит имя студента full_name, номер группы group_number и список

полученных оценок progress. В программе вводится список студентов. Далее список
сортируется по имени, потом выводятся студенты, имеющие неудовлетворительные
оценки.
class Student:
def __init__(self,full_name="", group_number=0, progress=[]): # конструктор
self.full_name = full_name # имя
self.group_number = group_number # номер группы
self.progress = progress # оценки
def __str__(self): # печатаемое представление экземпляра класса
txt = 'Студент: ' + self.full_name + ' Группа: ' + self.group_number
txt += ' Оценки:'
for x in self.progress:
txt += ' ' + str(x) # добавляем список оценок
return txt
#-----------------------------------------------------------------------------
def SortParam(st): # функция определяющая атрибут для сортировки
return st.full_name
#-----------------------------------------------------------------------------
st_size = 5 # количество студенов

students = [] # создание пустого списка
for i in range(st_size): # цикл для ввода st_size студентов
print("Введите полное имя студента: ")
full_name = input() # ввод фамилии
print("Введите номер группы: ")
group_number = input() # ввод группы
n=5
print('Введите ',n,' оценок в столбик: ') # у каждого студента n оценок
progress = []
for i in range(n):
score = int(input()) # ввод оценок
progress.append(score) # добавление оценок
# создание экзепляра класса Student:
st = Student(full_name, group_number, progress)
students.append(st) # добавление экземпляра в список

print("Students list:")
for st in students: # вывод полного списка студентов
print(st)

# сортировка по фамилии, ключ сортировки определяется функцией SortParam:
students = sorted(students, key=SortParam)

print("Sorted students:")
for st in students: # вывод отсортированного списка
print(st)

print("bad students:")
n=0 # счетчик количества неуспевающих
for st in students: # вывод неуспевающих
for val in st.progress:
if val<3 : # есть плохая оценка
print(st) # выводим студента с плохой оцекой
n += 1
break
if n == 0:
print("no matches were found.")

34

• Виртуальная модель процесса обучения
Пусть необходимо разработать виртуальную модель процесса обучения. В программе

должны быть объекты-ученики, учитель, кладезь знаний.
Потребуется три класса – "учитель", "ученик", "данные". Учитель и ученик во многом

похожи, оба – люди. Значит, их классы могут принадлежать одному надклассу "человек".
Однако в контексте данной задачи у учителя и ученика вряд ли найдутся общие атрибуты.

Определим, что должны уметь объекты для решения задачи "увеличить знания":
• Ученик должен уметь брать информацию и превращать ее в свои знания.
• Учитель должен уметь учить группу учеников.
• Данные могут представлять собой список знаний. Элементы будут извлекаться по
индексу.

class Data:
def __init__(self, info): #конструктор
self.info = list(info)
def __getitem__(self, i): # перегрузка [] для извлечения элемента из Data
return self.info[i]
class Teacher:
def __init__(self): #конструктор
self.work = 0 # количество учеников
def teach(self, info, pupil): # обучение данными из info учеников pupil
for i in pupil:
i.take(info) # учим ученика i
self.work += 1 # количество учеников увеличилось на 1
class Pupil:
def __init__(self): #конструктор
self.knowledge = [] # список полученных знаний
def take(self, info): # получение знания
self.knowledge.append(info)

В класс Teacher добавлено свойство экземпляров work, чтобы подсчитывать
количество проделанной учителем работы.

Теперь посмотрим, как объекты этих классов могут взаимодействовать между собой:
lesson = Data(['class', 'object', 'inheritance', 'polymorphism', 'encapsulation'])
marIvanna = Teacher()
vasy = Pupil()
pety = Pupil()
marIvanna.teach(lesson[2], [vasy, pety]) # учить обоих знанию lesson[2]
marIvanna.teach(lesson[0], [pety]) # учить pety знанию lesson[0]
print(vasy.knowledge) # вывод: ['inheritance']
print(pety.knowledge) # вывод: ['inheritance', 'class']

• Игра-стратегиия «Солдаты и герои»
В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее

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

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

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

35

Отправляем одного из солдат следовать за первым героем и выводим их
идентификационные номера.
from random import randint
class Person:
count = 0
def __init__(self, c):
self.id = Person.count
Person.count += 1
self.command = c
class Hero(Person):
def __init__(self, c):
Person.__init__(self, c)
self.level = 1
def up_level(self):
self.level += 1
class Soldier(Person):
def __init__(self, c):
Person.__init__(self, c)
self.my_hero = None
def follow(self, hero):
self.my_hero = hero.id
h1 = Hero(1) # первый герой
h2 = Hero(2) # второй герой
army1 = [] # первая армия
army2 = [] # вторая армия
for i in range(20):
n = randint(1, 2)
if n == 1:
army1.append(Soldier(n)) # добавление солдата в первую армию
else:
army2.append(Soldier(n)) # добавление солдата во вторую армию
print(len(army1), len(army2)) # численность армий
if len(army1) > len(army2): # повышение уровня героя для команды с большей
численностью
h1.up_level()
elif len(army1) < len(army2):
h2.up_level()
army1[0].follow(h1) # первому солдату следовать за 1 героем
print(army1[0].id, h1.id) ) # номера первого солдата и первого героя

36

• Класс «Битва»
В классе Battle реализована композиция: он включает два объекта типа Soldier.

from random import randint
class Soldier: # класс описывающий одного солдата
def __init__(self,name='Noname',health = 100): # конструктор
self.name = name # задаем имя воина
self.health = health # задаем начальное здоровье
def set_name(self, name):
self.name = name # есть возможность поменять имя
def make_kick(self, enemy): # метод моделирующий атаку на солдата enemy
enemy.health -= 20 # при атаке здоровье врага уменьшаем на 20
if enemy.health<0 :
enemy.health = 0
self.health += 10 # а собственное здоровье увеличиваем на 10
print(self.name, "бьет", enemy.name) # выводим кто кого бьет
print('%s = %d' % (enemy.name, enemy.health)) # выводим состояние врага
#-----------------------------------------------------------------------------
class Battle:
def __init__(self,u1,u2): # конструктор
# композиция: класс включает двух солдат u1 и u2
self.u1 = u1
self.u2 = u2
self.result = "Сражения не было" # строка для хранения состояния сражения
def battle(self): # метод моделирующий сражение
while self.u1.health > 0 and self.u2.health > 0:
n = randint(1, 2) # определяем, кто атакует
if n == 1:
self.u1.make_kick(self.u2) # если атакует первый
else:
self.u2.make_kick(self.u1) # если атакует второй
if self.u1.health > self.u2.health:# определяем, кто победил
self.result = self.u1.name + " ПОБЕДИЛ"
elif self.u2.health > self.u1.health:
self.result = self.u2.name + " ПОБЕДИЛ"
def who_win(self): # вывод результата
print(self.result)
#-----------------------------------------------------------------------------
first = Soldier('Mr. First',140) # созаем 1 солдата с именем Mr. First и здоровьем
140
second = Soldier() # созаем 2 солдата с паметрами по умолчанию
second.set_name('Mr. Second') # меняем имя 2 солдата
b = Battle(first,second) # создаем объект Battle
b.battle() # запускаем сражение
b.who_win() # выводим итог

37

• Класс «Колода карт»
Класс DeckOfCards содержит список карт. Конструктор класса Card инициализирует

значения масти и номера из списков NumsList и MastList, которые объявлены как общие
атрибуты класса.
import time
import random;
class Card(): # класс Карта
NumsList = ["Джокер",'2','3','4','5','6','7','8','9','10',
"Валет","Дама","Король","Туз"]
MastList = ["пик","крестей","бубей","червей"]
def __init__(self, i, j):# конструктор
self.Mastb = self.MastList[i-1]; # карта
self.Num = self.NumsList[j-1]; # масть
#----------------------------------------
class DeckOfCards(): # класс Колода карт
def __init__(self): # конструктор
self.deck = [None] * 56; # список из 56 карт
k = 0;
for i in range(1, 4 + 1):
for j in range(1, 14 + 1):
self.deck[k] = Card(i, j); # очередная карта
k += 1;
def shuffle(self): # перемешивание карт
random.shuffle(self.deck);
def get(self, i): # вытаскивание i-й карты из колоды
if 0 <= i <=55 :
answer = self.deck[i].Num;
answer += " ";
answer += self.deck[i].Mastb;
else :
answer = "В колоде только 56 карт"
return answer;
#----------------------------------------
deck = DeckOfCards(); # создали колоду
deck.shuffle(); # перемешали
print('Выберите карту из колоды в 56 карт:');
n=int(input())
if n<=56 :
card = deck.get(n-1);
print('Вы взяли карту: ', card, end='.\n');
else :
print("В колоде только 56 карт")

38

• Класс «Паспорт»
Класс ForeignPassport является производным от класса Passport. Метод PrintInfo

существует в обоих классах. PassportList представляет собой список, содержащий объекты
обоих классов. Вызов метода PrintInfo для каждого элемента списка демонстрирует его
полиморфное поведение.
class Passport():
def __init__(self, first_name, last_name, country, date_of_birth,
numb_of_pasport):
self.first_name = first_name
self.last_name = last_name
self.date_of_birth = date_of_birth
self.country = country
self.numb_of_pasport = numb_of_pasport

def PrintInfo(self):
print("\nFullname: ",self.first_name, " ",self.last_name)
print("Date of birth: ",self.date_of_birth)
print("County: ",self.country)
print("Passport number: ",self.numb_of_pasport)

class ForeignPassport(Passport):
def __init__(self, first_name, last_name, country, date_of_birth,
numb_of_pasport,visa):
super().__init__(first_name, last_name, country, date_of_birth,
numb_of_pasport)
self.visa = visa

def PrintInfo(self):
super().PrintInfo()
print("Visa: ",self.visa)

PassportList=[]
request = ForeignPassport('Ivan', 'Ivanov', 'Russia', '12.03.1967', '123456789',
'USA')
PassportList.append(request)

request = Passport('Иван', 'Иванов', 'Россия', '12.03.1967', '45001432')
PassportList.append(request)

request = ForeignPassport('Peter', 'Smit', 'USA', '01.03.1990', '21435688',
'Germany')
PassportList.append(request)

for emp in PassportList:
emp.PrintInfo()

39

• Класс «Склад оргтехники»
Классы Printer, Scaner и Xerox являются производными от класса Equipment. Метод

str() перегружен только в классе Printer, для остальных используется метод из базового
класса. Метод action() перегружен для всех производных классов. Вызов этих методов для
каждого элемента списка демонстрирует их полиморфное поведение.
class Equipment:
def __init__(self, name, make, year):
self.name = name # производитель
self.make = make # модель
self.year = year # год выпуска
def action(self):
return 'Не определено'
def __str__(self):
return f'{self.name} {self.make} {self.year}'
#------------------------------------------------
class Printer(Equipment):
def __init__(self, series, name, make, year):
super().__init__(name, make, year)
self.series = series # серия
def __str__(self):
return f'{self.name} {self.series} {self.make} {self.year}'
def action(self):
return 'Печатает'
#------------------------------------------------
class Scaner(Equipment):
def __init__(self, name, make, year):
super().__init__(name, make, year)
def action(self):
return 'Сканирует'
#------------------------------------------------
class Xerox(Equipment):
def __init__(self, name, make, year):
super().__init__(name, make, year)
def action(self):
return 'Копирует'
#------------------------------------------------
sklad = []
# создаем объект сканер и добавляем
scaner = Scaner('Mustek','BearPow 1200CU', 2010)
sklad.append(scaner)
# создаем объект ксерокс и добавляем
xerox = Xerox('Xerox','Phaser 3120', 2019)
sklad.append(xerox)
# создаем объект принтер и добавляем
printer = Printer("1200",'hp', 'Laser Jet', 2018)
sklad.append(printer)
# выводим склад
print("На складе имеются:")
for x in sklad:
print(x,end=' ')
print(x.action())
# забираем со склада все принтеры
for x in sklad:
if isinstance(x,Printer):
sklad.remove(x)
# выводим склад
print("\nНа складе осталось:")
for x in sklad:
print(x,end=' ')
print(x.action())

40

• Задача трёх тел
Задача трех тел — одна из задач небесной механики, состоящая в определении

относительного движения трёх тел (материальных точек), взаимодействующих по закону
тяготения Ньютона. В любой момент времени на каждое из тел действуют силы
гравитационного притяжения остальных двух тел. Сила, с которой первое из тел
притягивает второе, выражется формулой

????????? = −?????????????????? ,

где ? — гравитационная постоянаая, ?? и ?? — массы тел, ????????? = ??????− ?????? — вектор от
первого к тела к второму, где положение тел в пространстве задается радиус векторами ??????
и ??????. Остальные силы попарного взаимодействия можно получить заменной индексов.
Итого, движение всех трел задаётся системой динамических уравнений

?????????? = ??????????+ ??????????????????? = ????????? + ??????????????????? = ????????? + ??????????.
Такая система уравнения не имеет аналитического решения в общем случае, но можно
решить её приближенно, введя малый шаг по времени ∆ и используя следующую схему
обновления положения и скорости тела в следующий момент времени
( + ∆ )???????????????????? =
( )????????? + ?( )?????????∆ , ?( + ∆ )???????????????????? = ?( )?????????+
( )?????????∆ .

Рассмотрим решение задачи трех тел в объектно ориентированном стиле. Первое,
что бросается в глаза — многие величины описываются векторами в трехмерном
пространстве. Полезно в таком случае реализовать класс трехмерных векторов и
определить для него операции сложения, вычитания, вычисления его нормы, домножения
на скаляр и другие. В приведенных ниже примерах предполагается, что в программе
определен класс `Vector3D` удовлетворяющий всем перечисленным свойствам, а
реализация такого класса оставляется чителю в качестве упражнения (смотри задачи для
самостоятельного решения).

Класс Body описывает небесное тело. В рамках нашей задачи тело задаётся его
массой, положением пространства (радиус вектор) и вектором скорости. Метод update
рассчитывает изменение положения тела в пространстве и его вектоа скорости под
действием заданной силы за время ∆ по описанной выше схеме.
1   2   3   4   5


написать администратору сайта