Предизвикателства > Разбий ни ключалката


Разбий ни ключалката
Краен срок: 13.11.2024 21:00
Точки: 1

## Интродукция Тъй като нямате търпение да разберете резултатите си от първото контролно, даваме ви възможността да го направите по-рано от предвиденото. Който успее да отключи ключалката, ще види резултатите си в лога от изпълнението на тестовете си, когато крайният срок за предизвикателството приключи. Пък и ще получиш точка за правилно решено предизвикателство. ## Какво да напишете Напишете клас `LockPicker_&lt;ФН&gt;`, където "<ФН>" е заменено от вашия ФН. Класът трябва да очаква един позиционен аргумент при инициализация. Аргументът е обект, който представлява ключалка за отключване. Класът ви трябва да има публичен метод `unlock`, който да отключва ключалката **при едно извикване**, т.е. от първия път. ``` # lock е някакъв обект, за който пише по-долу lock_picker = LockPicker_FN1234(lock) lock_picker.unlock() ``` ## Какво да очаквате Обектът, който подаваме, т.е. ключалката, е абсолютна мистерия за вас, но той има механизъм, с който да ви помогне да се справите. Обектът има публичен метод `pick`. Извиквайки този метод, можете да се опитате да отключите ключалката. Методът получава произволен брой позиционни аргумент. Не очаква именувани. Ако аргументите, подадени на `pick`, са точно тези, които ключалката очаква (правилния тип, правилната стойност и правилния ред), то ключалката се отключва и методът `pick` връща `True`. Ако обаче това не се случи, методът ще хвърли грешки, които да ви помогнат да се поправите. Грешките, които можете да очаквате, са варианти на `TypeError` и `ValueError`, но подобрени. Разбирайте - наследили сме тези две вградени изключения и сме ги подобрили. Това го казваме, за да ви е ясно, че не можете да сравнявате типа на получената грешка директно с `TypeError` или `ValueError`, използвайки `type(ex) is TypeError`, защото типът им няма да е такъв, а просто ще го наследява. Всяка една от нашите грешки ще има две допълнителни полета с информация, полезна за вас: `position` `expected` Стойността на тези полета можете да вземете от грешката, която се е възбудила. ``` try: lock.pick() except Exception as ex: print(ex.position) print(ex.expected) ``` Ето и кога какви грешки ще видите. Грешките са подредени по приоритет. Ако имате няколко грешки, ще се възбуди първата намерена в този списък (от горе надолу). * Ако броят аргументи, подадени на `pick`, не съвпада с очаквания брой, ще получите `TypeError`. Специалните полетата в грешката ще са: * `position` - `None` * `expected` - `int`, който показва броя очаквани аргументи * Ако някой аргумент, подаден на `pick`, не е от очаквания тип, ще получите `TypeError`. Специалните полетата в грешката ще са: * `position` - `int`, който показва позицията на грешния аргумент, в списъка подадени аргументи, **ЗАПОЧВАЩА ОТ 1** * `expected` - `type`, който показва типът, който се очаква на тази позиция * Ако някой аргумент, подаден на `pick`, не е с очакваната стойност, ще получите `ValueError`. Специалните полетата в грешката ще са: * `position` - `int`, който показва позицията на грешния аргумент, в списъка подадени аргументи, **ЗАПОЧВАЩА ОТ 1** * `expected` - очакваната стойност на тази позиция ## Пример ``` # lock е някаква ключалка, а аргументите, които `pick` очаква за отключване, са (1, 'две') lock.pick() # TypeError, в който position=None и expected=2 lock.pick([], None) # TypeError, в който position=1 и expected=int lock.pick(1, 'two') # ValueError, в който position=2 и expected='две' ``` ## Аутро * Дори случайно да не си бил на контролно, ако си записан в курса, пак можеш да се бориш за точка. * Използваме ФН, който използвате в сайта на курса. Имаше колеги с няколко ФН номера - използвайте този, който намирате в профила си тук. * Обектите, които `pick` очаква, ще са само от вградени типове, с които сте запознати от лекциите. Няма да тестваме с нищо екзотично. * Разбираме, че има опасност да започнете да брутфорсвате, затова решенията ви ще трябва да се справят с проблема за не повече от 1 секунда, което ще е напълно достатъчно, ако имате адекватен алгоритъм. ПП: Ако искате да разберете как работят истинските ключари: https://www.youtube.com/playlist?list=PL14HqR92npOykl0oqFCMAQbjS8ehetLvN
 1import io
 2import sys
 3import unittest
 4
 5from secret_results import RESULTS
 6
 7
 8class StdBuffer:
 9    def __init__(self):
