INSERT문 – 테이블에 값 삽입하기 – SQLite3

테이블에 값을 삽입할 때는 INSERT 구문을 사용한다. 이 구문은 크게 세가지 형태로 만들어질 수 있다.

  1. INSERT INTO table (columns...) VALUES (.... ); : 각 칼럼에 (정의하지 않으면 스키마의 모든 칼럼에) ( ... ) 의 값을 각각 적용해서 새로운 레코드를 삽입한다. 지정한 칼럼이나 혹은 전체 칼럼과 값의 개수가 맞지 않으면 에러가 발생한다.
  2. INSERT INTO table SELECT ...; : SELECT 구문을 실행해서 그 결과의 각 행을 해당 테이블의 새로운 행으로 삽입한다. SELECT 결과의 각 칼럼의 순서와 개수는 테이블의 칼럼과 일치해야 한다. 아니면 최소한 칼럼 목록을 명시한다.
  3. INSERT INTO table DEFAULT VALUES; : 모든 칼럼의 디폴트값이 정의되어 있다면 디폴트 값으로 새 레코드를 생성한다.

액션

INSERT 키워드는 REPLACE, INSERT OR REPLACE 등으로 변경될 수 있다. 이는 특정 제한에 의한 충돌 발생 시 이를 해결하기 위한 방편을 정의한다. REPLACEINSERT OR REPLACE와 같은 명령이며 정확하게는 INSERT 문 내에서 ON CONFILCT에 대한 처리로, UNIQUE 제약이나 PRIMARY KEY가 겹치는 등의 삽입 오류가 발생했을 때, 기존 레코드를 UPDATE 하는 방식으로 처리한다.

파이썬 연습문제 – 누워있는 드럼통에 들어있는 기름의 부피

반지름이 R cm이고 높이가 H cm 인 드럼통에 기름이 들어있다. 이 드럼통 내의 기름의 높이가 A라 할 때 기름의 부피를 구하는 것은 원기둥의 부피를 구하는 것이므로 그리 어렵지 않다. 그런데 드럼통이 세워져 있지 않고 수평으로 누워서 설치되었을 때, 기름의 높이가 B 이면, 이 때 드럼 통 내의 부피를 계산해보자. 간단한(?) 적분 문제인데, 여기서는 구분구적법을 적용해서 근사치를 구해볼 것이다.

파이썬 연습문제 – 누워있는 드럼통에 들어있는 기름의 부피 더보기

ZMQ의 기본 개념들

일전에 간단하게 ZMQ(Zero MQ)에 대한 내용을 간단히 정리해본 바 있는데, 이 때는 소켓에 대한 내용을 살펴보다가 흘러흘러 닿은 부분이라 제대로 설명하지 못하고 공식문서에 나오는 예제를 그대로 옮기는 수준이었다.  ZMQ는 소켓 프로그래밍 API를 대체할 수 있는 정말 괜찮은 라이브러리라는 생각이 들어서 활용할 폭이 넓다고 판단됐다. 다만 용어나 개념에 대한 약간의 선행지식이 필요한 부분이 있다. 오늘은 ZMQ에서 사용되는 기본적인 개념에 대해서 알아보고, ZMQ를 통해서 간단한 에코서버와 클라이언트로 소켓통신을 구현하는 방법에 대해 살펴보도록 하겠다. 그리고 ZMQ를 사용하면 전통적인 소켓 접속을 구현하는 것보다 얼마나 편하며 또 멋지게 돌아가는지도 살펴보도록 하겠다.

ZMQ란?

  • 코드를 모든 언어, 모든 플랫폼 상에서 연결
  • inproc, IPC, TCP, TIPC, 멀티캐스트를 통해 메시지들을 전달
  • pub-sub, push-pull 그리고 router-dealer와 같은 스마트한 패턴
  • I/O 비동기 엔진을 지원하는 소형 라이브러리
  • 대규모의 활동적인 오픈소스 커뮤니티 지원
  • 모든 현대 언어와 플랫폼을 지원
  • 중앙집중, 분산, 소규모 및 대규모의 모든 아키텍처를 구성 가능
  • 완전한 상업적 지원을 동반하는 무료 소프트웨어

