개념정리

  • 코드 포인트(code point): 각 유니코드 문자에는 고유한 코드포인트(숫자)가 할당되어있습니다. 이 유니코드 하나하나에 매핑되는 숫자를 코드포인트라고합니다. 1:1로 매핑되어있고, 코드포인트를 어떤 인코딩 방식을 하냐에 따라, 문자열이 달라질 수 있습니다.
  • 인코딩: 코드포인트를 바이트 시퀀스로 변환하는 규식을 일컫습니다.
  • 디코딩: 바이트 시퀀스를 코드포인트로 역변환 하는 규칙을 일컫습니다
  • 유니코드(Unicode): 모든 문자를 컴퓨터에서 일관되게 표현하고 다루기 위한 국제 표준입니다. 모든 문자열은 코드포인트를 이용하여, 1:1 매핑되어있습니다. 그리고 유니코드로 표현되는 경우 "U+"라는 접두사를 사용합니다.

 

코드포인트: 문자와 1:1로 매핑하는 숫자

코드포인트는 문자와 1:1로 매핑되는 숫자입니다. 예를 들어, `U+1f62e`와 같이 생긴 숫자가 코드포인트입니다. 유니코드의 코드포인트는 유니코드 표준으로 "U+"라는 접두사를 붙여서 4자리에서 6자리의 16진수로 표현합니다. `U+` 뒤에 1f62e은 각각이 16문자 문자열입니다. 이 문자는 😮의 이모지를 뜻합니다.

 

이 코드포인트를 UTF-8로 인코딩한다면, f0 9f 98 ae가 됩니다.

>>> code_point = int("1f62e", 16) # "U+1f62e"은 16진수로 표현합니다.
>>> print(code_point)
128558

>>> chr(code_point)
😮

>>> chr(code_point).encode("utf-8")
b'\xf0\x9f\x98\xae'

 

UTF-8은 가변 길이 인코딩 방식으로, 각 유니코드 코드 포인트를 1바이트에서 4바이트까지 다양한 길이로 인코딩합니다. 아래에 간단한 UTF-8 인코딩 테이블을 제시하겠습니다:

유니코드 범위 (16진수)UTF-8 바이트 시퀀스 (이진수)

U+0000 - U+007F 0xxxxxxx
U+0080 - U+07FF 110xxxxx 10xxxxxx
U+0800 - U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 - U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

여기서 "x"는 유니코드 코드 포인트의 비트를 나타냅니다. UTF-8에서 첫 번째 바이트는 0, 110, 1110, 또는 11110으로 시작하며, 다음 바이트는 항상 10으로 시작합니다. 이 테이블을 사용하여 유니코드 문자를 UTF-8로 인코딩할 수 있습니다.

이제 "b'\xf0\x9f\x98\xae'"을 디코딩 과정을 설명해 보겠습니다. UTF-8에서 각 문자의 첫 번째 바이트를 보고 이 바이트가 어떤 범위에 속하는지에 따라 그 문자가 몇 바이트인지 결정됩니다. 그런 다음 각 바이트에서 필요한 비트를 추출하여 이를 유니코드 코드 포인트로 변환합니다.

UTF-8 디코딩의 과정은 다음과 같습니다:

  1. 첫 번째 바이트를 확인합니다. => '\xf0'이며, 이는 이진수로 '1111 0000'을 나타냅니다. 이 바이트는 4바이트 문자의 첫 번째 바이트를 나타냅니다. 
  2. 첫 번째 바이트를 보고 문자의 길이를 결정합니다. => '11110000'은 네 번째 범위에 속하므로 이 문자는 총 4바이트로 구성됩니다. 
  3. 첫 번째 바이트의 마지막 4비트를 제외한 나머지 비트는 유니코드 코드 포인트를 나타냅니다. 여기서는 '1110'으로 시작하므로 이후 3바이트의 유니코드 코드 포인트가 함께 사용됩니다.
  4. 다음 바이트부터는 모두 '10'으로 시작해야 합니다. 여기서는 '\x9f', '\x98', '\xae'가 모두 이 조건을 충족합니다.
  5. 각 바이트에서 첫 번째 바이트 이후의 유니코드 코드 포인트를 나타내는 비트를 추출합니다. 이 비트는 모두 '10xxxxxx' 형식입니다. 따라서 각 바이트의 마지막 6비트를 추출하여 이를 합칩니다.
  6. 마지막으로 이진수를 십진수로 변환한 다음 이를 유니코드 코드 포인트로 해석합니다.
  7. 유니코드 코드 포인트를 유니코드 문자로 변환합니다.

 

또 다른 파이썬에서의 예시를 들어보겠습니다. 안녕하세요의 문자열이 있는 경우 `ord()`함수를 이용해서 10진법으로 표현할 수 있습니다. hex은 정수를 입력받아 16진법으로 표현합니다. 그렇기에, 이 10진법으로 표현한 것을 다시 16진법으로 표현하기위해서 `hex()`함수를 이용해서 변환합니다. 그리고 출력하면 아래와 같이 얻을 수 있습니다. 그리고 각 10진법으로 표현하면 숫자로만 열거됩니다. 

>>> text = "안녕하세요"
>>> code_points = [hex(ord(char)) for char in text]
>>> print(code_points)
['0xc548', '0xb155', '0xd558', '0xc138', '0xc694']

code_points = [ord(c) for c in text]
>>> print(code_points)
[50504, 45397, 54616, 49464, 50836]

 

`0x`은 파이썬에서 16진법을 표현하는 방식입니다.  "안"에 해당하는 문자는 10진법으로 50504이며, 이를 16진법으로 표현하면 "C548"입니다.

windows 계산기를 이용하여 50504을 16진법으로 변경한 예시

 

"안녕하세요"를 utf-8로 인코딩하면 아래와 같습니다. b로 시작하는 것은 바이트 리터럴을 나타냅니다. 파이썬에서 바이트 리터럴은 바이트 문자열을 나타내며, 이는 일련의 8비트 바이트로 구성된 바이트 시퀀스입니다.

 

인코딩을 하고하면, b''로 표현되는데 어떤문자는 문자 그대로 표현되고, 어떤 경우는 16진법으로 표현됩니다. 이 차이는 무엇일까요? 

아래의 예시를 하나 보겠습니다. apple은 utf-8의 경우는 b'apple이 출력됩니다. b''여도 16진수로 표현되는 경우가 있고, 그렇지 않은 경우가 있는데요. 아스키문자(공백부터 물결표(~))까지는 아스키 문자 그대로 출력이 가능한 경우는 그대로 출력됩니다. 그렇지 않은 경우는 16진수로 표현되기 때문입니다. 아스키코드 확인(URL)

>>> text ="안녕하세요"
>>> text.encode("utf-8")
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> text2 = 'apple'
>>> text2.encode('utf-8')
b'apple'

>>> text2.encode('utf-8')[0]
97

>>> text2.encode('ascii')[0]
97

 

