Домашни > Another brick in the wall


Another brick in the wall
Краен срок: 26.11.2024 18:00
Точки: 10

#### Make America Great Again! Тъй като Доналд Тръмп спечели последните избори в САЩ, решихме да потренираме с построяване на стена. Не се знае дали няма да ни се наложи и ние да браним границите си. ![Let's build a wall!](/media/resources/build-a-wall.png) #### Загрявка Напишете 5 класа, които репрезентират материали, с които може да се строи стена: * `Concrete` * `Brick` * `Stone` * `Wood` * `Steel` Всеки от класове има идентична сигнатура. При инстанциране приема един позиционен аргумент от тип `int`, който указва масата на създадения материал. Всеки от класовете има дефинирана плътност. Тя е различна за всеки материал: * `Concrete` = 2500 * `Brick` = 2000 * `Stone` = 1600 * `Wood` = 600 * `Steel` = 7700 Всяка инстанция от класовете за материал трябва да има пропърти `volume` за изчисление на обема си. Обемът е равен на масата, разделена на плътността. Очаква се резултатът да е от тип `float`. ``` brick = Brick(2000) print(brick.volume) # 1.0 ``` Лесно, нали? Brace yourselves! #### Фабрика Напишете клас `Factory`, който репрезентира фабрика за производство на материали. Класът може да се инстанцира без аргументи, като резултат е една конкретна фабрика за материали. Можем да правим много фабрики. ``` factory = Factory() ``` Инстанциите от тип `Factory` трябва да могат да бъдат извиквани, т.е. да са callable. Извикването може да се случи по два начина - само с произволен брой именувани аргументи или само с произволен брой позиционни. Не можем да извикваме инстанцията със смесица от позиционни и именувани. Не можем да я извикаме и без аргумент. Ако се опитаме да приложим някой от тези два случая, очакваме да се върне `ValueError` с произволен текст. ``` factory = Factory() factory() # ValueError factory(obj1, name=obj2) # ValueError ``` #### Извикване с именувани аргументи Ако извикате инстанция на `Factory` с произволен брой именувани аргументи, очаква се имената им да съвпадат с един от класовете за материали дефинирани по-горе (или имена на динамично създадени материали, за които ще поговорим по-долу). Ако получите именуван аргумент с друго име - `ValueError` с произволен текст. Очакваната стойност зад името на всеки именуван аргумент е масата за съответния материал, т.е. `int`. Няма да тестваме с невалидни стойности зад имената. Очаква се да върнете като резултат `tuple`, който съдържа по една инстанция, създадена от съответния клас, имаща съответната маса. Редът в резултата трябва да съвпада с реда на подадените аргументи. Извикване със само един именуван аргумент е валидно - връщате `tuple` с един елемент. ``` factory = Factory() brick, wood = factory(Brick=1000, Wood=5000) print(brick.volume) # 0.5 print(wood.volume) # 8.(3) ``` #### Извикване с позиционни аргументи Ако извикате инстанция на `Factory` с произволен брой позиционни аргументи, очаква се аргументите да са инстанции на някой от дефинираните материали (било то петте по-горе, или динамично създадени, за които ще разберете на следващия ред). Очаква се да се създаде динамично нов клас, който репрезентира сплав от подадените материали (да, наясно сме, че не всичко може да се смеси в сплав, но думата е удобна, така че я използваме). Като резултат, трябва да върнете инстанция на новосъздадения клас. Новият клас трябва да има същата сигнатура като 5-те класа за материал от началото на задачата: * да очаква маса за инстанциране; * да има дефинирана плътност; * да имплементира `volume` пропърти. Плътността на новосъздадения клас трябва да бъде средно аритметично от плътностите на всеки от 5-те оригинални материала, които са използвани за създаването на класа. Виж [апендикса](#-6). Името на класа трябва да е конкатенирана версия на имената на тези от **петте класа от по-горе**, които са използвани като вход, в азбучен ред, отделени от долна черта. Масата на създадената инстанция, трябва да е сума от масите на обектите, които са подадени като вход. Ако инстанцията ви бъде извикана няколко пъти с обекти, чиито класове съвпадат (без значение от реда), не очакваме да правите нов клас, тъй като такъв вече сте направили. Просто връщате нова инстанция от вече съществуващия клас. Това важи и за вход от само един елемент (дори той да е един от 5-те в началото). Ако има клас с очакваното име, използвате него. Ако няма, правите нов. Приемете, че няма да тестваме с вход, който съдържа повече от едно срещане на някой от 5-те първоначални материали. Т.е. не очаквайте да получите примерно два материала `Wood`, или комбинация от типа `Concrete_Wood` и `Wood`. Всеки от 5-те материала може да се срещне само веднъж. ``` factory = Factory() brick1, wood1 = factory(Brick=10, Wood=5) brick2, wood2 = factory(Brick=20, Wood=15) factory(brick1, wood1) # Инстанция на новосъздаден клас с име "Brick_Wood". # Класът има плътност (2000 + 600) / 2 = 1300. # Инстанцията има маса 15 factory(brick2, wood2) # Инстанция на вече съществуващия клас "Brick_Wood". # Инстанцията има маса 35 ``` Динамично създадени класове, които генерирате при извикване на инстанциите с позиционни аргументи, трябва да са валиден инпут за последващи извиквания на инстанцията, било то в позиционен, или именуван аргумент. ``` factory = Factory() brick, wood = factory(Brick=10, Wood=5) concreate, = factory(Concrete=10) brick_wood = factory(brick, wood) another_brick_wood, = factory(Brick_Wood=10) # Валидно инстанциране на ново парче сплав с маса 10 и тип Brick_Wood, който е създаден на предишния ред brick_concrete_wood = factory(brick_wood, concreate) # Създава нов клас Brick_Concrete_Wood # Плътността на класа е (2000 + 2500 + 600) / 3 = 1700 # Масата на инстанцията е 25 ``` Логично е след като фабриката е използвала дадени инстанции на материали, подадени като позиционни аргументи, за създаване на нова инстанция, входните инстанции да станат неизползваеми. Нека всеки обект, който е подаден като позиционен на инстанция на `Factory`, да става невалиден. Той продължава да съществува и можете да видите типа му и масата му, но ако го подадете отново на фабрика, да се поражда `AssertionError` с произволен текст. `AssertionError` е вграден тип грешка - не е нужно да го дефинирате. ``` factory = Factory() brick, wood = factory(Brick=10, Wood=5) brick_wood = factory(brick, wood) brick_wood = factory(brick, wood) # AssertionError ``` #### А стената? Като за десерт, трябва да можем да проверим дали материалите, които са създадени от дадена фабрика, са достатъчно за изграждане на стена с конкретен обем. Имплементирайте метод `can_build`, който приема един позиционен аргумент от тип `int` и връща булева стойност, в зависимост от това дали материалите, създадени от конкретната фабрика, са достатъчно. Не ни интересуват обекти, които са създадени извън инстанцията, върху която извикваме `can_build`. Изчисленията включват както материали, създадени с позиционни аргументи, така и материали, създадени с именувани. Не включваме материали, които са преизползвани (подавани на фабрика като позиционни), защото те са невалидни. ``` factory = Factory() brick, wood = factory(Brick=2000, Wood=1200) print(brick.volume) # 1.0 print(wood.volume) # 2.0 print(factory.can_build(3)) # True print(factory.can_build(4)) # False ``` #### Много фабрики? Да, можем да създаваме много фабрики. Ако даден материал е направен с една фабрика, той може да се подава на друга фабрика. Ако дадена сплав е създадена от една фабрика, другите фабрики трябва да са наясно за този технологичен скок и да преизползват вече създадения клас за тази сплав. ``` factory1 = Factory() factory2 = Factory() brick1, wood1 = factory1(Brick=1, Wood=2) brick2, wood2 = factory2(Brick=1, Wood=2) brick_wood1 = factory1(brick1, wood1) # Създава нов клас "Brick_Wood" и връща инстанция от него brick_wood2 = factory2(brick2, wood2) # Използва съществуващия клас "Brick_Wood" и връща инстанция от него print(type(brick_wood1) is type(brick_wood2)) # True ``` Правилото, че не можем да преизползваме материали, ако сме направили от тях друг материал, важи и тук. ``` factory1 = Factory() factory2 = Factory() brick, wood = factory1(Brick=1, Wood=2) brick_wood = factory1(brick, wood) factory2(brick, wood) # AssertionError ``` #### Can build? И последно. Искаме да можем да видим дали материалите, създадени (и не преизползвани) от **всички** фабрики са достатъчно за изграждане на стена. Оценката се случва спрямо обемите на въпросните материали. Ако методът `can_build` е извикан на инстанция, той проверява само материалите направени с тази инстанция. Ако извикаме метод `can_build_together` на класа `Factory`, проверява всички материали създадени (и не преизползвани) от всички инстанции. ``` factory1 = Factory() brick1, wood1 = factory1(Brick=2000, Wood=1200) print(brick1.volume) # 1.0 print(wood1.volume) # 2.0 brick_wood1 = factory1(brick1, wood1) print(brick_wood1.volume) # 2.46... print(factory1.can_build(3)) # False factory2 = Factory() brick2, wood2 = factory2(Brick=2000, Wood=1200) print(brick2.volume) # 1.0 print(wood2.volume) # 2.0 brick_wood2 = factory2(brick2, wood2) print(brick_wood2.volume) # 2.46... print(factory2.can_build(3)) # False print(Factory.can_build_together(3)) # True ``` #### Апендикс за изчисляване на нова плътност Ако имате два материала, например `Wood` и `Concrete`, и ги обедините в сплав, плътността на новия материал е `(2500 + 600) / 2 = 1550`. Ако към тази сплав добавите и `Steel`, плътността не може да се изчисли като средно аритметично от `Concrete_Wood` и `Steel`, защото те имат различно съотношение. Такава сметка би дала резултат `(1550 + 7700) / 2 = 4625`. Това е грешно. Правилната сметка трябва да направите като използвате трите основни материала, които са използвани: `(density of Concrete + density of Steel + density of Wood) / 3.` `(2500 + 7700 + 600) / 3 = 3600.`
Дискусия
Павел Петков
21.11.2024 10:23