위는 ZMQ 홈페이지에서 소개하는 ZMQ의 특징이다. ZMQ는 제로엠큐라 불리는 분산 메시징 플랫폼을 구현하기 위한 라이브러리이다. 흔히 소켓 통신 구현을 간단히 할 수 있는 라이브러리로 많이 소개되는데, ZMQ는 사실 훨씬 더 많은 것을 간편하게 구축할 수 있으며, 더군다나 빠르게 처리되도록 할 수 있다. ZMQ의 컨셉은 여러 가지 측면을 가지고 있지만, 그중에 가장 근간이 되는 키워드라 함은 ‘여러 노드를 편리하게 이어주는 분산형 메시징 플랫폼’이다.  위의 특징 소개글에서도 약간 엿볼 수 있지만, 다음과 같은 특징을 가진다.

  1. ZMQ는 단순히 소켓과 소켓을 연결하는 라이브러리가 아닌, 노드와 노드를 연결하여 메시지를 주고받을 수 있는 플랫폼을 구축하는데 쓰인다. 이 때 노드는 소켓외 스레드, 프로세스, TCP로 연결된 다른 머신이 될 수 있다. 즉 노드가 어디에 있든지에 대해서 상관하지 않도록 메시징의 양 끝단이 추상화되어 있다.
  2. 전달되는 데이터는 모두 ‘메시지’라는 형태로 불린다. 메시지는 그 속에 무엇이 들어있는지를 신경쓰지 않으며, 여러 언어/플랫폼간에 호환이 가능하도록 길이값+데이터의 형태로 묶여 있다.
  3. ZMQ의 Q는 Queue를 의미한다. 이는 노드들이 연결되어 만들어진 플랫폼 내에서의 모든 메시지 전달은 자동으로 큐를 거치게 된다. 소켓의 경우 서버가 소켓을 열기 전에 클라이언트가 접속하는 상황은 에러가 되지만, ZMQ에서 이러한 순서는 중요하지 않다. 수신측이 없거나 유효하지 않은 메시지는 모두 큐잉되며, 나중에 소비되거나 혹은 버려질 수 있다. 반대로 메시지를 받아들이는 부분에서도 큐가 사용된다. 모든 큐에 관한 관리는 라이브러리가 자동으로 관리하며, 큐 관리에 들어가는 비용은 없다. (그래서 Zero MQ이다.)

컨텍스트 (context)

모든 ZMQ 관련 코드는 컨텍스트 객체를 만드는 것으로 시작하고, 모든 ZMQ소켓은 컨텍스트를 통해서 생성된다. 소켓을 생성하고 관리하는 컨테이너이자 관리자이다. 컨텍스트는 스레드 안전하며, 프로세스 내에서 하나만 생성하고 사용하여야 한다.

컨텍스트를 통해서 생성되는 소켓에서 발생하는 입출력은 실제로는 백그라운드 스레드에서 비동기로 일어나며, 이 과정은 전부 컨텍스트에 의해서 처리된다. 컨텍스트로부터 명시적으로 생성되는 소켓은 실제로는 소켓이 아니며, 즉시 생성되지도 않는다. 모든 것은 내부에서 자동으로 관리된다.

소켓

이 글에서 아무런 단서 없이 언급되는 ‘소켓’이라는 명칭은 ZMQ 내에서의 소켓을 말한다. 전통적인 네트워킹에 사용되는 UNIX 소켓은 “UNIX 소켓” 혹은 “전통적인 소켓”이라고 언급할 것이다. 이와 같은 명명 관습은 ZMQ를 다루는 이 글과 그 후속글에서 공통적으로 적용할 예정이다.

전통적인 UNIX의 소켓은 네트워크 포트를 추상화한 것이다. 이에 빈해 ZMQ의 소켓은 메시지를 전달하고 받는 창구의 개념이다. UNIX 소켓은 수신측인 상대편 노드가 열려있는 소켓이거나 혹은 상대방 소켓이 이쪽으로 붙어서 연결되었음을 상정한다. (예를 들어 클라이언트쪽에서 열려있지 않은 서버의 소켓에 접속하려 시도하면 강제로 연결이 끊기면서 에러가 발생한다.) 이에 반해 ZMQ의 소켓은, 컨텍스트 내에서 생성되는 소켓의 재추상화된 버전이다. 소켓은 TCP등의 프로토콜을 사용하는 전통적인 UNIX 소켓일 수도 있으며, 상대방 노드의 종류에 따라 inproc(동일 프로세스 내에서 스레드간 통신을 위한 규격) 혹은 IPC(동일 시스템 내에서 프로세스간 통신을 위한 규격)일 수 있다. 실제 물리적인 소켓은 필요에 따라 생성된다.

