본문 바로가기

카테고리 없음

[SeSAC 성동캠퍼스 1기] 인프라 활용을 위한 파이썬 프로그래밍

1. 소개

 SeSAC 성동캠퍼스에서 진행하는 클라우드 기반의 모빌리티 융합보안과정 교육 1주차는 인프라 활용을 위한 파이썬 프로그래밍을 배웠다.

2. 파이썬 프로그래밍

 인프라 활용을 위한 파이썬 프로그래밍 수업이라 virtualenv라이브러리를 사용하는 것으로 생각했지만, 기초를 잘 알려주는 파이썬 강의였다. 그동안 파이썬을 자주 사용하였지만, 이론적인 부분을 다시 복습할 수 있었다.


 이번 블로그 내용은 기초적인 부분보다 배운 내용을 바탕으로 파이썬을 잘 다룰 수 있는지 실습을 많이 시도했다. 강의 중간에 강사님께서 여러 문제 제시해주셨고 강의에서 배운 내용으로 파이썬 프로그래밍을 하였다.

 

 블로그의 내용을 파이썬 프로그래밍 실습에 초점을 맞춘 이유는 배운 내용을 응용해서 실제 코드에 적용하는 실력과 작성한 코드를 글로 설명하는 실력을 기르고 싶었기 때문이다.

 2 - 1. 구구단

 구구단을 출력할 때, 한글의 다단처럼 여러 줄로 출력하도록 하는 구구단 출력 프로그램을 만드는 것이다.

 

 예를 들어, 단이 3이라면, 첫 줄에는 '2 x 1 = 2    3 x 1 = 3   4 x 1 = 4'로 출력되는 것이다. 쉽게 설명하면 세로로 3줄씩 나눠서 출력하라는 의미다.

 

 아래는 나의 코드이다.

n = 9
m = 3
for i in range(2, n + 1, m):
    for j in range(1, n + 1):
        for k in range(i, i + m if i + m < n + 1 else n + 1):
            print(k, "X", j, "=", k * j, end = "\t")
        print()
    print()

 

(n = 9, m = 3)일 때의 출력

  코드에 대한 설명을 추가하면 n은 구구단을 몇 단까지 출력하는지, m은 몇 개의 세로줄로 출력하는지에 대한 변수이다. n(n>=2)의 값과 m(m>=1)의 값을 임의로 바꾸면서 테스트하면 될 것이다. 추가로 for k in range()안의 조건문은 i + m 과 n + 1 중 큰 작은 값을 사용하면 되므로 저렇게 작성했다.

 2 - 2. 가위바위보 게임

 나와 컴퓨터의 가위바위보 게임을 하는 간단한 프로그램을 만들게 되었다. 자세한 규칙을 설명하면 아래와 같다.

 

    가위, 바위, 보는 각각 0, 1, 2로 대체할 수 있다.

    나의 결정은 표준입력(input함수)을 통해 입력받고, 컴퓨터의 결정은 random함수를 이용한다.

    컴퓨터에게 3번 지는동안 나의 승리횟수를 출력한다.

 

 내가 작성한 코드는 아래와 같다.

import random
win_count = 0 # 승리 횟수
life = 3 # 남은 재도전 횟수
while life > 0:
    myHand = 333333333
    while myHand < 0 or myHand // 3 > 0: # 입력이 유효한지 확인
        myHand = int(input("나의 0(가위), 1(바위), 2(보) ==> "))
    comHand = random.randint(1, 333333333) % 3
    print("컴퓨터의 선택 =", comHand)
    if myHand == comHand:
        print("비겼습니다.")
    elif (myHand + 1) % 3 == comHand:
        print("졌습니다.")
        life -= 1
    else:
        print("이겼습니다.")
        win_count += 1
print("승리 횟수 =", win_count)

