C로 작성된 함수를 파이썬에서 사용하기

표준 파이썬 해석기는 C로 만들어져 있다. 그리고 C에는 동적 라이브러리를 링크하여 사용할 수 있는 기능이 마련되어 있다. 따라서 표준 파이썬 해석기를 만드는 사람들도 실행시간에 C로 만들어진 동적 라이브러리를 링크하여 사용할 수 있을 것이라는 생각을 당연히 했을 것이고, 그 결과 ctypes 라는 라이브러리가 탑재되어 있다. 

C로 작성된 함수를 파이썬에서 사용하기 더보기

파이썬의 숫자판별함수

파이썬에서 주어진 문자열이 숫자로 되어 있는지를 검사하는데에는 흔히 문자열의 메소드인 isdigit()을 사용한다. 그런데 파이썬의 문자열 타입을 조사해보면 같은 기능을 하는 거 같은 메소드가 무려 3종 세트로 알차게 구성되어 있는 것을 알 수 있다. 바로 isdigit(), isnumeric(), isdecimal() 이다. 그렇다면 이 세 가지 함수는 왜 각각 존재하는 것일까?

급한 사람을 위한 빠른 결론 – int 타입으로 안전하게 변환하고 싶다면 isdigit() 대신에 isdecimal()을 사용하여 검사하라

이 세 종류의 메소드는 문자값 중에서 ‘숫자’를 찾는데 사용된다. 우리 말에서 ‘숫자’라는 개념은 사실 모호한데 (응? 이게 왜 모호하다는 거지?) 우리는 일상생활에서 흔히 0, 1, 2, 3, .. , 9의 10개의 숫자를 사용하고 있다. 즉 우리는 10개의 ‘숫자’만을 갖고 있는 셈이다. 그런데 사실은 이게 좀 간단한 문제가 아닌거라는게 문제다.

흔히 사용하는 isdigit() 메소드를 보자. 말 그대로 문자열에 사용된 글자들이 ‘digit’인지를 확인하는 것이다. 아 그러니까 우리는 ‘이게 숫자로 된 문자열이네’라는 것을 알 수 있을 거라 추측한다. 그리고 digit, numeric, decimal은 다같이 ‘숫자’로 번역되기 때문에 이 사이의 차이가 뭔지를 정확하게 모르게 된다.

다시 이번에는 decimal 을 보자. 이것이 우리가 흔히 생각하는 ‘숫자’와 같은 개념이다. 즉 '0123456789'의 구성으로 이루어진 10개의 글자. 말 그대로 int 타입으로 즉시 변환이 가능한 리터럴을 구성하는 문자를 말한다.

아니 그렇다면 digit은 뭐길래? 특수 기호 중에서 어깨위에 제곱이나 세제곱을 표시하는 문자가 있다. digit은 이렇게 ‘숫자처럼 생긴’ 모든 글자를 다 숫자로 치는 것이다. 다음 예를 보면 digit과 decimal의 차이를 알 수 있을 것이다.

x = '3²'
x.isdigit()
# True
x.isdecimal()
# False
int(x)
# ERROR!!!!

위 예에서 볼 수 있는  에서 두 번째 ² 는 특수문자이며, 우리가 흔히 키보드로 입력하는 2와는 다른 코드 값이다. 이는 decimal 값을 표기하는데 사용하는 문자는 아니지만, 사람이 읽었을 때 숫자로 인지하기 때문에 isdecimal()에서는 False가 리턴되고, isdigit()에서는 True가 리턴된다.

따라서 만약 어떤 텍스트가 int 값으로 변환이 가능한지를 검사하고자 한다면 isdigit()을 사용해서는 안되며, isdecimal()을 써야 할 것이다.

그럼 isnumeric()은 어떤 것일까?

numeric 하다는 것은 보다 넓은 의미인데, isdigit()은 단일 글자가 ‘숫자’ 모양으로 생겼으면 True를 리턴한다고 했다. isnumeric()은 숫자값 표현에 해당하는 텍스트까지 인정해준다. 예를 들어 “½” 이런 특수문자도 isnumeric()에서는 True로 판정된다.

파이썬은 처음이라 – 느긋하긴 처음이라

흔하지 않은 컨셉이기는 하나 느긋함(lazyness)이라는 컨셉은 코드를 바라보는 시각을 크게 바꿀 수 있는 중요한 지점이 될 수 있다. 이 글에서는 파이썬에서 느긋함이란 무엇이며, 파이썬에서는 어떻게 적용되는지, 그리고 이 컨셉을 통해서 기존 코드를 어떤식으로 개선할 수 있는지에 대해 살펴보도록 하겠다.  다음은 이 글의 내용과 예제 코드를 이해하는데 필요한 몇 가지 사전 정보이다. 

파이썬은 처음이라 – 느긋하긴 처음이라 더보기

aiohttp에서 큰 파일을 업로드하는 법

파일 업로드는 보통 요청의 body에 인코딩된 파일 데이터를 넣어서 POST 요청으로 서버에 전달되는데, aiohttp에서는 다음과 같이 post를 처리하는 핸들러를 사용해서 이를 처리할 수 있다.

async def store_mp3_handler(request):
  data = await request.post()
  mp3 = data['mp3']
  filename = mp3.filename
  mp3_file = mp3.file
  content = mp3_file.read()
  return web.Response(body=content, headers=
     MultiDict({ 'CONTENT-DISPOSITION': mp3_file}))