실제로 소켓은 UNIX 소켓과 1:1로 대응하지 않는다. 우리가 바라볼 때 소켓의 뒤에는 수신자가 아닌 메시지를 주고 받기 위한 큐가 있을 뿐이다. 기술적으로 ZMQ에서는 하나의 소켓을 여러 개의 포트에 바인딩하거나, 여러 노드에 connect 할 수 있다. (이 경우에 송신할 때나 수신할 때, 여러 포트/노드로 번갈아가며 공평하게 하나씩 전송하거나 전송받는다.)

다음은 간단한 ZMQ에서의 소켓을 이용한 에코서버/클라이언트 구현이다. 컨텍스트가 어떤 소켓을 사용하고 어떤 세팅을 사용할 것인지에 대해서 우리는 큰 고민을 할 필요가 없으며, 몇 가지 핵심적인 코어 메시지 패턴 중에서 하나를 선택하면 된다. 우리가 만들려고 하는 에코 서버/클라이언트는 클라이언트가 서버에 요청을 보내고, 그 요청에 대한 응답을 받는 식으로 서로 한 걸음씩 짝을 맞추어 커뮤니케이션하는 패턴을 갖는다. 이를 ZMQ에서는 REQ-REP 패턴이라고 한다. 이 패턴에 기초해서 소켓을 만들고, 사용한다.

## echo-server.py
## 서버측 코드
import zmq, time

## 컨텍스트를 생성한다.
ctx = zmq.Context()

def run_server():
  ## zmq 소켓을 만들고 포트에 바인딩한다.
  sock = zmq.socket(zmq.REP)
  sock.bind('tcp://*:5555')
  while True:
    ## 소켓을 읽고 그 내용을 출력한 후 다시 되돌려 준다.
    msg = sock.recv()
    print(f'Recieved: {msg.decode()}')
    time.sleep(1)
    sock.send(msg)

run_server()

----
## echo-client.py
## 클라이언트 코드

import zmq, sys

## 동일하게 컨텍스트를 만들고 
ctx = zmq.Context()

def run_client(port=7777):
  ## 컨텍스트로부터 소켓을 만들고 서버에 연결한다.
  sock = ctx.socket(zmq.REQ)
  sock.connect(f'tcp://localhost:{port}')
  while True:
    ## 키보드로부터 입력받은 내용을 서버로 전송하고,
    ## 다시 서버의 응답을 출력한다.
    ## bye를 메시지로 보내고 받으면 소켓을 닫는다.
    line = input()
    sock.send(line.encode())
    rep = sock.recv()
    print(f'Reply: {rep.decode()}')
    if rep.decode() == 'bye':
      sock.close()
      break

port = sys.argv[1] if len(sys.argv) > 1 else 7777
run_client(port)

이 코드들은 전통적인 소켓을 이용한 에코서버 구현보다 훨씬 간단하다.

  1. 소켓을 생성하는데에는 어떤 패밀리나 타입 정보도 필요하지 않다. 단지 컨텍스트에게 메시지 패턴 정보만 알려줄 뿐이다.
  2. 바인드하는 액션외에 listen()이나 accept() 같은 작업은 필요하지 않다. 실제로 포트를 듣고, 연결을 수락하는 물리적인 포트에 대한 추상화된 객체가 존재할 것이나, 이 모든것은 컨텍스트 내부에 있으며, 자동으로 관리된다.
  3. 파이썬의 표준 소켓 api와 비교했을 때, sendall()은 존재하지 않는다. 모든 메시지는 마치 전체가 한 덩어리로 보내지는 것처럼 보인다.

실제 실행에서도 몇 가지 차이를 보이는데, 우선 여느 소켓 서버-클라이언트 예제와 달리 이 파일들에 대해서는 클라이언트를 먼저 실행해도 아무런 에러 없이 실행된다. 또한 클라이언트를 2개 이상 실행했을 때 동시 접속이 되는 것처럼 동작한다. 이는 소켓이 상대방 노드가 아니라 메시지 큐에 맞닿아있고, 큐를 통해 들어오는 메시지를 처리하기 때문에 가능한 일이다.

