Композиционный подход в объектно-ориентированном программировании. Урок 7

Методическая разработка урока
Элективный курс: Введение в объектно-ориентированное программирование на Python
Уровень: Программирование для начинающих

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

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

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

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

Рассмотрим использование композиции при программировании на Python с помощью конкретного примера.

Описание задачи

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

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

Параллелепипед

Обои клеятся только на стены, следовательно площади верхнего и нижнего прямоугольников нам не нужны. Из рисунка можно заключить, что площадь одного прямоугольника равна x * z, второго – у * z. Противоположные прямоугольники равны, значит общая площадь четырех прямоугольников будет равна S = 2xz + 2уz = 2z(x+y). Потом из этой площади надо будет вычесть общую площадь дверей и окон. Двери и окна — это прямоугольники (как вычислить их площадь должно быть понятно).

Создание классов-частей

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

Можно заметить, что фактически у нас есть три типа объектов - это объекты-окна, объекты-двери и объекты-комнаты. Получается три класса. Окна и двери являются частями помещения, а значит могут создаваться внутри класса «комнаты». Кроме того, для данной задачи существенное значение имеют только два свойства: длина и ширина. Поэтому классы «окна» и «двери» можно объединить в один. Понятно, что если для задачи были бы важны другие свойства (например, толщина стекла, материал), то возможно следовало бы создать два класса.

class Win_Door:
     def __init__(self,x,y):
          self.square = x * y 

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

Создание класса-контейнера

Можно по-разному реализовать класс-контейнер. Есть подозрение, что многое зависит от задачи, решаемой программистом, его мастерства и вкуса. Классы-части можно вызывать в методе __init__, тем самым объекты-части будут автоматически создаваться при создании объекта-контейнера. Однако в данной задаче мы пойдем другим путем: окна и двери будут создаваться специальным для этих целей методом (будем считать, что так интересней). Также класс должен содержать метод для вычисления площади требуемых обоев (wallpapers). В конце можно добавить метод, в котором реализован вывод тех или иных данных.

class Room:
     def __init__(self,x,y,z):
          self.square = 2 * z * (x + y)
     def win_door(self, d,e, f,g, m=1,n=1):
          self.window = Win_Door(d,e)
          self.door = Win_Door(f,g)
          self.numb_w = m
          self.numb_d = n
     def wallpapers(self):
          self.wallpapers = self.square - \
               self.window.square * self.numb_w \
               - self.door.square * self.numb_d
     def printer(self):
          print ("Площадь стен комнаты равна "\
          ,str(self.square)," кв.м")
          print ("Оклеиваемая площадь равна: ", \
               str(self.wallpapers), " кв.м")

В методе __init__ создается атрибут square объекта представляющий собой площадь стен комнаты. Метод принимает три аргумента: длину, ширину и высоту помещения.

В методе win_door создаются два объекта: window и door, а также атрибуты numb_w и numb_d (в последних будут содержаться значения о количестве окон и дверей). Если при вызове данного метода в программе не будет указано количество окон и дверей, то по умолчанию будут подставлены значения равные 1.

Метод wallpapers вычисляет площадь_требуемых_обоев = площадь_комнаты — площадь_окна * количество_окон — площадь_двери * количество_дверей. В коде данная строка разбита на несколько строчек с помощью знака \ (так делают, если строка очень длинная). Также обратите внимание, как происходит обращение к свойствам square объектов-частей: указывается объект класса Room (в классе его заменяет self), далее объект-часть, и наконец, сам атрибут (свойство) объекта-части.

Метод printer просто выводит данные.

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

После того, как классы созданы, посмотрим как это все работает.

1. Создаем объект класса Room:

labor34 = Room(5,4,2)

2. Создаем в помещении labor34 окна и двери:

labor34.win_door(1.5,1.5, 2,1, 2)

Обратите внимание, что количество дверей не указано, а значит их будет ровно 1.

3. Вычисляем метры обоев:

labor34.wallpapers()

4. Просим вывести, что получилось:

labor34.printer()

В результате работы метода printer интерпретатор должен выдать что-то вроде этого:

Площадь комнаты равна 36 кв.м
Оклеиваемая площадь равна: 29.5 кв.м

Может показаться, что в программе всего один реальный объект — labor34. Однако это не так. Там есть еще объекты labor34.window и labor34.door. Чтобы в этом убедиться достаточно обратиться к их свойствам.

print(labor34.window.square)
print(labor34.door.square)

Практическая работа

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

# Объем человека   class Arm:

# Объем человека
 
class Arm: # Объем обоих рук
    def __init__(self,length, radious):
        self.arms = 2 * (length * (radious ** 2) * pi)