10        self.buffer = io.StringIO()
11
12    def __enter__(self):
13        self.stds = sys.stdin, sys.stderr, sys.stdout
14        sys.stdin = self.buffer
15        sys.stderr = self.buffer
16        sys.stdout = self.buffer
17
18    def __exit__(self, *args, **kwargs):
19        sys.stdin, sys.stderr, sys.stdout = self.stds
20
21
22with StdBuffer():
23    import solution
24
25
26class TestLockPicker(unittest.TestCase):
27    """Unit test for the LockPicker_* class."""
28
29    def test_sanity(self):
30        """Ensure the class with expected FN is found."""
31        self.assertTrue(found, "Не намирам клас с очакваното име.")
32
33
34found = False
35# Look for a class in student's solution that matches the expected format
36for item in dir(solution):
37    if item.startswith("LockPicker_"):
38        # Try to extract the FN from the class name.
39        fn = item.replace("LockPicker_", "")
40        # If not possible, show relevant error and break -> just run the tests
41        if fn not in RESULTS:
42            print("Факултетният номер е невалиден. Убеди се, че класът ти "
43                  "е във формат LockPicker_FN1234.")
44        else:
45            found = True
46        break
47# If there was no break above, no valid class name was found.
48else:
49    print("Не намирам клас с очакваното име. "
50          "Очаквам формат LockPicker_FN1234, "
51          "където FN1234 е валиден ФН на студент от курса.")
52
53
54if __name__ == "__main__":
55    unittest.main()
  1import unittest
  2import traceback
  3
  4import timeout_decorator
  5
  6from secret_results import RESULTS
  7
  8import solution
  9
 10
 11# Secret lock combination
 12_SECRET_COMBINATION = (1, {'реч': 'ник'}, 'Монти Пайтън',
 13                       {1, 2, 'три'}, True, 3.14159)
 14
 15
 16class LockError(Exception):
 17    """Exception that can store additional attributes."""
 18
 19    def __init__(self, message, position, expected):
 20        self.position = position
 21        self.expected = expected
 22        super().__init__(message)
 23
 24
 25class LockTypeError(LockError, TypeError):
 26    """A kind of a TypeError that stores additional attributes."""
 27    pass
 28
 29
 30class LockValueError(LockError, ValueError):
 31    """A kind of a ValueError that stores additional attributes."""
 32    pass
 33
 34
 35class Lock:
 36    """A lock mechanism, defended by a secret combination."""
 37
 38    def __init__(self, *slots):
 39        """Initialize based on secret slots."""
 40        self._slots = slots
 41        self.is_unlocked = False
 42
 43    def pick(self, *attempts):
 44        """Try to pick using attempt values.
 45        
 46           Raise informative exceptions when failed.
 47           Change the state of the instance when successfull and return True.
 48        """
 49        if len(attempts) != len(self._slots):
 50            raise LockTypeError("This is a different kind of lock.",
 51                                None, len(self._slots))
 52        for position, (attempt, slot) in enumerate(zip(attempts, self._slots),
 53                                                   start=1):
 54            if type(attempt) is not type(slot):
 55                raise LockTypeError("You shall not pass!", position, type(slot))
 56        for position, (attempt, slot) in enumerate(zip(attempts, self._slots),
 57                                                   start=1):
 58            if attempt != slot:
 59                raise LockValueError("You shall not pass!", position, slot)
 60        # Unlocked only when the method is called with a correct attempt 
 61        self.is_unlocked = True
 62        return True
 63
 64
 65class TestLockPicker(unittest.TestCase):
 66    """Unit test for the LockPicker_* class."""
 67
 68    def test_picking(self):
 69        """Ensure the lock is picked based on already stored boolean var."""
 70        # This is just to assign points on success
 71        # as the automatic test runner expects at least one test case.
 72        self.assertTrue(correct, "Не успя да отключиш.")
 73
 74
 75@timeout_decorator.timeout(1)
 76def test_unlocker(unlocker):
 77    unlocker.unlock()
 78
 79
 80# Define a lock based on secret
 81lock = Lock(*_SECRET_COMBINATION)
 82# Assume the lock is not picked
 83correct = False
 84# Look for a class in student's solution that matches the expected format
 85for item in dir(solution):
 86    if item.startswith("LockPicker_"):
 87        # Try to extract the FN from the class name.
 88        fn = item.replace("LockPicker_", "")
 89        # If not possible, show relevant error and break -> just run the tests
 90        if fn not in RESULTS:
 91            print("Факултетният номер е невалиден. Убеди се, че класът ти "
 92                  "е във формат LockPicker_FN1234.")
 93            break
 94        # Get the LockPicker class from the solution
 95        LockPicker = getattr(solution, item)
 96        try:
 97            # Try to unlock the lock
 98            test_unlocker(LockPicker(lock))
 99            # If successfull, assign correct = True to affect unit tests