ZMQ의 소켓은 물리적인 포트에 묶이는 UNIX 소켓이 아니라고 했다. 따라서 메시징 타입이 같다면, 하나의 소켓이 (이미 바인딩되지 않은) 다른 포트들에 멀티로 바인딩되는 것도 가능하다.

sock.bind('tcp://*:5555')
sock.bind('tcp://*:7777')

이렇게 동일한 소켓 하나가 두 개 이상의 포트에 묶이도록 호출하는 것이 아무런 문제가 되지 않으며, 클라이언트를 쪼개서 각각 다른 포트들에 접속하도록 했을 때에도 서버 하나로 동작하는게 가능하다.

메시지

ZMQ 소켓을 통해서 주고 받는 데이터는 이진 raw 데이터 스트림이 아니다. ZMQ는 ‘메시지’라 불리는 길이값과 데이터를 결합한 단위의 데이터 타입을 주고 받는다. 따라서 recv()send()에서 별다른 인자 없이 데이터 전체를 하나의 메소드 호출로 주고 받을 수 있게 한다. 또한 이는 문자열을 다루는 매커니즘이 서로 다른 언어 구현간에 발생할 수 있는 문자열 데이터 전달을 처리하는 좋은 돌파구가 된다. 적어도 파이썬을 쓰는 한, 메시지에 대해서는 별다른 처리가 필요하지 않을 것이기 때문에 일단은 넘어가자.

메시지 패턴

컨텍스트가 어떻게 소켓과 각 소켓의 입출력 동작을 마법처럼 관리할 수 있을까? 사실 컨텍스트는 메시지 패턴에 따라서 사용되어야 할 소켓이 어떻게 동작해야 할 것인지를 판단할 수 있다. 메시지 패턴은 두 노드 혹은 여러 노드간에서 메시지를 주고 받는 방향성과 흐름을 정의하는 방법이다. 네트워크 구성 노드들의 연결 방식에 따라서 전체 네트워크의 위상이 달라질 수 있겠지만, ZMQ는 하나의 소켓이 다른 소켓 혹은 소켓들과 어떻게 동작해야 하는지를 몇 개의 패턴으로 구분하고, 그에 맞게 최적화된다. 실질적으로는 매우 많은 패턴들이 있을 수 있겠지만, ZMQ는 몇 개의 기본적인 패턴들을 정의하고 있다.

  1. REQuest – REPly 패턴 : 서버-클라이언트가 각각 한 번씩의 요청과 응답을 주고 받는다.
  2. PUBlisher – SUBscriber 패턴 : 서버가 발행하는 메시지가 각각의 클라이언트로 분산되어 전파된다.
  3. Pipleline : PUSHPULL 패턴의 소켓이 연결되어 단일 방향으로 메시지를 개별전송한다.

이외에도 메시징 네트워크를 확장하기 위한 프록시나 Poller와 같은 디바이스 몇 가지가 정의되어 있다. 분명한 것은 ZMQ는 그렇게 많지 않은 패턴을 제공하고 있음에도 불구하고 매우 다양한 구조를 손쉽게 만들 수 있다는 점이다. 그것은 네트워크 내의 각 노드사이에서 데이터나 신호가 흘러가는 흐름을 디자인하기에 달려 있으며, 우리는 ZMQ를 이용하여 가장 쉽게 할 수 있는 것 (바로 두 노드 간의 통신을 구축하는 것)을 사용하여 얼마든지 다방향 네트워크를 구축할 수 있다는 점이다.

bisect – 이진탐색 알고리듬

이진탐색은 데이터가 정렬된 상태일 때, 특정한 값을 매우 우수한 성능으로 탐색할 수 있는 알고리듬으로 실제로도 널리 이용된다. 이진탐색은 정렬된 데이터에서 특정한 값을 찾을 때 사람이 하는 것과 유사하게 처리되는데, 리스트의 한 가운데 지점의 값을 비교하여 찾는 값이 그보다 작으면 왼쪽에 그보다 크면 오른쪽에 있다는 것으로 탐색 범위를 매 시행마다 절반으로 줄여나가기 때문에 특히 규모가 큰 데이터에서 빠르게 조회가 가능하다는 장점이 있다.1