class Leg: # Объем обоих ног
    def __init__(self,length,radious):
        self.legs = 2 * (length * (radious ** 2) * pi)
class Head: # Объем головы
    def __init__(self,area):
        self.head = area
class Body: # Объем чрева 
    def __init__(self,area):
        self.body = area
class Man_volume: # Объем человека
    def __init__(self,length1,length2,radious1,radious2,area1,area2):
        self.arms = Arm(length1,radious1)
        self.legs = Leg(length2,radious2)
        self.head = Head(area1)
        self.body = Body(area2)
    def man_vol(self):
        self.man_vol = self.arms.arms+self.legs.legs+self.head.head+self.body.body
    def printer(self):
        print ("Volume of man is equal to",self.man_vol)
 
man = Man_volume(70,100,20,50,100,1000 "cubic santimeter")
man.man_vol()
man.printer()

Проверьте пожалуйста код и

Проверьте пожалуйста код и скажите, в чем проблема.
выдаёт ошибку:
Traceback (most recent call last):
File "F:\Python34\People.py", line 24, in
m1.mn(90,190,25,'Филипп')
File "F:\Python34\People.py", line 21, in mn
self.m = Man(k,s,y,n)
TypeError: object() takes no parameters

class Woman:
    def peop_wom(self,kg,sm,year, name):
        self.name_is = name
 
class Man:
    def peop_man(self,kg,sm,year, name):
        self.name_is = name
 
class People:
    def weight(self, kg):
        self.weigh_t = kg
    def growth(self, sm):
        self.growt_h = sm
    def age(self, year):
        self.years = year
    def out_w_g_a(self):
        print(self.weight,'kg',self.growth,'sm',self.years,'years')
    def wmn(self,k,s,y,n):
        self.w = Women(k,s,y,n)
    def mn(self,k,s,y,n):
        self.m = Man(k,s,y,n)
 
m1 = People()
m1.mn(90,190,25,'Филипп')

В ошибке интерпретатор вам

В ошибке интерпретатор вам пишет, что "объект не принимает параметры". Действительно, Man - класс без параметров. Но когда вы его используете (в последней строке), то передаете 4 аргумента. Вместо метода peop_man попробуйте использовать метод __init__

нога и рука -конечности.

нога и рука -конечности. Параметры одни и те же. Тоже самое с головой и туловищем. И получаем оптимизацию.
Area - это площадь. ЛУчше использовать слово volume.
Непонятно, как "протащили" pi.
"cubic santimeter" - где аттрибут, который это примет?

Приведенная в уроке

Приведенная в уроке композиция при запуске выдает ошибку "NameError: global name 'Win_Door' is not defined".

Выше класса Room пропиши

Выше класса Room пропиши класс Win_door

class Win_Door:
     def __init__(self,x,y):
          self.square = x * y 

Задача такая, нужно

Задача такая, нужно высчитать, сколько радиаторов отопления нужно для отопления дома. Расчет найден в интернете и немного упрощен. Суть - на обогрев 1м2 жилья нужно 110 Вт, если в комнате больше одного окна, вводится увеличивающий коэффициент. Секция чугунного радиатора дает 120 Вт, если радиатор стальной, то больше. И все это для 1, 2х комнат дома.

##класс Radiator, это общий для радиаторов,
##приведен для демонстрации использования классов и подклассов
## s - количество секций радиатора
## m - материал, по-умолчанию чугунный
>>> class Radiator:
	def __init__(self, s, m = 'chugun', c = 'white'):
		self.section = s
		self.mat = m
		self.color = c
 
##подкласс one_rad(Radiator) - отдельно взятый радиатор, наследует
##атрибуты суперкласса и содержит функцию вычисления, сколько
##он дает тепла, также, если материал задан на стальной,
##тепла отдает больше (коэффициент 1,65)
>>> class one_rad(Radiator):
	def teplo(self):
		self.teplo_bat = self.section * 120
		if self.mat == 'stalnoy':
			self.teplo_bat = self.teplo_bat * 1.65
 
 