100            # and show the results
101            if lock.is_unlocked:
102                correct = True
103                print("Резултат от контролното:\n"
104                     f"    {RESULTS[fn]['correct']}/25 верни отговора.\n"
105                     f"    {RESULTS[fn]['points']} точки.")
106            # If the lock is still locked - sorry
107            else:
108                print("Неуспешно отключване.")
109        # Any error that the unlock() method from the solution might throw
110        except Exception:
111            print(f"Грешка при опит за отключване:\n{traceback.format_exc()}")
112        break
113# If there was no break above, no valid class name was found.
114else:
115    print("Не намирам клас с очакваното име. "
116          "Очаквам формат LockPicker_FN1234, "
117          "където FN1234 е валиден ФН на студент от курса.")
118
119
120if __name__ == "__main__":
121    unittest.main()
Дискусия
Георги Кунчев
13.11.2024 19:18

След като крайният срок дойде и публикуваме резултатите, ще можете да видите тестове, както винаги. Този път, обаче, в тях има два импорта, които ще ви гръмнат. Съветвам просто да вземете дефинициите на класа за ключалка и да тествате директно. Ако някой има проблеми, нека пише. ``` import timeout_decorator from secret_results import RESULTS ``` Иначе, ако махнете таймаут декоратора и дефинирате RESULTS като речник с ключ вашия ФН, зад който има речник с ключове "correct" и "points", зад които има рандом стрингове, всичко трябва да сработи
Георги Кунчев
13.11.2024 18:31

Да дефинираш клас динамично чрез `type` - не. Напълно валидно. Да сетваш името в `globals` - бих казал доста неприятно. Не мисля, че има вариант, в който това да е нужно. Независимо от случая, трябва да има по-адекватен вариант. За текущия контекс с предизвикателството ви, едва ли.
Павел Петков
13.11.2024 17:10

Проблем ли е кода да се напише динамично, така че да няма значение какъв е факултетния номер. ``` def unlock(self): pass fn = "на някой номера" globals()[f"LockPicker_{fn}"] = type( f"LockPicker_{fn}", (object,), { "unlock": lambda self: unlock(self) "__init__" : lambda self, lock: setattr(self, "lock", lock) } ) ``` Въпроса ми е този код лоша практика ли е?
Виктор Бечев
13.11.2024 15:50

Аз ще бъда задник и ще ти я махна поради две причини: 1. Кодът има грешки и не тръгва. 2. Лошото форматиране на блока _(просто сложи всичко в \```, за да се мине с един copy-paste)_. Ако ги оправиш - ще си я получиш обратно.
Георги Кунчев
13.11.2024 15:23

@Даниел_Манчевски, давам точка за споделеният ти код, но не давам гаранция, че работи.
Даниел Манчевски
13.11.2024 14:50

``` class MyTypeError(TypeError): def __init__(self, position, expected): self.position = position self.expected = expected class MyValueError(ValueError): def __init__(self, position, expected): self.position = position self.expected = expected class Lock: def __init__(self, *args): self.ideal = list(args) def pick(self, *args): if len(self.ideal) != len(*args): raise MyTypeError(None, len(self.ideal)) for i, el in enumerate(*args): if type(el) is not type(self.ideal[i]): raise MyTypeError(i + 1, type(self.ideal[i])) if el != self.ideal[i]: raise MyValueError(i + 1, self.ideal[i]) return True lock = Lock("123", 65) lock_picker = LockPicker_XXX(lock) print(lock_picker.unlock()) ```
Стефан Шиваров
13.11.2024 12:09

Тоест идеята е да пробваме да викаме pick с някакви аргументи, и в зависимост от върнатата грешка да променяме аргументите, и така докато не се отключи?
Георги Кунчев
13.11.2024 11:37

Няма значение какво връща. Просто след изпълнение трябва да е отключил ключалката, т.е. да е извикал `pick` с правилните аргументи.
Ивайло Кънчев
13.11.2024 11:34

Unlock трябва ли да връща True?