아래의 아스키코드를 보면 Dec, Hx, Oct, Char가 있습니다. Dec은 Decimal(10진법)이며, Hx은 Heximal(16진법)입니다. Oct은 Octal으로 8진법입니다. apple을 utf-8로 인코딩한다음에 표현해보면, 97이나오고, 97에해당한는 문자(char)은 'a'입니다. 마찬가지로, 이를 ascii로 인코딩해도 97이나옵니다.

UTF-8와 ASCII는 대부분의 영문 알파벳 및 일부 특수 문자에 대해서는 동일한 방식으로 인코딩됩니다. ASCII는 UTF-8의 서브셋이며, UTF-8은 ASCII를 포함하고 있기 때문에 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩이 동일한 결과를 생성합니다.

따라서 'utf-8'과 'ascii' 인코딩을 사용하여 같은 문자열을 인코딩할 경우, 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩 결과가 동일할 수 있습니다. 이 경우에는 두 결과가 모두 97로 동일하게 나타나게 됩니다.

 

파이썬에서의 자료형: bytes, bytearray

  1. bytes:
    • bytes는 불변(immutable)한 바이트 시퀀스입니다. 즉, 한 번 생성되면 내용을 변경할 수 없습니다.
    • 바이트 객체는 0부터 255까지의 숫자로 이루어진 값들의 불변된 시퀀스입니다.
    • 주로 파일 입출력, 네트워크 통신 등에서 사용됩니다.
    • 예를 들어, b'hello'는 바이트 리터럴로, 문자열 'hello'를 바이트로 표현한 것입니다.
  2. bytearray:
    • bytearray는 변경 가능한(mutable)한 바이트 시퀀스입니다. 즉, 내용을 변경할 수 있습니다. 
    • 0부터 255까지 숫자로 이루어진 값들의 시퀀스입니다.
    • 바이트 배열은 bytes와 유사하지만, 변경 가능하고 가변성이 있습니다.
    • bytearray는 리스트와 유사하게 인덱싱 및 슬라이싱을 통해 요소를 수정하거나 추가할 수 있습니다.
    • 주로 이진 데이터를 변경하거나 조작할 때 사용됩니다.
    • 예를 들어, bytearray(b'hello')는 바이트 배열을 생성하고 초기값으로 문자열 'hello'를 사용한 것입니다.

간단히 말해, bytes는 불변하고 bytearray는 변경 가능한 바이트 시퀀스를 나타냅니다. 때에 따라서는 데이터를 수정해야 하는 경우 bytearray를 사용하고, 데이터를 변경할 필요가 없는 경우에는 bytes를 사용하는 것이 좋습니다.

In [23]: s = "안녕하세요"

In [24]: len(s)
Out[24]: 5

In [25]: s.encode('utf8')
Out[25]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

In [26]: encoded_s = s.encode('utf8')

In [27]: type(encoded_s)
Out[27]: bytes

In [28]: bytes("안녕하세요", encoding='utf-8')
Out[28]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

 

바이트(bytes)은 바이트어레이(bytearray)은 슬라이싱 및 인덱싱에 따라 아래와 같은 결과를 보입니다.

  • bytes의 슬라이싱: bytes
  • bytes의 인덱싱: int
  • bytearray의 슬라이싱: bytearray
  • bytearray의 인덱싱: int
>>> s = "안녕하세요"
>>> bytes(s, encoding='utf-8')
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> bytes(s, encoding='utf-8')[0]
236

>>> bytes(s, encoding='utf-8')[:1]
b'\xec'

