Композиция

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

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

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

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

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

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

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

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

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

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

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

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

class Room:
    def __init__(self, x, y, z):
        self.square = 2 * z * (x + y)
        self.wd = []
    def addWD(self, w, h):
        self.wd.append(WinDoor(w, h))
    def workSurface(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.addWD(1, 1) 
r1.addWD(1, 1)
r1.addWD(1, 2)
print(r1.workSurface()) # выведет 44.6

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

Приведенная выше программа имеет ряд недочетов и недоработок. Требуется исправить и доработать, согласно следующему плану.

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

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

Исправьте код так, чтобы у объектов Room были только четыре поля – width, lenght, height и wd. Площади (полная и оклеиваемая) должны вычислять лишь при необходимости путем вызова методов.

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

Разработайте интерфейс программы. Пусть она запрашивает у пользователя данные и выдает ему площадь оклеиваемой поверхности и количество необходимых рулонов.

Курс с примерами решений практических работ: android-приложение, pdf-версия.

Комментарии

Ответ на от AnnaKo

Там в коде небольшая несостыковка с именами классов WinDoor и Win_Door
В 1 части кода:
class Win_Door: # <-- здесь Win_Door
     def __init__(self, x, y):
          self.square = x * y
А во 2 части:
class Room:
    def __init__(self, x, y, z):
        self.square = 2 * z * (x + y)
        self.wd = []
    def addWD(self, w, h):
        self.wd.append(WinDoor(w, h)) # <-- а тут WinDoor
    def workSurface(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.addWD(1, 1) 
r1.addWD(1, 1)
r1.addWD(1, 2)
print(r1.workSurface()) # выведет 44.6

При запуске кода возникает следующая ошибка: E0202:An attribute defined in test line 15 hides this method (14, 5)

13...
14 def wallpapers(self):
15        self.wallpapers = self.square - self.window.square * self.numb_w - self.door.square * self.numb_d
16...

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

Ошибка E0202

Описание
Используется, когда класс определяет метод, который скрыт атрибутом экземпляра с тем же именем.

Это сообщение принадлежит проверке классов.

Примерами являются:

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

Помогите разобраться с данным вопросом. Заранее благодарен.

Ответ на от Дмитрий

Похоже на то, что нельзя определять атрибут и метод с одним и тем же именем. Случай: 

класс определяет метод, а клиент устанавливает атрибут экземпляра с тем же именем

В данном случае есть метод wallpapers() и атрибут wallpapers. Первый вызывается например так labor34.wallpapers(), второй так labor34.wallpapers (внутри метода как self.wallpapers). У меня программа интерпретируется без ошибки. Возможно зависит от среды разработки. 

И все таки, что-то не так с win_door/Win_Door. Выдает ошибку при любом регистре букв.

Traceback (most recent call last):
  File "part.py", line 17, in <module>
    kitchen.win_door(1.8, 2, 2, 2)
  File "part.py", line 5, in win_door
    self.window = win_door(c, d)
NameError: name 'win_door' is not defined 

import copy
class win_doors:
    def __init__(self, x, y):
        self.square=x*y
class room:
    def __init__(self, x,y,z):
        self.x=x
        self.y=y
        self.z=z
        self.wd=[]
    def sq(self):
        return 2 * self.z * (self.x + self.y)
    def addWD(self,w,h):
        self.wd.append(win_doors(w,h))
    def workSurface(self):
        new_square=self.sq()
        for i in self.wd:
            new_square-=i.square
        return new_square
    def w_paper(self,len,wid):
        self.sq_wp=len*wid
        return round(self.sq()/self.sq_wp,1)
print("Введиет параметры комнаты:")
MyRoom=room(float(input("Днинна - ")),float(input("Ширна - ")),float(input("Высота - ")))
for i in range(int(input("Введите совокупное количество окон и дверей:"))):
    print("Введите размеры окна или двери:")
    MyRoom.addWD(float(input("Высота:")), float(input("Ширина:")))
print("Общая площадь стен составляет {}.".format(MyRoom.sq()))
print("Площадь поклейки обоев составляет {}.".format(MyRoom.workSurface()))
 
print("Для поклейки потребуется рулонов {} обоев.".format(MyRoom.w_paper(float(input("Длинна листа обоев:")),float(input("Длинна листа обоев:")))))

Подскажите пожалуйста, почему если a = 28/9, а затем a-=a%2 выводит 2.0? и что это за приколы в питоне с раундом..round(2.5) => 2, round(3.5) => 4

Ответ на от Yurii

>>> a = 28 / 9
>>> a
3.111111111111111
>>> b = a % 2
>>> b
1.1111111111111112
>>> a - b
2.0
  1. / - обычное деление
  2. % - нахождение остатка. Видимо в Python при этом дробная часть просто не берется в расчет и добавляется к результату.
  3. Из 3.1(1) вычесть 2.1(1) получится 2.0. 

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

class Room:
    def __init__(self, x, y, z):
        self.le = x
        self.wi = y
        self.he = z
        self.wd = []
    def fsq(self):
        self.square = 2 * self.he * (self.le + self.wi)
        return self.square
    def addwd(self, w, h): 
        self.wd.append(WikDor(w, h))
    def workSurface(self):
        new_square = self.fsq()
        for i in self.wd:
            new_square -= i.wksquare
        return new_square
    def setOneWP(self, l, hg):
        self.oneR = l*hg
        self.NR = self.workSurface() / self.oneR
    # good code! ====================
        if self.NR%2: 
            if self.NR%2==0.0 or self.NR%2==1.0:
                return int(self.NR)
            else:
                return int(self.NR)+1
    #================================         
class WikDor:
    def __init__(self, x, y):
        self.wksquare = x * y
 
print('введите размеры помещения \n=========================')
myRoom = Room(float(input('длина: ')), float(input('ширина: ')) , float(input('высота: ')))
print('-------------------------')
print('введите размеры площади(одного окна или двери), которая будет отниматся от площади помещения')
myRoom.addwd(float(input('укажите ширину: ')), float(input('укажите высоту: ')))
while input('указываем еще размеры кокой то площади (окна или двери)?, "y" or "n": ') == 'y':
    myRoom.addwd(float(input('укажите ширину: ')), float(input('укажите высоту: ')))
else:
    print('-------------------------')
print('для поклейки вам необходимо ', myRoom.setOneWP(float(input('длина одного обоечного рулона: ' )), float(input('его ширина: ' ))),' рулонов обойной бумаги!')
input('press any key')

Практическая работа Благодаря недавно пройденному Вашему курce по Tkinter получилось сделать довольно удобный интерфейс.
from tkinter import *
from tkinter import messagebox as mb
 
root = Tk()
root.title("Расчет обоев")
 
class Win_Door(object):
    def __init__(self, w, h):
        self.square = w * h
 
class Room(object):
    def __init__(self, w, l, h):
        self.width = w
        self.lenght = l
        self.height = h
        self.wd = []
 
    def r_square(self):
        room_square= 2*self.height*(self.width+self.lenght)
        return room_square
 
    def add_WD(self, w, h):
        self.wd.append(Win_Door(w, h))
 
    def work_surface(self):
        new_square = self.r_square()
        for i in self.wd:
            new_square -= i.square
        return round(new_square, 1)
 
    def calc_wallpaper(self, w, l): #расчет количества трубок обоев
        print(self.work_surface())
        print(l, " ", self.height, " ", w)
        roll = round((self.work_surface()/(self.height*(l // self.height)*w)),1)
        return roll
 
class Application(Frame):
    """кно для заполнения размеров"""
    def __init__(self, master):
        super(Application, self).__init__(master)
        self.grid()
        self.create_widgets()
 
    def calcul(self):
        sel = [self.e1, self.e2, self.e3, self.e14, self.e15]
        for s in sel:
            s.config(bg="white")
        try:
            r1 = Room(float(self.e1.get()),float(self.e2.get()),float(self.e3.get()))
            print(r1.r_square())
            if self.e4.get() and self.e5.get():
                r1.add_WD(float(self.e4.get()),float(self.e5.get()))
            if self.e6.get() and self.e7.get():
                r1.add_WD(float(self.e6.get()),float(self.e7.get()))
            if self.e8.get() and self.e9.get():
                r1.add_WD(float(self.e8.get()),float(self.e9.get()))
            if self.e10.get() and self.e11.get():
                r1.add_WD(float(self.e10.get()),float(self.e11.get()))
            if self.e12.get() and self.e13.get():
                r1.add_WD(float(self.e12.get()),float(self.e13.get()))
            ws=r1.work_surface()
            wp=r1.calc_wallpaper(float(self.e14.get()),float(self.e15.get()))
            text="Площадь оклеиваемой поверхности (м3): "+str(ws)+"\nКоличество обоев (трубки): " + str(wp)
            self.t.delete(0.0, END)
            self.t.insert(0.0, text)
            print(r1.calc_wallpaper(float(self.e14.get()),float(self.e15.get())))
        except ValueError:
            mb.showinfo("Внимание", "Не заполнены данные для рассчёта")
            for s in sel:
                if not s.get():
                    s.config(bg="yellow")
 
    def create_widgets(self):
        """Создание кнопок и окошек для заполнения"""
        Label(text="Размеры комнаты (в м)", font=('bold')).grid(row=0, columnspan=3, sticky = W)
        Label(text="Ширина").grid(row=1, column=0)
        Label(text="Длина").grid(row=1, column=1)
        Label(text="Высота").grid(row=1, column=2)
        self.e1=Entry(width=10)
        self.e1.grid(row=2, column=0)
        self.e2=Entry(width=10)
        self.e2.grid(row=2, column=1)
        self.e3=Entry(width=10)
        self.e3.grid(row=2, column=2)
        Label(text="Размеры окон (в м)", font=('bold')).grid(row=3, columnspan=3, sticky = W)
        Label(text="Ширина").grid(row=4, column=1)
        Label(text="Высота").grid(row=4, column=2)
        Label(text="Ширина").grid(row=6, column=1)
        Label(text="Высота").grid(row=6, column=2)
        Label(text="Ширина").grid(row=8, column=1)
        Label(text="Высота").grid(row=8, column=2)
        Label(text="1 окно").grid(row=5, column=0)
        Label(text="2 окно").grid(row=7, column=0)
        Label(text="3 окно").grid(row=9, column=0)
        self.e4=Entry(width=10)
        self.e4.grid(row=5, column=1)
        self.e5=Entry(width=10)
        self.e5.grid(row=5, column=2)
        self.e6=Entry(width=10)
        self.e6.grid(row=7, column=1)
        self.e7=Entry(width=10)
        self.e7.grid(row=7, column=2)
        self.e8=Entry(width=10)
        self.e8.grid(row=9, column=1)
        self.e9=Entry(width=10)
        self.e9.grid(row=9, column=2)
        Label(text="Размеры дверей (в м)", font=('bold')).grid(row=10, columnspan=3, sticky = W)
        Label(text="Ширина").grid(row=11, column=1)
        Label(text="Высота").grid(row=11, column=2)
        Label(text="Ширина").grid(row=13, column=1)
        Label(text="Высота").grid(row=13, column=2)
        Label(text="1 дверь").grid(row=12, column=0)
        Label(text="2 дверь").grid(row=14, column=0)
        self.e10=Entry(width=10)
        self.e10.grid(row=12, column=1)
        self.e11=Entry(width=10)
        self.e11.grid(row=12, column=2)
        self.e12=Entry(width=10)
        self.e12.grid(row=14, column=1)
        self.e13=Entry(width=10)
        self.e13.grid(row=14, column=2)
        Label(text="Размеры трубки обоев (в м)", font=('bold')).grid(row=15, columnspan=3, sticky = W)
        Label(text="Ширина").grid(row=16, column=0)
        Label(text="Длина").grid(row=16, column=1)
        self.e14=Entry(width=10)
        self.e14.grid(row=17, column=0)
        self.e15=Entry(width=10)
        self.e15.grid(row=17, column=1)
        Button(text="Расчитать площадь и количество обоев", font=('bold'),bg="lightgrey", command=self.calcul).grid(row=18, columnspan=3, sticky = W)
        self.t=Text(width=45, height=3)
        self.t.grid(row=19, columnspan=3)
 
app=Application(root)
root.mainloop()