계속 바위만 냈을 때의 출력

 코드에 대한 설명을 추가하면, 서로 같은 번호라면 비겼다고 판별한다. 그리고 내가 각각 가위(0), 바위(1), 보(2)를 입력할 때, 컴퓨터가 각각 바위(1), 보(2), 가위(0)를 내면 내가 진다. 여기서 나의 입력값에 1을 더하고 3으로 나눈 나머지는 컴퓨터의 값과 같다는 것을 이용하여 지는 것을 판단할 수 있다. 그 외는 이기는 경우이다. 
 코드를 작성하는 방식은 사람마다 다르므로, 저 코드가 정답이 아니라 저런 코드도 있다는 것으로 생각하면 될 것이다.

 2 - 3. 복권 프로그램

 해당 프로그램은 지난 주의 복권 번호 6개를 입력받고, 1~45 범위의 정수를 반환하는 랜덤 함수를 사용하여, 지난주 복권 번호와 같은 번호가 나올 때까지 시도를 몇 번 했는지 측정하는 프로그램을 만드는 것이다. 

 

 가장 중요한 점은 리스트를 배우기 전이라 리스트 없이 코드를 작성해야 한다는 점이다. 복권이므로 새로 추출한 번호의 순서는 상관없다.

 

 아래는 내가 작성한 코드다.

import random

print("지난 주 복권 번호 입력")
l1 = int(input("지난 주 첫번째 번호 입력:"))
l2 = int(input("지난 주 두번째 번호 입력:"))
l3 = int(input("지난 주 세번째 번호 입력:"))
l4 = int(input("지난 주 네번째 번호 입력:"))
l5 = int(input("지난 주 다섯번째 번호 입력:"))
l6 = int(input("지난 주 여섯번째 번호 입력:"))

count = n1 = n2 = n3 = n4 = n5 = n6 = 0
while True:
    n1 = random.randint(1, 45)
    n2 = random.randint(1, 45)
    while 1 * (n1 ^ n2) == 0:
        n2 = random.randint(1, 45)
    n3 = random.randint(1, 45)
    while 1 * (n1 ^ n3) * (n2 ^ n3) == 0:
        n3 = random.randint(1, 45)
    n4 = random.randint(1, 45)
    while 1 * (n1 ^ n4) * (n2 ^ n4) * (n3 ^ n4) == 0:
        n4 = random.randint(1, 45)
    n5 = random.randint(1, 45)
    while 1 * (n1 ^ n5) * (n2 ^ n5) * (n3 ^ n5) * (n4 ^ n5) == 0:
        n5 = random.randint(1, 45)
    n6 = random.randint(1, 45)
    while 1 * (n1 ^ n6) * (n2 ^ n6) * (n3 ^ n6) * (n4 ^ n6) * (n5 ^ n6) == 0:
        n6 = random.randint(1, 45)
    count += 1
    if (((n1 ^ l1) * (n1 ^ l2) * (n1 ^ l3) * (n1 ^ l4) * (n1 ^ l5) * (n1 ^ l6)) +
        ((n2 ^ l1) * (n2 ^ l2) * (n2 ^ l3) * (n2 ^ l4) * (n2 ^ l5) * (n2 ^ l6)) +
        ((n3 ^ l1) * (n3 ^ l2) * (n3 ^ l3) * (n3 ^ l4) * (n3 ^ l5) * (n3 ^ l6)) +
        ((n4 ^ l1) * (n4 ^ l2) * (n4 ^ l3) * (n4 ^ l4) * (n4 ^ l5) * (n4 ^ l6)) +
        ((n5 ^ l1) * (n5 ^ l2) * (n5 ^ l3) * (n5 ^ l4) * (n5 ^ l5) * (n5 ^ l6)) +
        ((n6 ^ l1) * (n6 ^ l2) * (n6 ^ l3) * (n6 ^ l4) * (n6 ^ l5) * (n6 ^ l6))) == 0:
        break
print(str(count) + "회만에 지난 주 번호와 동일한 번호를 추출했습니다.")
print("지난 주 번호 6개 =", l1, l2, l3, l4, l5, l6)
print("지금 추출한 번호 6개 =", n1, n2, n3, n4, n5, n6)