>>> bytearray(s, encoding='utf-8')
bytearray(b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94')

>>> bytearray(s, encoding='utf-8')[:1]
bytearray(b'\xec')

>>> bytearray(s, encoding='utf-8')[0]
236

 

Struct module

파이썬 내장 모듈인 `struct` 모듈은 바이트로 전환하거나, 바이트를 문자열로 변경할 때, 주로 사용됩니다. 포맷을 지정하는 등의 역할도 가능합니다. `struct`모듈은 주로 아래와 같은 기능을 합니다.

  1. 패킹(Packing):
    • struct.pack(format, v1, v2, ...) 함수를 사용하여 파이썬 데이터를 이진 데이터로 변환합니다.
    • 이진 데이터는 바이트 문자열 또는 바이트 배열로 반환됩니다.
    • 주어진 형식(format)에 따라 데이터가 패킹됩니다. 예를 들어, I는 4바이트의 부호 없는 정수를 나타내는 형식입니다.
  2. 언패킹(Unpacking):
    • struct.unpack(format, buffer) 함수를 사용하여 이진 데이터를 파이썬 데이터 타입으로 변환합니다.
    • 주어진 형식에 따라 바이트 데이터가 해체됩니다.
    • 이 함수는 바이트 문자열 또는 바이트 배열을 받아서 주어진 형식에 따라 데이터를 언패킹합니다.
  3. 형식 지정자(Format Specifiers):
    • 패킹 및 언패킹 작업에 사용되는 형식 지정자는 바이트 순서, 데이터 유형 및 크기를 정의합니다.
    • 예를 들어, I는 부호 없는 4바이트 정수를 나타내며, f는 4바이트 부동 소수점을 나타냅니다.

 

Memory view

메모리뷰는 바이트 시퀀스를 생성/저장하는게 아니라(복사X), 공유메모리 방식으로 버퍼데이터를 조작할 때 주로 사용됩니다. 복사가 아니기에 직접 값을 변경은 불가능합니다.

메모리는 메모리에 대한 접근을 보다 효율적으로 다루는 데 사용됩니다. 아래는 memoryview의 간단한 예시입니다.

# 바이트 데이터로부터 memoryview 생성
>>> data = b'hello world'
>>> mv = memoryview(data)

# memoryview의 내용 확인
>>> print(mv)  # <memory at 0x7fbba30d2080>>

# memoryview를 이용하여 데이터 접근
>>> print(mv[0])  # 104 (b'h'의 ASCII 코드 값)
>>> print(mv[1:5])  # <memory at 0x7fbba98234c0>>
>>> print(bytes(mv[1:5]))  # b'ello' (슬라이싱된 바이트 데이터)

# memoryview를 이용하여 데이터 수정
>>> mv[6] = 66  # ASCII 코드 66은 'B' (원본 데이터가 수정불가, TypeError 발생)

 

예시: GIF포맷처리

GIF은 움짤로 주로 사용되는데요. GIF포맷을 struct을 이용해서 언패킹해보겠습니다.

GIF포맷은 아래와같이 포맷이 지정되어있습니다.

  • 0번부터 3바이트는 GIF을 의미하며,
  • 3번부터 3개의 바이트는 87a, 89a GIf 버전을 의미합니다. 
  • offset 6부터 2바이트: logicla width 
  • offset 8부터 2바이트: logical height

이를 파이썬으로 접근하면 아래와 같습니다. fmt의 "<"은 리틀엔디언, "3s"은 3개의 바이트, "H"은 하나의 정수를 의미합니다.

>>> import struct

>>>fmt = "<3s3sHH"

>>>with open("200w.gif", "rb") as fh:
   ...:     img = memoryview(fh.read())
   ...: 


>>> header = img[:10]
>>> header
<memory at 0x7f8b757bfac0>

>>> bytes(header)
b'GIF89a\xc8\x00\xff\x00'

>>> struct.unpack(fmt, header)
(b'GIF', b'89a', 200, 255)
  • <: 리틀 엔디안(Endian)을 나타냅니다. 리틀 엔디안은 낮은 주소부터 데이터를 읽음을 의미합니다. (파일 형식에 따라 다를 수 있으며, 여기서는 일반적으로 사용되는 리틀 엔디안을 가정합니다.)
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 형식을 나타내는 문자열입니다. "GIF"을 읽어왔습니다.
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 버전을 나타내는 문자열입니다. "89a"을 읽어왔습니다.
  • HH: 각각 2바이트(16비트)의 부호 없는(short) 정수를 나타냅니다. 여기서는 GIF 이미지의 가로 및 세로 크기를 나타내는데 사용됩니다.

src: https://www.researchgate.net/figure/GIF-Header-Format-adapted-from-14_tbl1_228678318

 

문제: tiff파일에서 바이트 조직

이 사이트에서 다운로드받은 tiff파일의 이미지 헤더를 다음을 참조하여(스펙), 이 파일 포맷에서 다음을 구하세요.

  • 바이트 오더: 리틀엔디언, 빅엔디언
  • tiff 파일 여부:
  • IFD 시작 오프셋:

 

 

 

이중 모드 스트링 / 바이트 API

바이트로 정규표현식을 만들면 "\d"와 같은 글자나 "\w"은 아스키문자만 매칭되지만 str로 이 패턴을 만들면 아스키문자외에 유니코드나 숫자도 매칭이된다. 

 

import re

# bytes로 정규표현식 패턴 컴파일하기
pattern_bytes = re.compile(rb'\d')

# bytes 대상으로 매칭 시도하고 모든 매칭 결과 찾기
binary_data = b'123 abc 456'
matches_bytes = pattern_bytes.findall(binary_data)

print("Matches with bytes regex pattern:", matches_bytes)
Matches with bytes regex pattern: [b'1', b'2', b'3', b'4', b'5', b'6']

 

import re

# str로 정규표현식 패턴 컴파일하기
pattern_str = re.compile(r'\d')

# str 대상으로 매칭 시도하고 모든 매칭 결과 찾기
text = '123 abc 456'
matches_str = pattern_str.findall(text)

print("Matches with str regex pattern:", matches_str)
Matches with str regex pattern: ['1', '2', '3', '4', '5', '6']
반응형

요약


  • 메인루틴(Main routine): 메인루틴은 보통 프로그램의 시작점이며, 프로그램의 주 흐름을 담당합니다. 메인루틴은 일련의 작업을 수행하고 다른 서브루틴이나 코루틴을 호출할 수 있습니다. 프로그램이 시작되는 메인코드라고 생각하면 됩니다.
  • 서브루틴(subroutine): 메인루틴에서 호출되는 함수, 또는 서브루틴에서 호출되는 함수들을 의미합니다. 즉, 다른 루틴에서 호출되는 경우를 의미합니다.
  • 코루틴(Coroutine): 메인루틴에서 호출되지만, 코루틴은 실행되는 도중에, 일시중단되어 다시 메인루틴으로 전환되었다가 다시 코루틴으러 전환될 수 있는 "제어흐름"이 가능한 루틴을 의미합니다.

 

코루틴, 메인루틴, 서브루틴은 프로그래밍에서 중요한 개념이며, 이를 이해하는 것은 프로그래밍의 성능을 향상시키는 데 도움이 됩니다. 각각의 이유를 자세히 설명하겠습니다.

  1. 프로그램의 구조 이해(메인루틴/서브루틴의 이해):
    • 메인루틴은 프로그램의 주요 흐름을 담당하며, 프로그램의 시작점입니다. 이를 이해함으로써 프로그램이 어떻게 실행되는지 전반적으로 이해할 수 있습니다.
    • 서브루틴은 재사용 가능한 작은 조각의 코드를 나타냅니다. 이를 통해 코드를 더 모듈화하고 유지 보수하기 쉽게 만들 수 있습니다.
  2. 코드의 재사용과 모듈화(서브루틴/코루틴의 이해):
    • 서브루틴을 사용하면 비슷한 작업을 수행하는 코드를 여러 곳에서 재사용할 수 있습니다. 이로써 코드의 중복을 피하고 유지보수성을 향상시킬 수 있습니다.
    • 코루틴은 서브루틴과 유사하지만, 일시 중단되고 다시 시작될 수 있기 때문에 작업의 흐름을 제어하는 데 유용합니다. 이를 통해 비동기 작업이나 상태 유지가 필요한 작업을 효율적으로 처리할 수 있습니다.
  3. 동시성과 비동기 프로그래밍(코루틴의 의해):
    • 코루틴은 동시성과 비동기 프로그래밍에서 중요한 역할을 합니다. 코루틴을 이용하면 여러 작업을 동시에 실행하고 작업 간에 상태를 공유할 수 있습니다. 이를 통해 복잡한 동작을 효과적으로 조정하고 성능을 향상시킬 수 있습니다.
  4. 효율적인 코드 실행:
    • 코루틴은 작업의 일시 중단과 다시 시작을 가능하게 함으로써 I/O 바운드 작업을 효율적으로 처리할 수 있습니다. 이를 통해 대기 시간이 긴 작업을 실행하는 동안 다른 작업을 수행할 수 있으며, 시스템 자원을 더 효율적으로 활용할 수 있습니다.

이러한 이유들로 인해 코루틴, 메인루틴, 서브루틴을 이해하는 것은 프로그래밍에서 중요한 역할을 합니다. 이러한 개념을 잘 숙지하고 활용함으로써 보다 효율적이고 유연한 코드를 작성할 수 있습니다.

 

메인루틴(Main routine)


메인루틴은 프로그램의 주요 흐름을 담당하는 부분으로, 일반적으로 프로그램의 시작점이며 종료 지점이 될 수도 있습니다. 메인루틴은 전체 프로그램의 실행 흐름을 정의하고 조절하며, 다른 서브루틴이나 함수를 호출하여 작업을 수행하고 그 결과를 처리합니다.

메인루틴은 보통 다음과 같은 역할을 수행합니다:

  1. 프로그램의 시작점: 메인루틴은 일반적으로 프로그램이 실행되면 가장 먼저 실행되는 부분입니다. 프로그램의 시작 지점을 정의하고 초기화 작업을 수행합니다.
  2. 주요 작업 수행: 메인루틴은 프로그램이 수행해야 하는 주요 작업을 정의하고 호출합니다. 이는 사용자의 입력을 받는, 데이터를 처리하는, 다른 모듈이나 서브루틴을 호출하는 등의 작업을 포함할 수 있습니다.
  3. 서브루틴 호출: 메인루틴은 필요에 따라 다른 서브루틴이나 함수를 호출하여 작업을 분할하고 모듈화합니다. 이를 통해 코드를 재사용하고 유지보수성을 향상시킬 수 있습니다.
  4. 프로그램의 종료: 메인루틴은 프로그램이 종료되는 지점을 나타냅니다. 모든 주요 작업이 완료된 후 정리 작업을 수행하고, 필요에 따라 리소스를 해제하고 프로그램을 종료합니다.

간단한 예시로 메인루틴을 파이썬코드로 구현해보겠습니다. 아래와 같이 input으로 이름, 나이를 입력받아 출력하는 프로그램입니다. 다른 모듈에서 호출되는 것 없이 main()함수가 호출되고 종료됩니다. 이 main함수에서 프로그램이 시작되고, 종료되기에, "프로그램의 흐름이 정의"되었다라고 말할 수 있습니다. 즉, main()이 메인루틴이 됩니다.

def main():
    print("프로그램 시작")

    # 사용자 입력 받기
    name = input("이름을 입력하세요: ")
    age = int(input("나이를 입력하세요: "))

    # 정보 출력
    print("입력된 정보:")
    print("이름:", name)
    print("나이:", age)

    # 종료 메시지 출력
    print("프로그램 종료")

if __name__ == "__main__":
    main()

 

서브루틴(Subroutine): 메인루틴이나 서브루틴에서 호출되는 함수


서브루틴은 다른 프로그램에서 호출되는 (=피호출)되는 함수를 의미합니다. 예는 아래와 같습니다. 위의 예시와 같이 main()이 호출되고 종료되기에 메인루틴은 main()이라는 함수입니다. 그리고 이 main()내에서 calculate_sum()이 호출되고 종료됩니다. 즉, main인 메인루틴에 의해서 호출되는 calculate_sum이 서브루틴입니다. 

def main():
    print("메인루틴 시작")
    result = calculate_sum(3, 4)
    print("결과:", result)
    print("메인루틴 종료")

def calculate_sum(a, b): # 서브루틴
    return a + b

if __name__ == "__main__":
    main()

시간에 흐름에 따라, 두 루틴을 시각화하면 아래와 같습니다. 메인루틴이 서브루틴을 호출하고, 그 동안 메인루틴은 서브루틴이 일을 다 처리할 때까지, 잠시 작업대기를 합니다. 그리고, 서브루틴의 일이 끝나면, 메인루틴으로 다시 작업이 넘어가서 메인루틴의 테스크가 종료됩니다.

Figure 1. 시간의 흐름에 따른 메인루틴-서브루틴

 

코루틴(Coroutine): "일시중지 후 재시작"이 가능한 함수


코루틴은 일시 중단되고 다시 시작될 수 있는 함수로, 일반적인 함수와 다르게 실행 중에 일시 중단되어 값을 주고 받을 수 있습니다. 이를 통해 비동기적인 작업이나 상태를 유지하는 작업을 효율적으로 처리할 수 있습니다. 특히, 코루틴은 서브루틴과 다르게 메인루틴으로 작업상태가 넘어가도, 코루틴내 내용을 잃지않고 유지할 수 있습니다. 그리고, 이런 메인루틴->코루틴, 코루틴->메인루틴의 작업상태를 넘겨 실행하는 단계를 "진입점"이라고합니다. 이런 진입점은 서브루틴은 "메인루틴->서브루틴"의 1개의 진입점을 가지지만, 코루틴은 여러 개의 진입점을 가집니다.

 

코루틴을 사용하려면, 코루틴의 성격을 정리해보겠습니다. 

  • 코루틴은 generator의 특별한 형태입니다. (generator도 데이터를 yield와 함께 하나씩 얻어올 수 있고, 그 다음 차례가 그대로 유지되기에 코루틴으로 취급합니다.)
  • 코루틴은 파이썬의 2.5부터 generator로 설명되지만, 실제로 async, await구문과 함께쓰려면 3.5의 이상부터 필요합니다.

그리고, 코루틴의 시작/값보내기/값얻기의 구현방법을 알아야합니다.

  • 코루틴의 시작: 코루틴은 보통 while True 구문을 이용해서 계속해서 일반적으로 코루틴의 시작을 알립니다.
  • 코루틴에 값보내기: 코루틴에 값을 보내는 방법은 코루틴객체.send(객체)로 외부의 값을 코루틴으로 전달합니다.
  • 코루틴 내에서 값 얻기: 이 값을 가져오기 위해서, `yield` 키워드를 이용하여 값을 얻어옵니다.

 

아래의 예시에서는 `searcher()`가 코루틴 함수입니다. 하나 씩 내용을 확인해보겠습니다.

  1. while구문으로 코루틴을 계속 실행하는 상태로 둡니다.
  2. while 구문이 이하가 도는 경우, yield구문에서 값이 올 때까지 기다립니다.
  3. next()가 호출되어야하는 이유는 제너레티너나 코루틴을 실행하고 첫번째 값을 얻기위해 호출합니다. 코루틴은 yield 표현식이 보이면 일시중단됩니다.
  4. 코루틴객체.send(객체)의 구문으로 코루틴에 값을 보내면, 그 보내는것과 동시에 코루틴이 실행됩니다. 다시 while True 구문에 의해서 yield 키워드가 등장하고, 코루틴이 일시중지됩니다. 이와 동시에 다시 메인루틴으로 작업이 옮겨갑니다.

 

def searcher():
    """
    검색어를 받아들이고 출력하는 코루틴
    """
    while True:
        query = yield  # 일시 중단하고 외부로부터 값을 받음
        print("검색어:", query)

# 코루틴 생성
search = searcher()
next(search)  # 코루틴 시작

# 코루틴에 값 전달
search.send("파이썬")
search.send("데이터 분석")

Figure2. 메인루틴-코루틴의 시간에 엔트리포인트 예

또, 다른 예시입니다. 코루틴은 일시정지를 함으로, 코루틴 내의 그 내용을 잃어버리지 않고 유지할 수 있다고 했습니다. 코루틴에서 average, count, total라는 변수를 계속 코루틴에서 들고 있고, send로 더할 때 마다, 코루틴의 변수가 변경되는 것을 확인할 수 있습니다.

def running_average():
    """
    입력된 숫자들의 평균을 계산하는 코루틴
    """
    total = 0
    count = 0
    average = None

    while True:
        value = yield average  # 새로운 값 받기
        total += value
        count += 1
        average = total / count

# 코루틴 생성
calc_average = running_average()
next(calc_average)  # 코루틴 시작

# 코루틴에 숫자 전달하고 평균 계산
print("평균:", calc_average.send(10))  # 출력: 10.0
print("평균:", calc_average.send(20))  # 출력: 15.0
print("평균:", calc_average.send(30))  # 출력: 20.0

 

 

코루틴을 이용한 async, await의 활용


asyncio을 사용해서 비동기적으로 작업을 수행할 수 있습니다. asyncio을 사용하기위해 대표적인 키워드 2개를 살펴보겠습니다.

      • async 키워드: async def로 함수를 정의하면 해당 함수는 코루틴 함수로 선언됩니다. 이는 함수가 비동기적으로 실행될 수 있음을 나타냅니다. 아래와 같이, async def 로 정의된 함수는 인자를 담아 호출하면, coroutine object로 정의됨을 알 수 있습니다.

    • await 키워드: await 키워드는 비동기 작업이 완료될 때까지 대기하고, 해당 작업의 결과를 반환합니다. await 있는 키워드가 실행되면, 이 라인의 구문이 실행되는 동안, "비동기적으로 메인루틴으로 잡이 넘어가고", "완료되면 메인루틴의 작업에서 다시 코루틴으로 작업이 넘어옵"니다.  Q. "async함수를 정의하면 꼭 await구문을 써야하냐?"할수도있는데, 꼭 사용하지 않아도 됩니다만, 비동기작업을 안쓸꺼면 굳이 async로 정의할 필요는 없을 것 같습니다
async def hello():
    print("안녕하세요, 코루틴!")

async def main():
    print("메인 루틴 시작")
    await hello()
    print("메인 루틴 종료")

asyncio.run(main())

 

아래의 예시를 보겠습니다.

  1. `async def`로 fetch_url이라는 함수를 비동기적인 함수로 정의했습니다. 
  2. `await` 키워드를 fetch_url내에 정의합니다. ` fetch_url `이 호출되고 await구문이 있는 라인이 실행되면 sleep이 실행되면서, 작업을 메인루틴으로 넘깁니다.
  3. 메인루틴이 실행중이다가, await구문이 완료되면 그 다음인 return으로 반환됩니다.
  4. `async main`에서는 await asyncio.gather로 비동기작업을 여러개로 구성할 때, 모든 작업이 완료될 때까지 대기해주는 함수입니다. 
import asyncio

async def fetch_url(url):
    print(f"URL 가져오는 중: {url}")
    await asyncio.sleep(1)  # 비동기적으로 1초 동안 대기
    print(f"URL 가져오기 완료: {url}")
    return f"Content of {url}"

async def main():
    tasks = [fetch_url("https://www.example.com"), fetch_url("https://www.google.com")]
    completed_tasks = await asyncio.gather(*tasks)
    print("모든 작업 완료:")
    for result in completed_tasks:
        print(result)

asyncio.run(main())

 

언제쓰면 유용한가? "네트워크 요청", "file I/O bound task", "데이터베이스 쿼리"


  1. 동시성 증가: 비동기 프로그래밍을 사용하면 여러 I/O 작업을 동시에 처리할 수 있습니다. 한 작업이 완료될 때까지 기다리지 않고 다음 작업을 시작할 수 있습니다.
  2. 응답 시간 개선: I/O 바운드 작업을 동시에 처리함으로써 전체 시스템의 응답 시간을 개선할 수 있습니다. 작업이 완료될 때까지 기다리는 동안 다른 작업을 수행하여 전체 처리 시간을 최소화할 수 있습니다.
  3. 자원 효율성: 비동기 프로그래밍을 사용하면 블로킹되는 대기 시간 동안 다른 작업을 수행할 수 있으므로 시스템 자원을 더 효율적으로 활용할 수 있습니다.
  4. 스케일링 용이성: 비동기 프로그래밍을 사용하면 더 많은 동시 요청을 처리할 수 있으며, 이로써 시스템의 스케일링이 용이해집니다.
반응형

요약


FastAPI middleware은 어플리케이션의 요청 및 응답처리과정중에 중간에 위치하여, 여러 작업을 수행할 수 있는 요청->동작 사이, 응답->동작사이에 여러 기능을 수행
주로 요청 및 응답을 변형하거나(예, 암호화), 인증, 로깅및 모니터링, 예외처리등을 수행.
ASGI (Asynchronous Server Gateway Interface) 프로토콜을 따르는 웹 애플리케이션에서 비동기 요청을 처리하기 위해, request._receive속성에 설정하도록함


fastAPI의 미들웨어는 @app.middleware("http")와같이 함수위의 데코레이터로 사용가능합니다.
async def set_body(request: Request, body: bytes) 함수는 원래 요청 객체에 바이트로 된 본문을 설정하기 위해 사용됩니다. 
아래의 함수 내에서 async def receive() -> Message 함수를 정의하고, 이 함수는 request 객체의 _receive 속성으로 설정됩니다. 
따라서 ASGI 프레임워크는 요청을 수신할 때 이 _receive 함수를 호출하여 요청 객체의 본문을 설정하여 로깅할 수 있습니다.

async def set_body(request: Request, body: bytes):
    async def receive() -> Message:
        return {"type": "http.request", "body": body}

    request._receive = receive


@app.middleware("http")
async def log_request_payload(request: Request, call_next):
    payload = await request.body()
    await set_body(request, payload)
    request.app.state.logger.debug(f"Request payload: {payload.decode()}")
    response = await call_next(request)
    return response

 



아래의 call_next의 인자가 callable로 (reqeust)을다시 인자로 받아야하는데, FastAPI.reqeust은 아래와 같이 구성되어있습니다.
request._receive 속성이 property로 설정되어있고 타입이 empty_receive라 Courutine의 타입을 받게되어있습니다.
그래서 async def로 정의한 함수를 넣어주도록하고, 그 메시지의 형태를 stream내애 얻어지는 형태랑 유사하게 받도록합니다.

class Request(HTTPConnection):
    _form: typing.Optional[FormData]

    def __init__(
        self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send
    ):
        super().__init__(scope)
        assert scope["type"] == "http"
        self._receive = receive
        self._send = send
        self._stream_consumed = False
        self._is_disconnected = False
        self._form = None

    @property
    def method(self) -> str:
        return self.scope["method"]

    @property
    def receive(self) -> Receive:
        return self._receive

    async def stream(self) -> typing.AsyncGenerator[bytes, None]:
        if hasattr(self, "_body"):
            yield self._body
            yield b""
            return
        if self._stream_consumed:
            raise RuntimeError("Stream consumed")
        self._stream_consumed = True
        while True:
            message = await self._receive()
            if message["type"] == "http.request":
                body = message.get("body", b"")
                if body:
                    yield body
                if not message.get("more_body", False):
                    break
            elif message["type"] == "http.disconnect":
                self._is_disconnected = True
                raise ClientDisconnect()
        yield b""
반응형

conda 설치후 아래와 같은 에러가 발생


$ conda activate myenv
usage: conda [-h] [--no-plugins] [-V] COMMAND ...
conda: error: argument COMMAND: invalid choice: 'activate' (choose from 'clean', 'compare', 'config', 'create', 'info', 'init', 'install', 'list', 'notices', 'package', 'remove', 'uninstall', 'rename', 'run', 'search', 'update', 'upgrade', 'build', 'content-trust', 'convert', 'debug', 'develop', 'doctor', 'index', 'inspect', 'metapackage', 'render', 'skeleton', 'verify', 'pack', 'token', 'server', 'env', 'repo')

 

 

해결방법: conda 초기화가 안되어 발생하는 문제


1. Conda 업데이트: 먼저 Conda를 최신 버전으로 업데이트하십시오. 아래 명령어를 실행하여 Conda를 업데이트합니다.

$ conda update -n base -c defaults conda

 

2. 시스템 환경 변수 확인: Conda가 설치된 경로를 시스템 환경 변수에 제대로 추가했는지 확인하십시오. 다음 명령어를 사용하여 Conda의 경로를 확인하고 확인하세요. "/home/사용자/anaconda3/bin" 이러한 경로가 존재해야합니다.

$ echo $PATH
/home/<사용자>/anaconda3/bin:/home/heon/anaconda3/condabin:/home/heon/.vscode-server/bin/e7e037083ff4455cf320e344325dacb480062c3c/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0:/mnt/c/Windows/System32/OpenSSH:/mnt/c/Users/4pygm/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/4pygm/AppData/Local/Programs/Microsoft VS Code/bin:/snap/bin

 

3. shell 재시작

$ source ~/.bashrc  # 또는 ~/.zshrc

 

4. Conda 초기화

$ conda init

 

반응형
%load_ext autoreload
%autoreload 2
반응형

요약


np.array()와 np.asarray()는 NumPy에서 배열을 생성하는 함수입니다.

그러나 두 함수 사이에는 몇 가지 차이점이 있습니다. 데이터가 복사가 되는지 여부와, 데이터 유형이 변화되는지 여부의 차이가 있습니다.

 

 

차이점 비교


  • 복사 여부
    - np.array(): 기본적으로 배열의 복사본을 생성합니다.
    - np.asarray(): 배열의 복사본을 생성하지 않고, 가능한 경우에는 입력 배열의 뷰(view)를 반환합니다. 즉, 입력 배열과 반환된 배열이 메모리를 공유할 수 있습니다.

 

  • 데이터 유형 변환:

    - np.array(): 입력된 데이터의 유형에 따라 새로운 배열의 데이터 유형이 결정됩니다. 필요에 따라 데이터 유형을 명시적으로 지정할 수도 있습니다.
    - np.asarray(): 입력된 배열과 동일한 데이터 유형을 갖는 배열을 생성합니다. 따라서 입력 배열의 데이터 유형을 유지합니다.

 

코드 비교


간단한 예제로 두 함수의 동작을 살펴보겠습니다. 아래의 예제에서 np.array()를 사용하여 배열 arr1을 생성하고, 이를 np.asarray()로 배열 arr2를 생성합니다. 그런 다음 arr1의 첫 번째 요소를 변경하면 arr2도 동일하게 변경됩니다. 즉, np.asarray()는 입력 배열과 같은 데이터를 참조하므로 변경 사항이 반영됩니다. 즉, arr1의 0번 인덱스만 변경했음에도, np.asarray은 array을 데이터를 그대로 참조하며, 뷰를 생성하므로 arr1을 변경함에따라 arr2도 변경됩니다.

요약하자면, np.array()는 항상 복사본을 생성하며, 데이터 유형 변환을 수행할 수 있습니다. np.asarray()는 가능한 경우에는 복사본 대신 입력 배열의 뷰를 반환하며, 데이터 유형 변환은 수행하지 않고 입력 배열의 데이터 유형을 유지합니다.

import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.asarray(arr1)

arr1[0] = 99

print(arr1)  # 출력: [99  2  3]
print(arr2)  # 출력: [99  2  3]

 

반응형

input = "abccbaz"


# 필랜드롬인지 판정하는 함수
def is_palindrome(sub_str:str) -> str:
    return sub_str == sub_str[::-1]

# 가장 긴 필랜드롬
def search_longest_palindrome(string:str) -> str:

    # 문자열의 길이가 2이하인데, 0번, 1번 문자열이 같은 경우
    if len(string) <= 2:
        if is_palindrome(string):
            return string

    longest_palindrome = str()

    for start_idx in range(1, len(string)):
        for window in range(1, start_idx):
            substring = string[start_idx-window:start_idx+window]
            if is_palindrome(substring):
                if len(longest_palindrome) < len(substring):
                    longest_palindrome = substring


    return longest_palindrome

search_longest_palindrome(input)

 

 

애너그램(Anagram)이란?

 


애너그램은 문자를 재배열하여, 다른 뜻을 가지는 단어로 바꾸는 것을 말한다. 예를 들어, 리그 오브 레전드의 챔피언과 "마오카이(Maokai)"은 "I am oak(떡갈나무)"와 같이 단어의 수와 각각의 문자열이 동일한데, 순서만 재배치함으로써, 다른 뜻을 가지는 단어들을 의미한다. 파이썬에서 단어들을 이런 애너그램 단위로 묶으려면 어떻게 해야할까? 다음과 같은 문제가 주어졌다고 가정하자.

 

아래와 같이 문자열이 주어졌을 때, 애너그램단위로 문자열 6개를 각각 유니크한 애너그램 단위로 묶고자한다.

inputs = ["education", "auctioned", "cautioned", "eat", "ate", "bet"]

 

솔루션

defaultdict을 이용하면, 바로 Key의 생성없이 Key, value의 할당이 가능하다. 각각의 인자를 list에 바로 저장할 때, 문자열이 애나그램인지 파악하기 위해서, 애너그램여부를 sorted()을 이용해서 정렬해서 파악한다. sorted()은 리스트, 문자열을 인자로 받을 수 있어서, 이를 이용해서 "".join(sorted(word))의 구문을 실행시키면, 정렬된 문자열이 반환된다.

# python3.x

inputs = ["education", "auctioned", "cautioned", "eat", "ate", "bet"]

# Solution
from collections import defaultdict

def sorted_words(inputs):
    anagram_groups = defaultdict(list)
    for idx, word in enumerate(inputs):
        anagram_groups["".join(sorted(word))].append(idx)

    return anagram_groups

 

 

팰린드롬(Palindrome, 회문)


팰린드롬(Palindrome)은 흔히 "요기요"는 거꾸로 해도 "요기요", "소주 만 병 만 주소"와 같이, 거꾸로 해도 똑같은 의미의 문장이 되는 문자열을 의미한다. 팰린드롬은 주어진 문자열이 있을 때, 문자열내에 팰린드롬이 있을 수도 있고, 없을 수도 있다. 예를 들어, "소주 주세요"와 같은 문자열은 팰린드롬이 없지만, "이 글의 원어는 국립국어원의 출처입니다"과 같이 팰린드롬이 모든 문자열이 아닌, 전체 문자열의 부분으로 가지고 있는 경우도 있다. 이렇듯, 문자열이 주어졌을 때, 가장 긴 팰린드롬을 찾아보자.

 

input = "abccbaz"

 

솔루션: 1) 우선 필랜드롬인지 검사하는 함수가 하나 독립적으로 있으면, 디자인이 쉬워진다. 그 후, 포인터 2개(첫 번째는 시작포인터, 두 번째는 윈도우)를 이용해서 검사를 진행한다.

 