##класс room - описание комнаты, длина, ширина, число окон,
##функция self.potera_okon(cho) вычисляет доп. расходы на отопление,
##чем больше окон, тем больше требуется тепла.
##
## в функции def kol_bat(self, sek, mat='chugun') - 
## вычисляем кол-во радиаторов нужное для комнаты, сначала
##вычисляем площадь комнаты, заводим объект-радиатор  - 
##self.bat_in_room, затем в строке
##self.teplo_bat = self.bat_in_room.teplo()
##вызываем функцию def teplo(self) класса one_rad и следовательно
##создаем атрибут self.teplo_bat
##после этого вычисляем сколько нужно батарей с заданным кол-вом
##секций и из определенного материала
##
##в функции def skolko(self): вывод на печать для разных значений
## чисел, чтобы окончания были по-русски
>>> class room:
	def __init__(self, l, sh, cho):
		self.long = l
		self.shir = sh
		self.ch_okon = cho
		self.potera_okon(cho)
	def potera_okon(self, chn):
		if chn > 1:
			self.koef_poter = 1 + (chn * 0.5)
		else:
			self.koef_poter = 1
	def kol_bat(self, sek, mat='chugun'):
		self.s_room = self.long*self.shir
		self.bat_in_room = one_rad(sek, mat)
		self.teplo_bat = self.bat_in_room.teplo()
		self.kol_vo_bat = (self.s_room*110*self.koef_poter)/self.bat_in_room.teplo_bat
	def skolko(self):
		if round(self.kol_vo_bat,0) == 1:
			print ('nado', round(self.kol_vo_bat, 0), 'radiator')
		elif 1<round(self.kol_vo_bat,0)<5:
			print ('nado', round(self.kol_vo_bat, 0), 'radiatora')
		else:
			print ('nado', round(self.kol_vo_bat, 0), 'radiatorov')
 
##класс DOM позволяет создать объект с характеристиками одной
##или двух комнат и посчитать сколько нужно радиаторов на весь
##дом, функция-метод def skolko(self): показывает полиморфизм :)
>>> class DOM:
	def __init__(self, a,b,c,s,m='chugun', aa=0,bb=0,cc=0,ss=0,mm='chugun'):
		self.kom1 = room(a,b,c)
		self.kom2 = room(aa,bb,cc)
		self.teplo_doma(s,m,ss,mm)
	def teplo_doma(self, s,m,ss,mm):
		self.kol_rad_in_room1=self.kom1.kol_bat(s,m)
		self.kol_rad_in_room2=self.kom2.kol_bat(ss,mm)
		self.kol_rad_in_room=round(self.kom1.kol_vo_bat,0)+round(self.kom2.kol_vo_bat,0)
	def skolko(self):
		if self.kom2.long == 0:
			self.kol_kom = 1
		else:
			self.kol_kom = 2
		if self.kol_rad_in_room == 1:
			print ('na', self.kol_kom, ' kom. nuzhno', self.kol_rad_in_room, 'radiator')
		elif 1<self.kol_rad_in_room<5:
			print ('na', self.kol_kom, ' kom. nuzhno', self.kol_rad_in_room, 'radiatora')
		else:
			print ('na', self.kol_kom, ' kom. nuzhno', self.kol_rad_in_room, 'radiatorov')
 
##строим дом
>>> dom1 =DOM(4,6,3,7,'chugun', 3,4,1,7,'stalnoy')
 
## вычисляем сколько нужно всего радиаторов (обращаемся к методу
## skolko() класса DOM() )
>>> dom1.skolko()
na 2  kom. nuzhno 9.0 radiatorov
 
##из какого материала радиаторы в первой комнате
>>> dom1.kom1.bat_in_room.mat
'chugun'
 
##из какого материала радиаторы во второй комнате
>>> dom1.kom2.bat_in_room.mat
'stalnoy'
 
##сколько радиаторов надо в первую комнату (обращаемся к методу
## skolko() класса room() - полиморфизм )
>>> dom1.kom1.skolko()
nado 8.0 radiatorov
 
## сколько радиаторов нужно во вторую комнату
>>> dom1.kom2.skolko()
nado 1.0 radiator
 
## сколько Вт дает один чугун. радиатор при заданном кол-ве секций
>>> dom1.kom1.bat_in_room.teplo_bat
840
 
##тоже для стального
>>> dom1.kom2.bat_in_room.teplo_bat
1386.0
 
## сколько точно нужно радиаторов в первую комнату
>>> dom1.kom1.kol_vo_bat
7.857142857142857
 
## сколько точно нужно во вторую
>>> dom1.kom2.kol_vo_bat
0.9523809523809523

с классом DOM конечно неудобно, не смог сделать, чтобы типа
Если кол-во_комнат = 2, то CREATE 2объекта
ну то есть чтобы сколько надо комнат, столько и создаешь.

Также пытался создавать нужное кол-во объектов примерно так:

i = 0
while i < 5:
      obj[i] = name_class()
      i = i+1