프로그램의 출력

 코드에 대한 설명을 추가하면, 새로 추출하는 번호는 이전 번호와 중복되었는지 확인한 후에 다음 번호를 추출한다. 예를 들어, 세 번째 번호를 추출할 때, 첫번째나 두번째 번호와 중복되면, 중복되지 않을 때까지 재추첨을 한 후에 네 번째 번호를 추출하기 시작한다.

 

 중복되었는지 확인되는 부분을 비교문(==)이 아니라 베타적논리합(^)연산으로 사용한 것이 특징인데, 같은 값을 베타적논리합연산으로 처리하면 0이 된다. 따라서 이번에 새로 추출한 번호가 기존에 추출한 번호와 중복인지 확인하기 위해 기존에 추출한 번호화 베타적논리합연산을 한 값들을 다 곱해서 0이 되면 중복된 번호가 있다는 의미이다. 해당 방식이 비교문을 여러 번 사용하는 것보다 연산속도가 빠르므로 코드를 위와 같이 작성했다.

 

 아래 부분에서 거대한 if문은 최적화 방법을 찾지 못해서 코드가 길어졌다. 리스트를 사용하지 못한다는 제약조건이 있지만, 제한이 없었다면 단순하게 구현할 수 있다.

 2 - 4. 숫자 퍼즐

 수요일에는 리스트와 함수를 배웠다. 2차원 리스트를 배울 때, 강사님께서 자신이 2차원 배열을 배울 때에는 간단한 숫자 퍼즐 프로그램을 만들어보았다고 말씀하셨다. 내가 생각하기에도 수요일에 배운 리스트, 함수 그리고 그동안 강의에서 배웠던 내용을 이용하여 실력을 향상시키기 위해서 숫자 퍼즐을 만들어 보았다.

 

 아래는 내가 작성한 코드이다.

import random

def initilaize():
    n = 0
    while n < 2:
        n = int(input("격자의 크기를 입력하세요.(2 이상) > "))
    grid = []
    for i in range(n):
        grid.append([0] * n)
        for j in range(n):
            grid[i][j] = i * n + j + 1
    grid[n - 1][n - 1] = 0
    return grid, n

def direct_l2i(d):
    direction_dictionary = {'w':0, 'a':1, 's':2, 'd':3}
    return direction_dictionary[d] if d in direction_dictionary else d

def can_move(i, j, d, n):
    d = direct_l2i(d)
    return (d == 0 and i > 0) or (d == 1 and j > 0) or (d == 2 and i < n - 1) or (d == 3 and j < n - 1)

def move(grid, i, j, d):
    d = direct_l2i(d)
    next_i = i
    next_j = j
    if d == 0:
        next_i -= 1
    elif d == 1:
        next_j -= 1
    elif d == 2:
        next_i += 1
    elif d == 3:
        next_j += 1
    grid[i][j], grid[next_i][next_j] = grid[next_i][next_j], grid[i][j]
    return next_i, next_j

def is_solve(grid, n):
    for i in range(n):
        for j in range(n):
            if grid[i][j] > 0 and grid[i][j] != i * n + j + 1:
                return False
    return True

def print_grid(grid, n):
    for i in range(n):
        for j in range(n):
            print(grid[i][j], end = "\t")
        print()
    print()

def mix(grid, n, mix_count):
    zero_row = zero_col = n - 1
    while is_solve(grid, n):
        for _ in range(mix_count):
            direction = random.randint(0, 3)
            if can_move(zero_row, zero_col, direction, n):
                zero_row, zero_col = move(grid, zero_row, zero_col, direction)
    print_grid(grid, n)
    return zero_row, zero_col

def solve(grid, zero_row, zero_col, n):
    count = 0
    while not is_solve(grid, n):
        key = input("이동할 방향을 입력해 주세요(w = 위쪽, a = 왼쪽, s = 아래쪽, d = 오른쪽):")
        while not (key == 'w' or key == 'a' or key == 's' or key == 'd'):
            key = input("이동할 방향을 입력해 주세요(w = 위쪽, a = 왼쪽, s = 아래쪽, d = 오른쪽):") 
        while not can_move(zero_row, zero_col, key, n):
            print("잘못된 방향입니다.")
            key = input("이동할 방향을 입력해 주세요(w = 위쪽, a = 왼쪽, s = 아래쪽, d = 오른쪽):")
        zero_row, zero_col = move(grid, zero_row, zero_col, key)
        count += 1
        print_grid(grid, n)
    return count