이전에 이진 탐색 알고리듬 적용을 위한 이진 탐색 트리 클래스를 작성하는 내용으로 포스팅을 발행한 적이 있는데, 파이썬은 bisect라는 내장 라이브러리를 제공한다. 이 라이브러리는 별도의 타입이나 클래스를 요구하지 않으면서 몇 가지 함수를 이용해서 기본 리스트에 대해서 이진 탐색을 수행하는 api들을 제공한다.

bisect : Python Starndard Library 참고

이 모듈은 이미 존재하는 리스트가 있을 때[^2] 주어진 값 x 가 이 리스트상에서 위치해야 할 인덱스 값을 구하는 식의 함수들로 구성되며 크게 두 가지로 나뉜다.

  1. 주어진 리스트 a와 값 x 가 있을 때, x 가 위치해야 할 인덱스를 구하는 함수
  2. 그리고 같은 조건에서 값 x를 올바른 위치에 삽입하는 함수

각각의 함수는 _left, _right 접미사가 붙는데, 이는 주어진 값 x와 같은 값이 리스트 내에 있을 때, x의 위치가 동일한 값으로부터 왼쪽에 있을지 오른쪽에 있을지를 결정하는 기준이 된다. (참고로 접미사가 생략되는 경우 _right라고 간주한다.)

  • bisect_left(arr, x, lo=0, hi=len(arr)) : 리스트 a에서 값 x가 들어갈 인덱스를 구한다. 참고로 lo=, hi=는 리스트의 특정 범위 내에서만 이진 탐색을 수행하려할 때 사용한다. (모든 함수에 공통적으로 추가할 수 있는 인자이다.)
  • bisect_right(arr, x) : bisect()와 같은 함수. bisect_left()와 비슷한 동작을 하나, 같은 값이 있으면 그 값의 오른쪽 위치를 취한다.
  • insort(arr, x) : bisect()로 구해진 위치에 x 값을 삽입한다.
  • insort_left(arr, x) : bisect_left()로 구해진 위치에 x값을 삽입한다.

활용하기

이 모듈을 사용하면 별도의 트리 구조 타입을 만들지 않고도 이진 탐색 알고리듬을 활용할 수 있다.

경우에 따라서는 이런 함수 기반 api 보다 클래스를 만드는 것을 더 선호하는 사람들이 있다. java등의 다른 객체 지향 언어에 대한 경험을 갖고 있을수록 이러한 경향을 보이는 듯 하다. 흥미로운 점은 파이썬은 그 자체로 객체 지향 언어이면서 그 내부에서는 흔히 클래스를 만들어서 문제를 해결하는 것을 그리 권장하지 않는 것 같은 인상을 보인다.

클래스를 쓰기보다는 이렇게 함수 번들로 이루어진 API를 제공한다거나, 제너레이터나 데코레이터를 이용해서 가능하면 클래스 구현을 우회할 수 있는 다양한 방법들을 제시한다.

이진 탐색

이미 정렬된 어떤 리스트에 대해서 임의의 값 x가 존재하는지를 검사하는 함수를 작성해보자.

def binary_search(arr, x):
  i = bisect.bisect_left(arr, x)
  return i < len(arr) and arr[i] == x

bisect_left(a, x)의 결과가 i 라 할 때, a[i]x보다 크거나 작은 값일 수 있다. 따라서 a[i]x와 같은지를 체크해야 한다.  또한 bisect_left() 함수는 xa에 존재하는지 여부는 관심이 없기 때문에 a의 모든 원소보다 x가 크다면 ilen(a)와 같은 값이 나올 것이다. 이 경우는 x가 존재하지 않는 것으로 본다.

리스트를 정렬하기

빈 리스트로부터 insort() 함수를 이용해서 원소를 계속 추가하면 자연스럽게 정렬된 리스트를 얻을 수 있다.  다음 코드는 난수로 생성된 리스트의 각 원소를 insort()를 이용해서 a에 추가해서 정렬된 리스트 a를 얻는 방법을 보인다.

import bisect, random
a = []
b = [random.randrange(1, 50) for _ range(50)]
for x in b:
  bisect.insort(a, x)

점수에 따라 등급나누기

bisect() 를 활용할 수 있는 의외의 방법이 있는데, 바로 특정 점수 구간을 성적을 매길 때 사용하기 좋다는 것이다.  예를 들어 다음과 같은 점수 구간이 있다고 하자. (오른쪽 경계는 포함하지 않음)

  • 0 ~ 50 : F
  • 50 ~ 60 : D
  • 60 ~ 70 : C
  • 70 ~ 90 : B
  • 90 ~ 100 : A