ну и чтобы соответственно создалось пять объектов obj0, obj1, obj2
и т.д., но ошибку выдает((

#Решил не писать свой код, а

#Решил не писать свой код, а оптимизировать твой
#т.к.очень актуальная проблема для меня сейчас - отопление)
#=======================================================
#1.перешел от ко-ва батарей в комнате на количество секций
#т.к. вполне могла по решению случится так, что батарея имеет дофига секций
#и из-за этого программа покажет, что такой батареи много (покажет 0)
#=======================================================
#2.поудалял атрибуты и методы лишние
#=======================================================
#3.Решил проблему для расчета какого-угодно количества комнат в доме
#количество комнат дома вводится прямо в создании дома
#Для меня только осталось неясным - а как же учитывать, что не
#все стенки конкретной комнаты - наружные?
#Буду рад любой критике rapackivi@mail.ru
class Section_of_radiator:
    def __init__(self,m='chugun'):
        self.mat = m
        #self.color = c
        self.teplo_out = 120
        if self.mat == 'stalnoy':
            self.teplo_out *=1.65
 
class room:
    def __init__(self,l,sh,cho):
        self.s_room=l * sh
        if cho > 1:
            self.koef_poter = 1 +(cho * 0.5)
        else:
            self.koef_poter=1
 
    def kol_section(self,mat='chugun'):
        self.section_in_room = Section_of_radiator(mat) #создали объект
        self.kol_vo_section = (self.s_room*110*self.koef_poter/ \
                          self.section_in_room.teplo_out)
 
    def skolko(self):
        print 'neobhodimoe k-vo sekciy - ', self.kol_vo_bat
 
class House:
    def __init__ (self,q_room):#vvodim kov-vo komnat
        self.quant_room=q_room
    def inp_room_param(self):
        print 'vsego v dome', self.quant_room
        i=0
        self.summ=0
        while i<self.quant_room:
            x=input("shirina komnati №" + str(i+1)+' ')
            y=input("dlina komnati №" + str(i+1)+' ')
            z=input("k-vo okon №"+ str(i+1)+' ')
            mat=input("material batarey? ")
            self.room_in_house=room(x,y,z)                      #sozdaem komnatu #i+1
            self.rad_in_room=self.room_in_house.kol_section(mat)#privyazivaem metod
            addSumm=self.room_in_house.kol_vo_section           #vitaskivaem raschet batarey
            self.summ +=addSumm
            i+=1
 
    def out(self):
        print round(self.summ,2)

Немного переделал пример из

Немного переделал пример из урока, подклассы Win и Door оставил пустыми их со временем можно будет расширять уникальными свойствами и методами

class Hole:
            def __init__(self,x,y,n=1,c='белый'):
                        self.square = x*y
                        self.numb=n
                        self.color=c
 
class Win(Hole):
            pass
 
class Door(Hole):
            pass
 
class Room:
            def __init__(self,x,y,z,c='бежевый'):
                        self.square = 2*z*(x+y)
                        self.color=c
 
            def hole(self, x1,y1,n1, x2,y2,n2):
                        self.window = Win(x1,y1,n1)
                        self.door = Door(x2,y2,n2,'серый')
 
            def wallpapers(self):
                        self.wallpapers = self.square - \
                        self.window.square * self.window.numb- \
                        self.door.square * self.door.numb
 
            def printer(self):
                        print ("Площадь стен комнаты равна:",\
                        str(self.square),"кв.м")
                        print ("Оклеиваемая площадь равна:", \
                        str(self.wallpapers),"кв.м")
                        print('Цвет обоев  -',self.color)
                        print('Цвет окон   -',self.window.color)
                        print('Цвет дверей -',self.door.color)
 
komnata = Room(10,6,3)
komnata.hole(2,1.5,3,  2,1,1)
komnata.wallpapers()
komnata.printer()
print()

композиция

Пожалуйста, объясните откуда взялись конструкции
self.window = Win_Door(d,e)
self.door = Win_Door(f,g)
Я не могу понять что это за Win_Door.Интерпретатор пишет 'NameError: global name 'Win_Door' is not defined'

ответ

Выше класса Room пропиши класс Win_door

class Win_Door:
     def __init__(self,x,y):
          self.square = x * y 

mosquito

"Кроме того, для данной задачи существенное значение имеют только два свойства: длина и ширина. Поэтому классы «окна» и «двери» можно объединить в один."

если уже говорить о объектах то объединять их нельзя.. максимум сделать наследником какого то класса с общими параметрами.. окна и двери это две разные абстракции два разных классах =\

для вычисления площади обоев

для вычисления площади обоев это как раз таки таки это один класс

Подскажите, пожалуйста, в чем

Подскажите, пожалуйста, в чем разница между композицией и множественным наследованием.

ну, как я понимаю -

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