아래와 같이 문자열이 주어졌을 때, 필랜드롬은 적어도 문자열의 길이가 2인 경우만 취급한다. 따라서, 필랜드롬의 문자가 2개이하면서, 0번, 1번의 문자가 동일한 경우는 조기 return을 시켜줄 수 있다.

# 가장 긴 필랜드롬
def search_longest_palindrome(string:str) -> str:

    # 문자열의 길이가 2이하인데, 0번, 1번 문자열이 같은 경우
    if len(string) <= 2:
        if is_palindrome(string):
            return string

 

그 외의 대부분의 경우를 위한 함수를 작성할 때는 아래와 같이 로직을 짤 수 있다. 1) 필랜드롬을 검사하기위한 시작인덱스(start_idx)는 0이 아니라 1부터 시작한다. 2) 윈도우사이즈는 start_idx을 넘으면 indexError가 날 것이기에, start_idx을 넘으면안된다. 또한 start_idx은 1부터 시작한다(0이면 시작점의 알파벳 하나를 비교하는 꼴이기 때문). 그리고, 가장 긴 필랜드롬을 찾는 것이기 때문에, 중간에 저장해놓을 변수를 하나 지정해서 다음과 같이 작성해볼 수 있다.

 

 

 

반응형

Summary: set, dict은 해시함수를 이용한 해시값을 저장하고있는 해시테이블이란 자료구조를 사용하기 때문에, 빠른 탐색이 가능함. 해시는 임의의 변수(문자열 등)를 숫자로 변환하여, 매핑하는 것을 의미함. 따라서, 임의의 변수가 오더라도 숫자로 매핑할 수 있고, 이 숫자로 매핑한 경우 인덱스처럼 사용할 수 있어 빠른 검색이 가능함.

 

 