이 때 특정 점수에 따른 그레이드를 결정하기 위해서 흔히 if .. elif .. elif ...로 이어지는 분기문을 많이 쓰는데, 점수 구간 표에서 주어진 점수가 어느 위치에 들어가느냐를 가지고 그 위치를 등급의 인덱스로 써서 간단히 구현할 수 있다.

def get_grade(score):
  r = (50, 60, 70, 90, 100)
  g = 'FDCBA'
  return g[bisect_right(r, score)]

다음페이지에서는 bisect 모듈의 실제 구현 코드를 살펴보자.

bisect 모듈은 실제로 순수 파이썬으로 작성되었다. bisect_leftinsort_left 의 구현을 살펴보자. 알고리듬 자체가 단순/명확하기 때문에 초보자가 보기에도 어려움이 없을 것이다.

def bisect_left(a, x, lo=0, hi=None):
  if lo < 0:
    raise ValueError('lo must be non-negative')
  if hi is None:
    hi = len(a)
  while lo < hi:
    mid = (lo + hi) // 2  ## 찾는 범위의 중간위치를 구해서
    if a[mid] < x : lo = mid + 1  ## 찾는 값이 더 크면 오른쪽 절반으로 범위 축소
    else: hi = mid  ## 크거나 같으면 왼쪽 절반으로 범위 축소. 
    ## 이 때, lo == hi 가 되면 찾은 것이고, lo > hi 가 되면 없는 것이다. 
  return lo