여기서 문제는 request.post() 메소드가 요청 데이터를 한꺼번에 메모리로 읽어들이기 때문에 메모리 부족으로 서버가 죽을 수 있는 상황이 있다는 것이다. 따라서 aiohttp에서 일반적으로 처리할 수 있는 요청의 크기는 2MB로 제한된다. 하지만 이 크기는 어지간한 사진 하나의 용량도 감당하기 어렵기 때문에 뭔가 다른 방법이 필요하다. (보통은 일종의 옵션 값 같은 걸로 최대 처리 요청 크기를 변경할 수 있을 줄 알았는데, 없었다.)

request.multipart는 이러한 문제를 피하는 멀티파트 리더로 기능할 수 있다. 리더 객체를 생성한 다음에는 멀티파트 요청을 단위 콘텐츠 별로 읽어들일 수 있다. 개별 파트의 헤더를 먼저 읽고, 다시 최종적으로 file 필드로부터 버퍼로 파일의 일부 내용을 순차적으로 읽어서 처리할 수 있다.

async def store_mp3_handler(request):
  reader = await request.multipart()

  field = await reader.next()
  assert field.name == 'name'
  name = await field.read(decode=True)
  
  field = await reader.next()
  assert field.name == 'mp3'
  filename = filed.filename

  size = 0
  with open(os.path.join('/spool/yarrr-media/mp3/', filename), 'wb') as f:
    while True:
      chunk = await field.read_chunk()
      if not chunck:
        break
      size += len(chunk)
      f.write(chunk)
  return web.Response(text=f'{filename} sized of {size} successfully stored.')

ZMQ 멀티파트메시지

멀티파트 메시지는 하나의 메시지 프레임 내부에 여러 개의 독립적인 메시지 프레임이 들어 있는 것을 말한다. 이는 하나의 프레임에서 처리하기 힘든 데이터 조각들을 모아서 처리할 때 유용하다. 예를 들어 바이너리 파일 데이터를 전송하려는 경우에는 보내는 쪽이나 받는 쪽이나 전송하는 데이터가 이진데이터라는 것을 알고 있다 가정하여 바이트 스트림을 전송할 수 있다. 하지만 이렇게 하면 실제 데이터 외부에 있었던 정보, 이를 테면 파일 이름이나 생성한 날짜 같은 메타 정보를 전달하기가 어려워진다. 이런 경우 여러 정보들을 멀티 파트 메시지로 묶어서 하나의 프레임으로 전송하면 필요한 모든 정보를 같이 전달해 줄 수 있다.

멀티 파트 메시지 전송은 비단 ZMQ내에서만 사용되는 개념이 아니다. HTTP 규격에서도 멀티 파트 메시지 개념이 존재하며, 실제로 HTML 폼 전송에서 멀티 파트 전송이 널리 사용된다. 여러 개의 필드를 하나의 폼에서 묶어서 submit 한다거나, 여러 개의 파일을 한 번에 업로드하는 폼 들은 모두 enctype='multpart/formdata' 라는 폼 속성을 가지며, 실제 HTTP 전송 헤더 역시 Content-Type 에서 이 값이 사용된다.

ZMQ 에서도 여러 개의 메시지 혹은 프레임을 하나의 메시지로 묶어서 멀티파트로 전송할 수 있다. pyzmq를 사용하는 경우 이는 소켓 객체의 send_multipart() / recv_multipart()  두 개의 메소드로 간단히 구현된다. 다만 문자열이 아닌 버퍼(바이트 배열)를 주고 받아야 한다는 점에 주의만 하면 그외에는 매우 간단하다. 다음은 세 개의 문자열을 하나의 멀티파트 메시지로 전송하는 REQ-REP 예제이다.

## multimessage-server.py

import zmq

ctx = zmq.Context()

def run_server(port=5555):
  sock = ctx.socket(zmq.REP)
  sock.bind(f'tcp://*:{port}')
  keys = 'abc'
  while True:
    msg = dict(zip(keys, (x.decode() for x in sock.recv_multipart())))
    for k, v in msg.items():
      print(f'{k}: {v}')
    ## 대문자화하고 하나의 문자열로 만들어서 되돌려준다.
    sock.send_string(' '.join(msg[k].upper() for k in keys))

if __name__ == '__main__':
  run_server()

recv_multipart() 에 의해 리턴되는 값이 바이트배열의 시퀀스임을 알고 있다면 위 코드는 충분히 이해되리라 본다. REQ-REP 패턴에서 클라이언트에서 서버로 멀티 파트 메시지를 전송했다 하더라도, 반드시 응답이 멀티파트여야 할 필요도 없다. 다음은 위 서버 코드와 짝을 맞출 클라이언트 코드이다. 세 번의 키보드 입력 후 해당 문자열들을 한 번에 전송한다. 그리고 서버가 보내준 단일 메시지를 출력하는 것을 반복하는 간단한 코드이다.

import zmq

ctx = zmq.Context()

def run_client(port=5555):
  sock = ctx.socket(zmq.REQ)
  sock.connect(f'tcp://localhost:{port}')
  while True:
    data = [input().encode() for _ in range(3)]
    sock.send_multipart(data)
    res = sock.recv_string()
    print(res)

if __name__ == '__main__':
  run_client()