if __name__ == "__main__":
    grid, n = initilaize()
    zero_row, zero_col = mix(grid, n, n ** 3)
    print(str(solve(grid, zero_row, zero_col, n)) + "회 만에 해결하였습니다.")

숫자 퍼즐의 실행 화면

 코드에서는 각각의 기능을 함수로 만들었다. 해당 함수의 반환값을 다른 함수의 인자로 사용하기 때문에 어떤 함수가 어떤 값을 반환하는지 고민을 했던 것 같다. 각 함수는 각각의 역할을 하고 해당 역할을 함수의 이름으로 작명하였다. 설명이 필요한 함수는 아래와 같다고 생각해서 작성하였다.

 

 direct_l2i함수는 입력으로 받는 방향이 w, a, s, d인 문자열을 각각 0, 1, 2, 3인 정수로 변환해주는 역할을 한다.

 can_move와 move의 차이는 can_move가 움직일 수 있는지 확인만 해주는 함수지만, move함수는 실제로 움직이는 역할을 한다. 움직이는 방식은 0인 부분과 방향에 해당하는 숫자의 값을 바꿔주는 방식이다.

 2 - 5. 파일 복사

 파이썬에서 사용하는 파일 입출력 함수도 배웠다. 배운 것을 바탕으로 파일의 내용을 다른 파일에 복사하는 기능을 가진 코드도 작성했다. 해당 코드는 아래와 같다.

f1_path = 'txt1.txt'
f2_path = 'txt2.txt'
with open(f1_path, 'r', encoding = "UTF-8") as f1:
    with open(f2_path, 'w', encoding = "UTF-8") as f2:
        f2.writelines(f1.readlines())

 공부를 위해 해당 내용을 좀 더 찾아보니 파일입출력에서 open - close 방식 대신 with 방식을 선호한다고 한다. 그래서 open - close 방식 대신에 with open 방식을 사용했다.

 2 - 6. 문자열 암호화 / 복호화

 파일 입출력 함수를 응용하여 사용자에게 문자열을 입력받고, 받은 문자열을 암호화하여 파일에 저장하는 문제를 주셨다. 나는 호기심이 더 생겨서 암호화된 파일을 다시 복호화할 수 있을까하는 의문을 가지게 되었고, 복호화까지 하는 부분도 만들어 보았다. 해당 코드는 아래와 같다.

def enc(file_path, text, padding):
    with open(file_path, 'w', encoding = "UTF-8") as f:
        for line in text:
            for letter in line:
                f.writelines(chr(ord(letter) + padding))
            f.writelines('\n')

def dec(file_path, padding):
    with open(file_path, 'r', encoding = "UTF-8") as f:
        for line in f.readlines():
            for letter in line:
                if letter != '\n':
                    print(chr(ord(letter) - padding), end = "")
                else:
                    print()

text = []
line = "."
while line != '':
    line = input("내용을 입력하세요. > ")
    text.append(line)
file_path = 'txt3.txt'
padding = 100
enc(file_path, text, padding)
dec(file_path, padding)

입력 문자
암호화하여 파일에 저장한 상태
복호화하여 출력한 상태

 enc함수는 특정 파일에 입력받은 문자열을 암호화하여 저장하는 함수이며, dec함수는 특정 파일을 읽어서 내용을 복호화하여 출력해주는 함수이다.

 2 - 7. 설정파일 변경하기

 강사님께서 실무와 관련된 파이썬 문제를 주셨다. setup.config파일과 nginx.conf파일을 읽어서 setup.config파일의 내용 중에서 nginx.conf파일과 문자로 일치하는 부분의 값을 setup.config파일의 값으로 바꿔주는 프로그램을 만들어보라고 하셨다. 아래는 프로그램의 input과 원하는 결과값이다.

