Домашни > Функцията, която казва "Ni!"


Функцията, която казва "Ni!"
Краен срок: 22.10.2024 18:00
Точки: 10

### Контекст Тъй като все още не сте добили достатъчно познания за света на Монти Пайтън, искаме да ви обогатим със следния откъс от техен филм: [![The Knights Who Say "Ni!" - Monty Python and the Holy Grail](/media/resources/homework2.png)](https://www.youtube.com/watch?v=zIV4poUZAQo) ### Въведение Целта на домашното е да напишете функция, която симулира рицарите, които казват "Ni!". Както става ясно от видеото, рицарите винаги отговарят с "Ni!", освен ако не им бъде предоставено "shrubbery" (храсталак) и то само при условие, че храсталакът е "one that looks nice" (такъв, който изглежда хубаво) и "not too expensive" (не прекалено скъп). Е, не винаги, но ние ще го опростим. Целта ви е да напишете функция, която оценява получения си вход и определя дали е получила храсталак, който изглежда хубаво и не е прекалено скъп. ### Задачата Напишете функцията `function_that_says_ni`, която приема произволен брой позиционни и/или именувани параметри. Стойностите на тези параметри могат да бъдат от произволен тип, но тези, които касаят поведението на функцията, са само аргументите от тип `dict` (речник). Всички останали могат да бъдат пренебрегнати. Ако аргумент на функцията (било то позиционен или именуван) е от тип `dict`, то той е потенциално "храст". Речник, който описва "храст", трябва да отговаря на следното условие: **Речникът има ключ от тип `str` със стойност `name`, зад който ключ има стойност от тип `str` и някоя от следните стойности "храст", "shrub", "bush". Може и в комбинация от малки/главни букви.** Речник, който описва "храст", може да съдържа и друг ключ от тип `str` със стойност `cost`, зад който ключ има стойност от тип `int` или `float`, показващ цената на дадения храст, но такъв ключ може и да не се съдържа в речника. Речникът може да има и други ключове, но всичко извън `name` и `cost` не ни интересува. Например: ``` {"name": "чегарак", "cost": 1.00} # Не е "храст", защото "чегарак" не фигурира в списъка по-горе {"name": "храст", "cost": 2.00} # Това е "храст" {"name": "shrub", "cost": 3.00} # Това е "храст" {"name": "bush"} # Това е "храст". Цената не е задължителна. ``` Целта ви е да оцените всички "храст"-и, които вашата функция приеме като вход, и да прецените дали сте удовлетворени, или не. В случай, че не сте получили храсталак (комбинация от "храсти"-и), който изглежда хубаво и не е прекалено скъп, функцията ви трябва да връща `str` със стойност "Ni!". В противен случай трябва да върнете `str`, който съответства на цената на храсталака, който сте получили, във формата "1.23лв" (винаги закръглен до два символа след десетичната запетая и включвайки "лв" накрая. #### Цената (not too expensive) Цената на храсталак се определя като сума от всички "храст"-и, които функцията ви е получила като вход. Един храсталак е прекалено скъп, ако цената му е повече от 42.00лв. #### Хубостта (one that looks nice) Един храсталак изглежда хубав, когато броят от уникалните букви, които са използвани за изписване на имената на всички именувани аргументи на функцията, които представляват "храст"-и, е кратен на целочислената част от цената му. *Кратно се нарича цяло число, което се дели на друго число без остатък. Например: Ако цената на храсталака, който сте получили, е "2.80лв", то той е по-евтин от 42.00лв, но за да удовлетворите функцията, трябва да се уверите и че храсталакът е хубав. Хубав храсталак в този случай би бил само такъв, че броят от уникалните буквите, използвани в имена на именувани аргументи, които представляват "храст"-и, е кратен на 2 (цялата част от 2.80). ``` function_that_says_ni(ab={"name": "храст", "cost": 1.80}, bc={"name": "храст", "cost": 1}) # Броят уникалните букви, използвани за дефиниране на имената на именувани аргументи, е 3 ("a", "b" и "c"). # 3 не е кратно на 2 (цялата част от сумата), така че това не е хубав храсталак. ``` ### Уговорки * "Храст", който има `cost` от тип `float`, може да има стойност най-много до втора цифра след десетичната запетая. Няма да тестваме с по-висока точност. * "Храст", който няма поле с ключ `cost`, се счита за безплатен, т.е. има цена 0. * "Храст", който има поле с ключ `cost`, може да има само стойности `int` и `float` зад този ключ. Няма да тестваме с невалидни цени зад запазеното име за това. * Не очакваме да има такива недоразумения, но за всеки случай - няма да тестваме с вложени храсти. С други думи, не очаквайте аргумент, подаден на функцията, да е колекция, някоя от чиито стойности да е речник, който изпълнява условията за храст. Храст може да бъде само стойност на аргумент, подаден към функцията, но не и речници, които са част от колекция, подадена като аргумент. * Забележете, че при определяне на хубостта се използва целочислената част от общата цена на храсталака (включително цената на тези "храст"-и, които са влезли като позиционни аргументи), но само имената на именуваните аргументи, които са "храст"-и. * В случай, че целочислената част на храсталака е нула, няма как да дефинираме кратност при търсене на хубост, така че приемаме, че храсталакът не е хубав. * Ключовете на речник, които дефинират специална функция са "name" и "cost" и те винаги ще бъдат подавани с малки букви, точно така, както са написани в това изречение. Стойностите, които дефинират храст ("храст", "shrub", "bush"), могат да бъдат подавани и като комбинация от малки/главни букви (например "SHruB"). * Говорейки за имена на именувани аргументи и техния брой "уникални букви", включваме и случаите, в които измежду тези букви има символа "_". Нека този символ се брои за валидна буква. Като изключим този специален символ, всички останали символи ще са малки букви от латиницата, както се препоръчва в PEP8. Не очаквайте комбинации от малки и главни букви там. * **Генерално правило и занапред - очакваме от функциите да върнат резултат, а не да принтират каквото и да било.** ### Пример ``` function_that_says_ni({"name": "храст", "cost": 120}) > "Ni!" # Независимост от всичко останало, очевидно този храсталак е прекалено скъп function_that_says_ni({"name": "храст", "cost": 1}) > "1.00лв" # Цената е под максимума, а общият брой букви, използвани като именувани аргументи за дефиниране на "храст"-и, е 0, което е кратно на 1. function_that_says_ni(aabcc={"name": "bush", "cost": 3.20}) > "3.20лв" # Цената е под максимума. Общият брой уникални букви, използвани като именувани аргументи за дефиниране на "храст"-и, е 3 ("a", "b", "c"). # Целочислената част на цената също е 3. Кратността е изпълнена, т.е. храсталакът е ок и връщаме цената. ```
 1import unittest
 2
 3import solution
 4
 5
 6class TestSanity(unittest.TestCase):
 7    """Check if the function is present."""
 8
 9    def test_function(self):