해시함수를 이용한 해시값으로 매핑 예시. (ref: https://modeling-languages.com/robust-hashing-models/)

 

 

도입: 파이썬으로 자료를 저장하다보면 많은 양의 데이터를 저장해야하는 경우가 있다. 가령, 유전정보와 같이 1번 유전자의 100번째의 위치의 DNA가 바뀌었는지 여부 뿐만아니라... 101번, 10,000,000번째의 위치가 변경되었는지, 변경되었으면 무엇으로 변경되었는지 저장한다. 문제는 이렇게 많은 정보를 저장한 것을 읽어, 특정위치(Input으로 주어지는 위치)에 변이가 있었는지 찾아야한다면? 많은 연산을 해야할 것이다. 

 

 

 


해시란(hash)?

해시는 임의의 길이를 갖는 임이의 데이터를 고정된 길이의 데이터로 매핑하는 함수를 말한다. 쉽게 말해서, 입력값을 아무거나 넣더라도, 각기 다른 출력값을 반환하는 것이다(단, 출력값의 길이는 동일 // 입력값을 동일하게 할경우 출력값은 일관성 있게 동일). 예를 들어, 가장 쉬운 해시는 mod 연산이다. mod k 연산은 어떤 숫자를 k로 나눴을 때 나머지를 구하는 연산인데 이를 통해 해시를 구현할 수 있다. 아래의 예시와 같이 임의의 숫자 13, 3, 14, 23, 22를 7로 나눠 각각 나머지를 구했다. 이런식으로 각각 원소인 13을 6으로 매핑, 3을 3으로 매핑 14을 0으로 매핑하는 것처럼 원래 데이터를 매핑하는 것을 의미한다.

 

 

mod 7 (13) A -> G
mod 7 (3) G -> A
mod 7 (14) 0 T -> C
mod 7 (23) 2 C - > T
mod 7 (22) 1 C -> T

(Table 1. mod 7 연산의 결과)

 

 

 

그런 다음에, 6을 인덱스로 해서 "A->G"라는 데이터를 저장하고, 3에 "G->A"라는 것을 저장하듯 모든 배열을 위와같이 저장한다. 그러면, 14의 내용이 무엇인지 찾고자할때, 13, 3, 14, 23, 22의 중에 14를 찾아서 따라가는 것이 아니라, 1) 14을 mod 7 (14)을 해서, 0을 얻은다음, 2) 0의 인덱스에 무엇이 저장되어있는지 확인하면 된다. 이런 방법으로하면, 상당히 긴 배열에서도 13=14냐? 3=14이냐? 23이 23이냐?? 모두 1:1로 비교하는 방법(선형탐색)을 하지 않고, 바로 검색을 할 수 있다. 이 이유가 hash를 쓰는 이유이다. 