``` factory2 = Factory() brick2, wood2 = factory2(Brick=2000, Wood=1200) print(brick2.volume) # 1.0 print(wood2.volume) # 2.0 brick_wood2 = factory2(brick2, wood2) print(brick_wood2.volume) # 2.46 ``` Заради този пример направих така че да се форматира отговора до 2 десетичен знак, защото реалният отговор е 2.4615384615384617. Написах го като коментар, че не съм сигурен дали трябва, тоест този print трябва да изкара 2.4615384615384617 или 2.46 или няма значение, защото използваме self.assertAlmostEqual, и ако ползвам self.assertAlmostEqual трябва да кажа колко знака след десетичната запетая искам, защото в противен случай пак няма да работи коректно.
Нишка
Георги Кунчев
21.11.2024 10:53

Добавих едно многоточие в края на 2.46, за да е ясно, че това само визуализира, а не че трябва да се закръглява.
Ивайло Кънчев
21.11.2024 01:45

Ако на фактори подадем един валиден и един невалиден обект, след хвърляне на грешка валидният обект си остава валиден, нали? И също така форматирането на обема до 2рия знак след запетайката ли трябва да е?
Нишка
Виктор Бечев
21.11.2024 03:11

Да, обектите се използват само ако се стигне до крайния резултат. За точността на обема не сме поставили условие, така че каквото число излезе - такова, ако е с 10 символа след запетаята - so be it.
Йоан Байчев
21.11.2024 00:21