10        self.assertIn('function_that_says_ni', dir(solution), 'Убеди се, че функцията "function_that_says_ni" е налична с точно това име.')
11        self.assertTrue(callable(solution.function_that_says_ni), 'Убеди се, че "function_that_says_ni" е функция.')
12
13
14if __name__ == '__main__':
15    unittest.main()
 1import unittest
 2
 3from solution import *
 4
 5
 6class TestNi(unittest.TestCase):
 7    """Test all requirements related to the function that says 'Ni!'."""
 8
 9    NI = 'Ni!'
10
11    def test_empty(self):
12        """Test with empty input."""
13        self.assertEqual(function_that_says_ni(), self.NI)
14
15    def test_single_with_no_named_args(self):
16        """Test with a single shrub that is passed as positional argument."""
17        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 3.12}), '3.12лв')
18
19    def test_all_possible_shrub_strings(self):
20        """Test with all possible strings that define a shrub."""
21        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 1}), '1.00лв')
22        self.assertEqual(function_that_says_ni({'name': 'bush', 'cost': 1}), '1.00лв')
23        self.assertEqual(function_that_says_ni({'name': 'храст', 'cost': 1}), '1.00лв')
24        self.assertEqual(function_that_says_ni({'name': 'SHrub', 'cost': 1}), '1.00лв')
25        self.assertEqual(function_that_says_ni({'name': 'buSH', 'cost': 1}), '1.00лв')
26        self.assertEqual(function_that_says_ni({'name': 'хрАст', 'cost': 1}), '1.00лв')
27
28    def test_with_no_cost(self):
29        """Test with a shrub without defined cost."""
30        self.assertEqual(function_that_says_ni({'name': 'shrub'}), self.NI)
31
32    def test_multiple_shrubs_sumс(self):
33        """Test with a multiple shrubs and cornercase costs."""
34        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 20.00},
35                                               {'name': 'shrub', 'cost': 22}), '42.00лв')
36        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 19.99},
37                                               {'name': 'shrub', 'cost': 22}), '41.99лв')
38        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 20.01},
39                                               {'name': 'shrub', 'cost': 22}), self.NI)
40
41    def test_cost_whole_part_zero(self):
42        """Test with a total cost part equal to zero."""
43        self.assertEqual(function_that_says_ni({'name': 'shrub', 'cost': 0.1},
44                                               {'name': 'храст', 'cost': 0.2}), self.NI)
45
46    def test_named_arguments(self):
47        """Test with named arguments."""
48        # Case with whole-part cost exactly
49        # devided by number of unique chars = 7
50        self.assertEqual(function_that_says_ni(_abcde={'name': 'shrub', 'cost': 2.82},
51                                               _abcf={'name': 'bush', 'cost': 4.51}), '7.33лв')
52        # Case with whole-part cost exactly
53        # devided by number of ALL chars (3),
54        # but not by number of unique chars (2)
55        self.assertEqual(function_that_says_ni(a={'name': 'shrub', 'cost': 1},
56                                               ab={'name': 'bush', 'cost': 2}), self.NI)
57
58    def test_combination_of_arguments(self):
59        """Test with combination of named and positional arguments."""
60        self.assertEqual(function_that_says_ni({'name': 'bush', 'cost': 1},
61                                               {'name': 'храст', 'cost': 1},
62                                               a={'name': 'shrub', 'cost': 0},
63                                               ab={'name': 'bush', 'cost': 0}), '2.00лв')
64
65    def test_invalid_strings(self):
66        """Test with invalid strings that might be misinterpreted."""
67        self.assertEqual(function_that_says_ni({'no_name': 'bush', 'cost': 1}), self.NI)
68        self.assertEqual(function_that_says_ni({'name': 'not a shrub', 'cost': 1}), self.NI)
69        self.assertEqual(function_that_says_ni({'name': 'shrub', ' cost': 1}), self.NI) # Space before cost
70
71
72    def test_other_than_dicts(self):
73        """Test with inputs other than dicts."""
74        self.assertEqual(function_that_says_ni(1, 3.14, ['some_list'], some_arg={1, 2, 3}), self.NI)
75
76
77if __name__ == '__main__':
78    unittest.main()
Дискусия
Георги Кунчев
22.10.2024 21:10