왼쪽은 setup.config, 오른쪽은 nginx.conf파일이다.

 왼쪽의 파일에서 LISTON_PORT 부분과 SERVER_NAME 부분을 오른쪽 파일의 값으로 업데이트 시키는 것이 목적이다. 추가로 BACKEND 부분의 문자열을 오른쪽 파일 loaction / { 아래줄에 추가하는 것을 원하셨다. 따라서 원하는 출력값은 아래과 같이 유추할 수 있다.

원하는 결과의 nginx.conf파일

 파일 입출력과 사전 자료형을 이용하여 프로그램 코드를 작성하기 원하셨는데, 그 동안 진행했던 파이썬 코드중에 난해했다고 느꼈다. 아래는 내가 작성한 코드이다.

def substitution(setup_cfg, nginx_conf):
    result = ''
    substitution_dictionary = {}
    proxy = ''
    for line in setup_cfg:
        data = []
        remove_data = []
        if len(line.strip()) > 0 and line.strip()[0] != '#': # 주석 처리
            data = line.replace(' : ', ' ').replace(';', '').replace('\n', '').split(' ')
            remove_data = []
            for d in data:
                if len(d) < 2:
                    remove_data.append(d)
            for d in remove_data:
                data.remove(d)
            for d in data[:-1]:
                if d.count('_') > 0:
                    for s in d.split('_'):
                        substitution_dictionary[s] = data[-1]
                substitution_dictionary[d] = data[-1]
        if line.count('BACKEND') > 0:
            proxy = line[line.find('\"') + 1:line.rfind('\"')]
    for line in nginx_conf:
        data = []
        remove_data = []
        if len(line.strip()) > 0 and line.strip()[0] != '#': # 주석 처리
            data = line.replace(';', '').replace('\n', '').split(' ')
            for d in data:
                if len(d) < 2:
                    remove_data.append(d)
            for d in remove_data:
                data.remove(d)
            if len(data) > 1:
                for d in data[:-1]:
                    if d.upper() in substitution_dictionary.keys():
                        line = line.replace(data[-1], substitution_dictionary[d.upper()])
        if line.count('location / {') > 0:
            line = line.replace('location / {', 'location / {' + '\n\t\t' + proxy)
        result += line
    return result

if __name__ == "__main__":
    setup_cfg = nginx_conf = ''
    setup_cfg_path = 'setup.config'
    nginx_conf_path = 'nginx.conf'
    new_nginx_conf_path = 'new_nginx.conf'
    with open(setup_cfg_path, 'r', encoding = "UTF-8") as f:
        setup_cfg = f.readlines()
    with open(nginx_conf_path, 'r', encoding = "UTF-8") as f:
        nginx_conf = f.readlines()
    with open(new_nginx_conf_path, 'w', encoding = "UTF-8") as f:
        f.writelines(substitution(setup_cfg, nginx_conf))

 문자열을 split하기전에 replace함수를 사전에 사용하여 적절하게 최적화하고 split을 진행했다. 이렇게 되면 공백이나 문자열 길이가 0인 문자데이터가 쌓이는데, 해당 부분은 if len(d) < 2: 부분에서 삭제 처리한다. 추가로 location 부분은 특정한 문자로 확인할 수 있어서 위의 코드와 같이 따로 처리했다.

 

 실제로 실행해보니 원하는 결과의 nginx.conf파일의 내용이 new_nginx.conf파일에 작성되었다. 바로 적용하려면 new_nginx_conf_path 변수의 값을 nginx_conf_path로 대입하면 될 것이다.

 

 실무와 연관된 코드를 작성해서 좋은 경험이라고 생각한다.

3. 후기

 이론적인 내용뿐만 아니라 좋은 실습 문제를 많이 제안해주셔서 파이썬의 이론적인 부분만 아니라 활용성까지 배울 수 있었다. 실습했던 문제 중에 수준이 있었던 문제들도 많이 있어서 공부가 많이 되었다고 생각한다.