A -> G
G -> A
0 T -> C
2 C - > T
1 C -> T

 

 

그리고, 우리는 위의 6, 3, 0, 2, 1과 같은 값들을 해시값(hash value) 또는 다이제스트(Digest)라고 부르고, 해시값과 저장되어있는 값("A->G", "G-A"...)와 같이 위의 정보를 테이블형태로 저장하고 있는 자료구조를 해시테이블(hash table)이라고 한다.

 

 

 


Hash collision: 혹시나 해시값이 중복이면 어떻게되나?

 

해시충돌의 경우는 아래와 같다. 아래처럼 14의 값과 21을 해시함수 mod 7을 했을 경우 결과값(해시값)이 동일하다. 이 경우는 해시값이 중복인 경우 (예, 0인 경우), 14에서 부터 해시값이 나온건지 21에서부터 해시값이 0이 나온것인지 구분이 필요하다.

 

 

mod 7 (13) A -> G
mod 7 (3) G -> A
mod 7 (14) 0 T -> C
mod 7 (23) 2 C - > T
mod 7 (22) 1 C -> T
mod 7 (21) 0 T -> A

 

 

위의 예시만 봐도, 위의 mod 함수를 해시함수로 쓰는 경우 해시값(output)의 충돌은 발생한다. 가장 좋은 건 해시함수를 좋은 해시함수(결과물의 충돌이 거의 없는 함수)를 쓰는 것이 좋다. 그러나, 좋은 해시함수를 쓰더라도, 해시충돌이 발생할 확률은 언제나 존재한다. 이 원인에 대한 직관적인 이해는 비둘기집의 원리와 같다. 

 