Предвид уточнението, че инстанции на новосъздадените сплави могат да се създават само чрез обекта Factory и не са достъпни глобално, все още ли е валиден граничният случай за "Използване на материали между различни фабрики"?
Нишка
Виктор Бечев
21.11.2024 03:15

Не съм сигурен дали правилно разбирам въпроса, но създадените сплави от една фабрика са достъпни от всички фабрики, чрез използването на името им. Говоря за този пример: ``` factory = Factory() brick, wood = factory(Brick=10, Wood=5) concreate, = factory(Concrete=10) brick_wood = factory(brick, wood) another_brick_wood, = factory(Brick_Wood=10) # Валидно инстанциране на ново парче сплав с маса 10 и тип Brick_Wood, който е създаден на предишния ред ```
Василена Станойска
20.11.2024 18:05

Здравейте. Когато изчисляваме новата плътност, трябва ли да връщаме цяло число, или може да бъде и float?
Нишка
Николай Стоянов
20.11.2024 17:51

Здравейте! Когато създадем нова сплав, очаква ли се да можем да създадем инстанция от новия клас в глобалния scope или е приемливо това да става само от Factory обект?
Нишка
Георги Кунчев
20.11.2024 19:03

Може и само през `Factory` обект. Да ви караме да ги сетвате на `globals()` би било лицемерно от наша страна (и лоша практика).
Павел Петков
20.11.2024 15:36