def insort_left(a, x, lo=0, hi=None):
  if lo < 0:
    raise ValueError('lo must be non-negative')
  if hi is None:
    hi = len(a)
  while lo < hi:
    mid = (lo+hi) // 2
    if a[mid[ < x: lo = mid+1
    else: hi = mid
  a.insert(lo, x)

  1. 그래서 컴퓨터 과학에서는 정렬 알고리듬이 매우 중요하게 다뤄진다. 

LCD 패널 방식으로 숫자를 표시해보기 (파이썬)

LCD 처럼 숫자를 표시하는 코드를 만들어 보자

크기와 출력할 숫자를 입력받는다. 크기는 LCD 표시 요소 하나의 크기를 가리킨다. (크기는 1~10,  숫자는 0~99,999,999)  가로선은 -, 세로선은 | 문자를 통해서 표현하며, 사이즈만큼 길이가 길어진다. 따라서 하나의 문자를 표시하기 위해서는 가로는 size + 2, 세로는 size * 2 + 3 만큼의 공간이 필요하다.

예시 – 출력: 0123456789. size: 3

 ---         ---   ---         ---   ---   ---   ---   --- 
|   |     |     |     | |   | |     |         | |   | |   |
|   |     |     |     | |   | |     |         | |   | |   |
|   |     |     |     | |   | |     |         | |   | |   |
             ---   ---   ---   ---   ---         ---   --- 
|   |     | |         |     |     | |   |     | |   |     |
|   |     | |         |     |     | |   |     | |   |     |
|   |     | |         |     |     | |   |     | |   |     |
 ---         ---   ---         ---   ---         ---   ---

접근

왼쪽과 같이 LCD 숫자 하나를 표현하기 위해서는 총 7개의 패널이 필요하다. 각 패널의 번호를 왼쪽과 같이 지정한다고 하자. 어떤 숫자를 표현하고자 한다면 각 패널이 어떤 숫자에서는 켜질 것인지, 켜지지 않을 것인지를 구분해야 한다. 예를 들어 위쪽 가로 방향 패널인 0의 경우에는 1을 그려야 하는 시점에는 켤 필요가 없다. 하지만 0이나 2, 3 등을 그려야 하는 시점에서는 켜져야 한다.

즉 0번 패널이 켜져야 하는 숫자는 아래 그림에서 알 수 있듯이, 0, 2, 3, 4, 6, 7, 8, 9 가 된다.  같은 방식으로 각 패널마다 어떤 숫자에 켜져야 할 것인지를 미리 정해두도록 하자.

이렇게 각 패널마다 켜져야 하는 숫자들을 정리하는 것은 생각보다 간단하다. 위 그림을 이용해서 쉽게 리스트를 완성할 수 있다.

masks = ("02356789", "045689", "01234789", "2345689", "02689", "013456789", "0235689")

이제 숫자를 출력할 차례이다. 우선 한개의 숫자를 출력하는 과정을 생각해보자. 숫자를 출력하는 과정은 가로선을 표현하는 구간 3군데와 세로선을 표현하는 구간 2군데로 나뉜다.

가로선 표현

패널 1개의 크기가 S인 숫자의 폭은 S + 2 (양쪽의 1씩 세로선이 표시되어야 하므로)이다. 따라서 가로선의 경우 표시하려는 숫자의 해당 가로 패널이 켜지는 경우에는 (공백) + (-) * S + (공백)을 출력하게 된다. 만약 패널이 꺼져 있는 경우에는 (-) 대신에 공백이 출력될 것이다. 따라서 맨 위에 가로선을 표시하는 것은 출력하려는 숫자 d 에 따라서 다음과 같은 문자열을 만들게 된다.

' ' + ('-' if d in masks[0] else ' ') * s + ' '
## "#---#" 또는 "#####" 

세로선 표현

세로선을 표현할 때는 한 줄에 1번 2번 혹은 4번 5번 패널을 같이 판단해야 한다. 표시되는 문자의 상황은 가로선과 반대로 양끝에 패널이 있고 가운데는 크기 값 만큼의 공백이 들어온다. 따라서 사이즈 3일 때의 둘째줄은 다음과 같이 만들어진다.

('|' if d in masks[1] else ' ') + ' ' * s + ('|' if d in masks[2] else ' ')

그리고 이렇게 만들어지는 세로선은 사이즈 값만큼 반복하여 라인을 만들어야 한다. 사이즈 3인 경우에는 2,3,4 번 줄이 같은 내용을 반복 출력한다.

이후 가운데 가로선과, 다시 아래쪽 세로선, 마지막으로 바닥자리 가로선을 같은 식으로 표시한다.

정리

한 번에 한 글자에 대해서 한줄씩 문자열을 만드는 방법을 알아보았으니, 이번에는 여러 글자를 출력할 때 한줄은 어떻게 만드는지에 대해서 알아보자. 이미 모든 글자는 같은 규격으로 생성되며, 매번 줄을 만드는 방법을 알았다. 따라서 여러 글자의 경우에는 각 글자에서 해당줄의 문자열을 생성하고 공백으로 구분하여 하나로 합치면 된다. 이는 str.join() 을 이용하면 된다. 출력해야하는 숫자 문자열을 digits 라고 하면, 해당 코드는 리스트 축약을 통해서 손쉽게 표현할 수 있다.

## 가로줄
' '.join(
    [(' ' + ('-' if d in mask[0] else ' ') * s + ' ') for d in digits]
}

## 세로줄
' '.join(
    [('|' if d in mask[1] else ' ') + ' ' * s + ('|' if d in mask[2] else ' ')\
     for d in digits]
)

이렇게 각 줄을 생성하여 리스트에 담고 개행문자를 끼워서 하나로 합치면 출력해야하는 최종 문자열이 완성된다. 이제 코드를 정리해보자.

def make_presentation(digits, s=1):
  masks = ("02356789", "045689", "01234789", 
           "2345689", "02689", "013456789", "0235689")
  result = []  
  ## 윗줄
  result.append(' '.join([' '+('-' if d in masks[0] else '-') * s + ' '\
                          for d in digits]))
  ## 위쪽 세로줄
  for _ in range(s):
    result.append(' '.join([ ('|' if d in masks[1] else ' ') + ' ' * s +\
                             ('|' if d in masks[2] else ' ') + ' '\
                            for d in digits]))
  
  ## 가운데 가로줄
  result.append(' '.join([' '+('-' if d in masks[3] else '-') * s + ' '\
                          for d in digits]))

  ## 위쪽 세로줄
  for _ in range(s):
    result.append(' '.join([ ('|' if d in masks[4] else ' ') + ' ' * s +\
                             ('|' if d in masks[5] else ' ') + ' '\
                            for d in digits]))

  ## 바닥줄
  result.append(' '.join([' '+('-' if d in masks[6] else '-') * s + ' '\
                          for d in digits]))

  return '\n'.join(result)

def main():
  digits, size = input().split()[:2]
  size = int(size)
  print(make_presentation(digits, size))

정리

중복되는 복잡한 코드가 너무 많아서 이를 별도의 람다식으로 빼내었다.