비둘기집의 원리: "차량등록번호", "휴대전화번호"처럼 차량수/휴대전화가가 등록할수 있는 번호의 개수를 초과하는 경우와 같은 원리. 비둘기집이 9개가 있고, 각 비둘기집에는 한 마리씩 비둘기가 있을 수 있다고 가정해보자. 만일 비둘기가 10마리가 있다면 어떻게될까? 9개의 집 중, 하나는 두마리가 들어가야한다. 마찬가지로 "고정길이 문자열을 반환하는 해시값은 유한하다. 따라서, 임의의 가변길이 문자열의 개수 > 반환할 수 있는 고정길이 문자열이기에, 겹칠 수 있다"는 말이다. 예를 들어, 영어 알파뱃 26개인데 정보를 10개의 고정길이 문자열로 반환하는 해시함수를 만들었다고 하자. 해시함수가 반환할 수 있는 값은 26의 10제곱이다. 그러나 입력값을 0부터 ZZZZZZZZZZZZZZZZZZZZZ까지 엄청나게 많은 은 알파벳과 숫자의 조합이라면? 매핑할 수 있는 값의 종류가 모자르다.

 

Figure. Pigeons in holes.

자세한 내용은 아래를 참고하자 (https://en.wikipedia.org/wiki/Pigeonhole_principle)

따라서, 해시값이 충돌할 경우, 여러 방안이 존재한다. 체이닝, Open addressing (선형탐사, 제곱탐사, 이중해시).. 등 여러 방법이 존재한다. 가장 쉬운 예시는, 겹치는 해시값안에는 여러 변수를 일단 배열형태로 저장한 후에, 선형탐사하는 것이다. 일단 겹치는 경우, 저장하고 훨씬 적은 수를 선형탐사해서 찾겠다는 것과 같다.

 

 


Set, Dict 에서의 hash테이블: 파이썬의 set, dict자료구조는 해시테이블을 이용한다. 즉, dict의 key와 set의 원소를 x라한다면, hash(x)을 실행시켜 같으면 x원소가 반드시 존재한다는 것으로 활용할 수 있다. 아래는 set의 해시테이블을 직접 접근한 예시이다. 아래와 같이 파이썬의 내장함수는 해시함수를 제공하고 있다.

 

>>> hash
<function hash(obj, /)>

 

그리고, 파이썬의 객체는 hash함수값을 매직매서드로 가지고있다. 아래의 "a"라는 문자의 해시값은 "8870042888964940309"이다.

 

>>> "a".__hash__() 
8870042888964940309

 

그리고 set을 하나 만들어, 해시값을 소환해보자. set 내부에 해시테이블을 직접 접근하는 방법은 찾지 못했으나, 이와 유사하게 아래와같이 각 원소의 hash값을 알고있으면 이를 통해서 검색하면 매우 빠르다.

 

>>> myset = {"a", "b"}
>>> {element.__hash__() for element in myset}

{8703158610484096297, 8870042888964940309}

 

반응형

문자나 숫자를 컴퓨터끼리 송수신하기위해서는 문자나 숫자를 그대로 전송하는 것이 아니라, 이러한 숫자나 문자를 비트단위로 끊어서 전송하게된다. 예를들어 숫자 3을 보내기 위해서는 0011으로 전송하게된다. 우리가 알고 있는 디지털신호(0아니면 1)로 변환해서 보내기 용히하기 때문이다. 이때, 숫자를 16진수로 보내면 최대 한 전송단위 16까지를 보낼수있다. 이렇게 한 전송단위(16비트)을 다시 2진수로 변환하여 송수신할 수 있다. 즉, 파이썬 내부에서 다뤄지는 문자열, 숫자는 파이썬에서 생성된 객체이고, 이를 바로 트랜스포트에 그대로 싣는 것은 안된다는 것이다.

따라서, 우리는 객체를 byte으로 적절히 인코딩을 해줘서 보내고, 컴퓨터는 이를 디지털신호로 바꿔 전송한후, 컴퓨터가 16진수는 숫자를 4bit단위로 끊어서, 인식하게 된다. 아래와 같이, 16진법 앞에는 "0x"을 붙여주어, 사용한다. 앞에 접두사로 "0x"을 붙이는 이유는 컴퓨터 입장에서는 0001을 이라는 디지털 신호가 왔을때, 이게 2진법인지 8진법으로 해석해야하는지 16진법으로 해석해야하는지 모르기 때문이다. "0x"은 16진수로 신호를 해석하라는 의미가 된다.

 

그 다음에 아래의 16진법 표기법과 같이, 해석을한다. 16진법에서는 컴퓨터가 받아들이는 입장에서는 "10"에서의 "1"과 "01"에서의 "1" 동일하기 때문에, "10"을 A로 표기한다. 그 이후에 11은 B, 12은 C... 15은 F까지 표기를한다. 

10진법 Hex(16진법) Binary
0 0x00 0000
1 0x01 0001
2 0x02 0010
3 0x03 0011
4 0x04 0100
5 0x05 0101
6 0x06 0110
7 0x07 0111
8 0x08 1000
9 0x09 1001
10 0x0A 1010
11 0x0B 1011
12 0x0C 1100
13 0x0D 1101
14 0x0E 1110
15 0x0F 1111

 

Python 내부에서 이러한 구조체를 사용할 수 있게 내장함수가 구현되어있는데, stuct (structure)을 사용하면 구현할 수 있다. 예를 들어, 아래와 같이 5를 한 byte 사이즈로 표현하면 0x05가 될 것이고, 4개의 byte로 표현하면 0x가 4개로 이루어진 hexadecimal이 표현된다.

# hexadecimal.py
import struct

a = struct.pack('b', 5)
print(a)

b = struct.pack('i', 5)
print(b)

b'\x05'
b'\x05\x00\x00\x00'

 

아래와 같이 2개 이상의 문자또는 숫자를 보내야하는 경우에는 포맷에 그 크기에 맞춰 포맷팅을 보내면 된다. 그렇지 않으면 아래의 예시의 a와 같이 패킹을 할 수 없다. 5,5을 16진법으로 표현할 방법이 없기 때문이다. 

import struct

b = struct.pack('ii', 5, 5)
print(b)
# b'\x05\x00\x00\x00\x05\x00\x00\x00


a = struct.pack('b', 5, 5)
print(a)
# Traceback (most recent call last):
#   File "hexadecimal.py", line 3, in <module>
#     a = struct.pack('b', 5, 5)
# struct.error: pack expected 1 items for packing (got 2)
반응형

 

 

fixture 란?


단위 테스트를 위해서, fixture을 이용하면 사전에 미리 정의된 신뢰할만하고 일관성있는 테스트를 이용할 수  있다. fixture은 환경이나 데이터셋에 대해서도 테스트해볼 수 있다. 이 fixture은 "step"과 "data"을 정의하고 이 테스트를 

 

fixture을 이용한 서비스, 상태, 조작환경등이 인자(argument)을 통해 모두 접근가능하고, 각 fixture는 보통 파라미터를 지정해준다. 그리고 pytest을 이용할 때, 어떤 함수를 테스트할것인지 @pytest.fixture 와 같이 "@"데코레이터를 이용해서 설정할 수 있다.

 

@pytest.mark.parametrize(ids=?) 내 ids란?? 

parameterize로 전달해주는 파라미터에 대해서 string으로 각 아이디를 줄 수 있고, 이는 "-k"을 옵션을 이용해서 특별한 파라미터(케이스)에 대해서만 실행해볼 수도 있다.

 

@pytest.fixture(scope=??):


scope범위에 따라 데코레이터를 달아준 함수의 실행범위가 달라진다. 예를 들어, scope="function"인 경우는 데코레이터를 달아준 함수를 매번 call해서 사용하고, 소멸된다. 한편, scope="module"로 지정하면 ".py"파일 내에서는 무조건 1번만 생성된다. 한편 scope="session"인 경우는 최초 테스트 실행시 단 한번만 객체가 실행되며, 각 테스트에서 이 하나의 객체가 호출된다. 

Reference: https://velog.io/@gyuseok-dev/pytest-pytest.fixture

https://velog.io/@dahunyoo/Test-fixture-on-Pytest

 

yeild 사용


yield은 보통 제너레이터에서 lazy하게 데이터를 불러올떄 사용한다. pytest에서는 테스트 실해 후에 환경정리, 테스트 데이터들을 정리할 때 yield 을 이용할 수 있다. yield는 호출한 테스트케이스가 실행된 후 종료되어도 마지막까지 실행되게끔 해주는 키워드이다.

반응형

+ Recent posts