Направих лека промяна в тестовете, за да не прави проблеми при десетичните запетаи ``` import unittest from fourth_homework import * class BaseMaterialsTests(unittest.TestCase): """ in these tests expected_density is mass and all check if expected_density/mass == 1.0 also they test decimal places for volume """ def setUp(self) -> None: self.factory = Factory() def test_density_for_concrete(self): expected_density = 2500 concrete, = self.factory(Concrete=expected_density) self.assertEqual(concrete.volume, 1.0) def test_density_for_brick(self): expected_density = 2000 brick, = self.factory(Brick=expected_density) self.assertEqual(brick.volume, 1.0) def test_density_for_stone(self): expected_density = 1600 stone, = self.factory(Stone=expected_density) self.assertEqual(stone.volume, 1.0) def test_density_for_wood(self): expected_density = 600 wood, = self.factory(Wood=expected_density) self.assertEqual(wood.volume, 1.0) def test_density_for_steel(self): expected_density = 7700 steel, = self.factory(Steel=expected_density) self.assertEqual(steel.volume, 1.0) def test_for_volume_decimal_places_if_one_decimal_places(self): expected_density = 7700 * 7 steel, = self.factory(Steel=expected_density) self.assertEqual(steel.volume, 7.0) self.assertIsInstance(steel.volume, float) def test_for_volume_decimal_places_if_more_than_places_up(self): # от задачата разбирам че просто трябва резултата да се форматира до 2рия знак # ако не е така да ме поправи някой :) # 4453/7700 = 0.5783116883116883 => 0.58 expected_density = 4453 steel, = self.factory(Steel=expected_density) self.assertEqual(steel.volume, 0.5783116883116883) self.assertIsInstance(steel.volume, float) def test_for_volume_decimal_places_if_more_than_places_down(self): # 4453/600 = 7.421666666666667 => 7.42 expected_density = 4453 steel, = self.factory(Wood=expected_density) self.assertAlmostEqual(steel.volume, 7.42, places=2) self.assertIsInstance(steel.volume, float) class FactoryTests(unittest.TestCase): def setUp(self): self.factory1 = Factory() self.factory2 = Factory() self.wood1, self.steel1, self.brick1, self.stone1, self.concrete1 = self.factory1(Wood=5, Steel=15, Brick=60, Stone=300, Concrete=20) self.wood2, self.steel2, self.brick2, self.stone2, self.concrete2 = self.factory2(Wood=5, Steel=30, Brick=300, Stone=600, Concrete=2000) def test_factory_invalid_call_with_empy_call(self): with self.assertRaises(ValueError): self.factory1() def test_factory_invalid_call_with_args_and_kwargs(self): with self.assertRaises(ValueError): self.factory1(self.wood1, Wood=5) def test_factory_return_type(self): self.assertIsInstance(self.factory2(Wood=66), tuple) def test_two_factories_with_same_passed_arguments_produce_different_objects_of_same_type(self): self.assertEqual(type(self.wood1), type(self.wood2)) self.assertEqual(self.wood1.volume, self.wood2.volume) self.assertNotEqual(self.wood1, self.wood2) def test_factory_call_with_kwargs_with_not_existing_class(self): with self.assertRaises(ValueError): self.factory1(Baba=1) def test_volume_for_all_instances_returned_from_factory_call_kwargs(self): wood, steel, stone = self.factory1(Wood=600, Steel=100, Stone=3200) self.assertAlmostEqual(wood.volume, 1.0, ) self.assertAlmostEqual(steel.volume, 0.012987012987012988) self.assertAlmostEqual(stone.volume, 2.0) def test_for_recreating_already_existing_dynamic_class(self): wooden_steel = self.factory1(self.wood1, self.steel1) self.assertEqual(wooden_steel.__class__.__name__, "Steel_Wood") another_wooden_steel = self.factory1(self.wood2, self.steel2) self.assertEqual(type(wooden_steel), type(another_wooden_steel)) def test_for_reusing_same_materials_for_creation(self): self.factory1(self.wood1, self.steel2) with self.assertRaises(AssertionError): self.factory1(self.wood1) def test_new_generated_class_name_is_sorted_in_ascending_order(self): result = self.factory1(self.wood1, self.steel1, self.brick1) self.assertEqual(result.__class__.__name__, "Brick_Steel_Wood") result = self.factory1(self.stone1, self.steel2, self.wood2, ) self.assertEqual(result.__class__.__name__, "Steel_Stone_Wood") def test_new_generated_class_name_is_sorted_in_ascending_order2(self): result = self.factory1(self.wood1, self.steel2, self.stone2, self.brick1, self.concrete1) self.assertEqual(result.__class__.__name__, "Brick_Concrete_Steel_Stone_Wood") def test_volume_and_density_for_dynamically_created_materials(self): # density = (2500 + 7700 + 600) / 3 = 3600. # mass = 5 + 30 + 2000 = 2035 # volume = 2035 / 3600 = 0.5652777777777778 = 0.57 result = self.factory1(self.wood1, self.steel2, self.concrete2) self.assertAlmostEqual(result.volume, 0.5652777777777778) def test_different_factories_called_with_same_kwargs_return_materials_of_same_type(self): steel_stone = self.factory1(Stone=1, Steel=1) another_steel_stone = self.factory2(Steel=1, Stone=1) self.assertEqual(type(steel_stone), type(another_steel_stone)) def test_one_factory_use_material_and_another_tries_to_use_it_and_fails(self): steel_stone = self.factory1(self.steel2, self.stone1) self.factory1(steel_stone) with self.assertRaises(AssertionError): self.factory2(self.wood1, steel_stone) def test_build_method_returns_true_for_greater_than_volume(self): factory = Factory() factory(Brick(2500)) self.assertTrue(factory.can_build(1)) def test_build_method_returns_true_for_equal_volume(self): factory = Factory() factory(Brick(2000)) self.assertTrue(factory.can_build(1)) def test_build_method_returns_false(self): factory = Factory() factory(Brick(1500)) self.assertFalse(factory.can_build(1)) def test_build_method_with_many_materials_returns_false(self): factory1 = Factory() brick1, wood1 = factory1(Brick=2000, Wood=1200) brick_wood1 = factory1(brick1, wood1) self.assertEqual(factory1.can_build(3), False) def test_build_method_with_many_materials_returns_true(self): # density = (2000+ 600+7700) / 3 = 3433.3333333333335 # mass = 2000 + 1000 + 70000 = 73000 # volume = 73000 / 3433.3333333333335 = 21.262135922330096 = 21.2 factory1 = Factory() brick1, wood1, steel = factory1(Brick=2000, Wood=1000, Steel=70000) factory1(brick1, wood1, steel) self.assertTrue(factory1.can_build(21.26)) self.assertEqual(factory1.can_build(21.27), False) def test_string_after_combination(self): result1 = self.factory1(self.wood1, self.brick1) result2 = self.factory2(self.concrete2, self.stone1) result = self.factory1(result1, result2) self.assertEqual(result.__class__.__name__, "Brick_Concrete_Stone_Wood") if __name__ == "__main__": unittest.main() ```
Нишка
Виктор Бечев
21.11.2024 03:09

Единственият проблем, който намирам с тестовете ти, се корени в решението ти да вземеш произволен толеранс при закръгляване на обема. Може и да се окаже, че недоглеждам нещо, но не забелязвам някъде да сме поставили условие за това с каква точност да се изчислява той, докато в твоите тестове го има като презумпция. Разбирам защо си го направил, иначе пък сравнението на числа с плаваща запетая става ненадеждно. Затова използваме `assertAlmostEqual`. Точката ти я оставям, защото всичко друго работи, но това е за дисклеймър на колегите, както и тема за размисъл за теб.