피보나치 수열
1
1, 1, 2, 3, 5, ...
재귀 (Recursion)
자기 자신을 호출하는 방법이다.
1
2
3
4
def fib2(n:int) -> int:
if n < 2:
return n
return fib2(n - 1) + fib2(n - 2)
재귀를 사용하면 같은 인자에 대한 호출이 중복으로 발생한다. 이는 인자가 커질수록 기하급수적으로 증가한다.
기저 조건(또는 종료 조건)을 설정하지 않으면 무한 재귀 호출의 굴레에 빠질 수 있으므로 조심해야 한다!
메모이제이션 (Memoization)
사전 자료형에 함수의 리턴값을 저장하면 불필요한 호출을 줄일 수 있다.
1
2
3
4
5
memo:dict[int:int] = {0: 0, 1: 1}
def fib3(n:int) -> int:
if n not in memo:
memo[n] = fib3(n - 1) + fib3(n - 2)
return memo[n]
내장 데코레이터인 @lru_cache
는 함수의 리턴값을 자동으로 캐싱(저장)할 수도 있다. 데코레이터를 추가하면 최초 호출이 아닌 모든 함수 호출은 캐싱된 결과를 대신 반환한다. maxsize는 캐시할 수 있는 최대 개수를 의미하며, None
으로 설정 시 캐싱의 제한이 사라진다.
1
2
3
4
5
6
from functools import lru_cache
@lru_cache(maxsize=None)
def fib4(n:int) -> int:
if n < 2:
return n
return fib4(n - 1) + fib4(n - 2)
기존 재귀 함수와 구조는 같다.
lru : last recently used
튜플 언패킹 (Tuple unpacking)
재귀적으로 해결할 수 있는 문제는 반복문으로 해결할 수도 있다. 튜플 언패킹은 시간적, 공간적 효율의 두 마리 토끼를 모두 잡는 방법이다.
1
2
3
4
5
6
7
def fib5(n:int) -> int:
if n == 0: return n
last:int = 0
next:int = 1
for _ in range(1, n):
last, next = next, last + next
return next
제네레이터 (Generator)
1
2
3
4
5
6
7
8
9
from typing import Generator
def fib6(n:int) -> Generator[int, None, None]:
yield 0
if n > 0: yield 1
last:int = 0
next:int = 1
for _ in range(1, n):
last, next = next, last + next
yield next
피보나치 함수를 for 반복문에서 사용하는 range와 같은 제네레이터로 만든다면, 이전까지의 모든 결과를 yield
할 수 있다.
1
2
for i in fib6(20):
print(i)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
처음에는 0과 1을 생성, 이후 튜플 언패킹 방식으로 구한 값을 차례로 yield한다.
깨지지 않는 암호화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from secrets import token_bytes
from typing import Tuple
def random_key(length:int) -> int:
tb:bytes = token_bytes(length)
return int.from_bytes(tb, "big")
def encrypt(original:str) -> Tuple[int, int]: # 암호화
original_bytes:bytes = original.encode()
original_key:int = int.from_bytes(original_bytes, "big")
dummy:int = random_key(len(original_bytes))
encrypted:int = original_key ^ dummy
return dummy, encrypted
def decrypt(key1:int, key2:int) -> str:
decrypted:int = key1 ^ key2
temp:bytes = decrypted.to_bytes((decrypted.bit_length() + 7) // 8, "big")
return temp.decode()
-
random_key는
secrets
모듈의token_bytes
메서드를 사용해 원본 데이터와 같은 길이의 더미 데이터를 생성한다. -
encrypt는 두 데이터를 종합해 암호화한다. 결과물로 두 개의 키를 반환한다. (dummy, encrypted)
original_key, dummy, encrypted중 두 값을 알고 있으면 XOR 연산을 통해 나머지 하나의 값을 알 수 있다.
1 2 3
original_key ^ dummy = encrypted dummy ^ encrypted = original_key encrypted ^ original_key = dummy
-
이 성질을 이용해 decrypt로 두 개의 키를 종합해 복호화한다.
일련의 과정을 거친 반환값이 원래의 값과 같으면, OTP 알고리즘이 정상적으로 작동함을 알 수 있다.
1
2
3
4
5
if __name__ == "__main__":
original = "One Time Pad!"
key1, key2 = encrypt(original)
result:str = decrypt(key1, key2)
print(original == result)
1
True
Reference
-
고전 컴퓨터 알고리즘 인 파이썬
-
https://docs.python.org/3.8/library/functools.html#functools.lru_cache