За всички колеги, на които съм писал, че вместо да търсят с `in` в `list`, могат да търсят в `tuple`, за да е по-бързо - това е така, но не усезаемо. При `set` ползвате хеш таблици и е супер бързо. При `tuple` е по-бързо от `list`, но не много. Просто на моменти може би съм твърдял, че `tuple` дава константа сложност, което не е така. Като го напиша веднъж на някого и спя, после просто го цитирам без да се замисля.
Даниел Стефанов
21.10.2024 12:33

Ето още тестове: https://pastebin.com/QA1s4nVV Само трябва да си оправите import-a
Георги Балтиев
19.10.2024 11:06

Отговорих си на въпроса, nvm - не бях дочел внимателно условието
Георги Кунчев
18.10.2024 21:44

@Василена_Станойска, права си. При така зададеното условие, сума по-голяма от 27 би връщала винаги "Ni!". 42 бе избрано като "Oтговорът на въпроса за живота вселената и всичко останало", което в конкретния случай можеше просто да се замени от 27. Аз, като автор на задачата, не съм се замислил върху този детайл. Въпреки че той не влияе на условието и не е пропуск, който може да доведе до отнети точки, получаваш точка за наблюдателност!
Василена Станойска
18.10.2024 21:33

-> Символът `_` се разглежда като всеки един друг символ, т.е. дори да срещнеш `_` многократно в едно или в различни имена на именувани аргументи, които са "храст"-и, броиш го само веднъж. В такъв случай обаче, ако считаме, че името на именуваните аргументи може да ни бъде само от букви, "" и от "_", то в най-добрият случай ние можем да имаме 26 + 1 уникални символа, което после като проверяваме дали това число е кратно на общата сума, то ако сумата ни е по-голяма от 27, то ние винаги ще имаме резултат, че храстът не е хубав и така ограничението за максималната цена не трябва ли да бъде просто 27, вместо 42? Или просто това е поведението, което търсим?
Георги Кунчев
18.10.2024 20:59

@Василена_Станойска Символът `_` се разглежда като всеки един друг символ, т.е. дори да срещнеш `_` многократно в едно или в различни имена на именувани аргументи, които са "храст"-и, броиш го само веднъж.
Георги Кунчев
18.10.2024 20:57

@Павел_Петков "Ако се подаде dict който е невалиден храст това означава ли че целият храсталак е невалиден или просто се игнорира" -> Просто игнорирате невалидните храсти. Наличието на невалиден храст, невалиден речник, или друг невалиден обект, не влияе на целия резултат. Възможно е да има друго валидни храсти. "ако се игнорират в такъв случай трябва ли при позиционните аргументи да се броят само ключовете които са храсти" -> Отговарям с цитат от условието: "Един храсталак изглежда хубав, когато броят от уникалните букви, които са използвани за изписване на имената на всички именувани аргументи на функцията, които представляват "храст"-и" ...и разяснявам - да, използват се само имената на позиционните аргументи, които са валидни "храст"-и. Останалите се пренебрегват.
Василена Станойска
18.10.2024 16:36

Здравейте, имам въпрос свързан с предпоследната точка от "Уговорките": когато имаме "_" в името на именованата променлива и след това броим уникалните символи от всички имена на именованите променливи, то този символ броим ли го като уникален, или за всяко негово срещане той си се счита като отделна буква и взимаме предвид накрая и броя на всички негови срещания?
Павел Петков
18.10.2024 15:26

Ако се подаде dict който е невалиден храст това означава ли че целият храсталак е невалиден или просто се игнорира и ако се игнорират в такъв случай трябва ли при позиционните аргументи да се броят само ключовете които са храсти
Илиан Запрянов
18.10.2024 14:11

Примерни тестове за Домашно 2 https://docs.google.com/document/d/1XpVwY-X-NKEvOoYVRD_3qR06xLbnZm8v9ahs0i7L29Y/edit?usp=sharing