요약


register_buffer는 모델의 상태(state)로서 관리하고 싶은 텐서를 등록하는 데 사용됩니다. 즉, 이 메서드는 state_dict에 포함되어서, torch.nn.Module.state_dict()에 함께 저장되어, torch.save을 할 때, 함께 저장됩니다. 또한, register_buffer으로 등록된 텐서는 기본적으로 기울기를 계산하지 않습니다.

 

기능 1. state_dict을 통해 모델을 저장/로드 할 때, 함께 포함되도록

torch.nn.Module로 딥러닝 네트워크를 구성하고, 필요한 텐서(non-trainable)도 함께 저장이 가능합니다.

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

  • 9번줄:  self.register_buffer("running_mean", torch.zeros(10))으로 텐서를 하나 저장합니다. 이렇게되면 self. running_mean에 속성으로도 저장됩니다.
  • 21번줄: torch.save(model.state_dict(), "model_with_buffer.pth")에서 model.state_dict()을 이용해서 state_dict을 저장합니다. 이 때, register_buffer으로 등록한 running_mean= torch.zeros(10)도 함께 state_dict에 저장됩니다.
  • 24번줄: register_buffer을 사용하지않고 저장하려면, state_dict의 딕셔너리에 key-value을 별도로 이렇게 저장해줘야합니다.

 

기능 2. non-trainable parameter을 저장하는 경우

배치 정규화에서 배치 단위의 평균(mean)과 분산(var)은 통계량값만 저장하고, gradient로는 사용되지 않습니다. 이 때도 사용이 가능합니다.

 

반응형

기본개념: 코루틴, 메인루틴, 서브루틴

코루틴을 이해하기위해서는 메인루틴과 서브루틴을 이해하고 있으면, 더 쉽게 이해가 됩니다[이전포스팅]. 이 3가지 루틴을 정리하면 아래와 같습니다.

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

 

코루틴과 제너레이터

코루틴과 제너레이터 중, 코루틴이 더 일반적인 개념이며 코루틴 중에 반복가능한 데이터를생성하는 한 종류가 제너레이터라고 할 수 있습니다. 이 제너레이터와 코루틴을 굳이 비교하자면 아래와 같이 비교해볼 수 있습니다.

  코루틴 제너레이터
공통점 1. 함수 형태로 작성
2. yield 키워드로 생성하거나 반환
3. 상태유지: 이전 상태를 유지하며, 중단시점부터 재개가 가능
1. 함수 형태로 작성
2. yield 키워드로 생성하거나 반환
3. 상태유지: 이전 상태를 유지하며, 중단시점부터 재개가 가능
  종료 yield 완료시: StopIteration예외 yield 완료시: StopIteration예외
 차이점    
   목적 주로 비통기 작버을 처리하기 위해 사용 반복 가능한 객체를 사용
예) yield 1; yield 2..
   작성방법 yield 외에 await을 이용해서 중간에 값을 보내거 받을 수 있음  
   호출방법 send()메서드로 값을 주고 받음.  next()함수로 값을 하나씩받음
   종료 StopIteration 예외 StopIteration 예외

 

코루틴의 간단한 예시입니다.

def simple_coroutine():
    print("Coroutine started")
    x = yield  # 처음 호출될 때까지 대기
    print(f"Received: {x}")
    y = yield x * 2  # yield 우측에 있는 값은 반환하는 값입니다.
    print(f"Received: {y}")

# 코루틴 생성
coro = simple_coroutine()

# 코루틴 시작
print(next(coro))  # Coroutine started, 첫 번째 yield까지 실행

# 데이터 전송
print(coro.send(10))  # Received: 10

# 데이터 전송
coro.send(20)  # Received: 20

 

위의 예시처럼 코루틴을 시작하려면 반드시 "next()"을 호출하여 실행시켜야 합니다. 그렇지 않으면 아래와 같이 에러가 발생합니다. 즉, coroutine 시작지점까지는 함수가 실행되어 대기중이어야합니다.

Cell In[24], line 1
----> 1 coro.send(20)

TypeError: can't send non-None value to a just-started generator

 

 

코루틴과 마찬가지로, generator도 yield의 반환이 없으면 StopIteration 예외가 발생합니다.

In [1]: def simple_generator():
   ...:     yield 1
   ...:     yield 2
   ...:     yield 3
   ...: 

In [2]: gen = simple_generator()

In [4]: next(gen)
Out[4]: 1

In [5]: next(gen)
Out[5]: 2

In [6]: next(gen)
Out[6]: 3

In [7]: next(gen)
StopIteration                             Traceback (most recent call last)
Cell In[7], line 1
----> 1 next(gen)

StopIteration:

 

 

코루틴의 4가지 상태: GEN_CREATE, GEN_RUNNING, GEN_SUSPENDED, GEN_CLOSE

  1. GEN_CREATED (생성 상태): 코루틴이 생성되었지만 아직 시작되지 않은 상태입니다.
  • 특징: __next__() 또는 send(None)이 호출되기 전의 상태입니다.
def coroutine_example():
    yield

coro = coroutine_example()
print(coro)  # 코루틴 객체가 생성된 상태, 아직 실행되지 않음

2. GEN_RUNNING (실행 중 상태): 코루틴이 현재 실행 중인 상태입니다.

  • 특징: 코루틴이 실행되고 있으며, 내부적으로 yield 문을 만날 때까지 실행됩니다. 실행 중에 다시 재개될 수 없습니다.
def coroutine_example():
    print("Running")
    yield

coro = coroutine_example()
next(coro)  # "Running" 출력, 실행 중 상태

 

3. GEN_SUSPENDED (일시 중지 상태): 코루틴이 yield 문에서 일시 중지된 상태입니다. 주의해야할 것은 RUNNING 상태는 yield을 만나기 전까지 실행되는 것인데, 여기서는 yield가 이미 실행된 상태를 의미합니다.

그리고, inpsect.getgeneratorstate 함수로 코루틴의 상태를 조회해보면 GEN_SUSPENDED을 확인할 수 있습니다.

>>> def coroutine_example():
>>>     yield "Suspended"
>>>  

>>> coro = coroutine_example()
>>> print(next(coro))

>>> import inspect
>>> inspect.getgeneratorstate(coro)
>>> 'GEN_SUSPENDED'

 

4. GEN_CLOSED(종료상태): 코루틴이 종료된 상태입니다. 종료되면 다시 시작할 수 없습니다.

  • 특징: 모든 코드가 실행되었거나 close() 메서드가 호출된 상태입니다.
def coroutine_example():
    yield "Running"
    return "Done"

coro = coroutine_example()
print(next(coro))  # "Running" 출력
try:
    next(coro)  # StopIteration 예외 발생
except StopIteration as e:
    print(e.value)  # "Done" 출력, 종료 상태

 

각 상태를 연속선상으로 확인해보면 아래처럼 확인해볼 수 있습니다.

import inspect

def coroutine_example():
    print("Coroutine started")
    x = yield "Suspended at first yield"  # GEN_SUSPENDED 상태로 진입
    print(f"Received: {x}")
    y = yield "Suspended at second yield"  # 다시 GEN_SUSPENDED 상태로 진입
    print(f"Received: {y}")
    return "Done"  # GEN_CLOSED 상태로 진입

# 코루틴 객체 생성 (GEN_CREATED 상태)
coro = coroutine_example()
print(inspect.getgeneratorstate(coro))  # GEN_CREATED

# 코루틴 시작 (GEN_RUNNING 상태)
print(next(coro))  # "Coroutine started" 출력, "Suspended at first yield" 반환
print(inspect.getgeneratorstate(coro))  # GEN_SUSPENDED

# 데이터 전송 및 재개 (GEN_RUNNING 상태로 변환 후 GEN_SUSPENDED 상태로 다시 변환)
print(coro.send(10))  # "Received: 10" 출력, "Suspended at second yield" 반환
print(inspect.getgeneratorstate(coro))  # GEN_SUSPENDED

# 데이터 전송 및 종료 (GEN_RUNNING 상태로 변환 후 GEN_CLOSED 상태로 변환)
try:
    coro.send(20)  # "Received: 20" 출력, StopIteration 예외 발생
except StopIteration as e:
    print(e.value)  # "Done" 출력
print(inspect.getgeneratorstate(coro))  # GEN_CLOSED

 

while True 구문을 이용한 코루틴

while True를 사용한 코루틴으로 지수 이동 평균 (Exponential Moving Average, EMA)을 계산하는 것들을 해볼 수 있습니다.

아래는 그 예제입니다.

def ema_coroutine(initial_ema, alpha):
    ema = initial_ema
    print(f"Starting EMA with initial value: {ema}")
    while True:
        new_value = yield ema
        ema = (alpha * new_value) + ((1 - alpha) * ema)
        print(f"Updated EMA to: {ema}")

# 코루틴 생성 및 초기화
alpha = 0.1
initial_ema = 50  # 초기 EMA 값
ema_gen = ema_coroutine(initial_ema, alpha)
current_ema = next(ema_gen)  # 코루틴 시작, 최초의 EMA 값을 받음

# 새로운 값으로 EMA 업데이트
print(ema_gen.send(55))  # 새 데이터 포인트 55를 전달하고 업데이트된 EMA 출력
print(ema_gen.send(60))  # 새 데이터 포인트 60을 전달하고 업데이트된 EMA 출력

# 코루틴 종료
ema_gen.close()

 

yield 에서 값을 반환하는 순서를 주의할 필요가 있습니다.

  1. send(50): 코루틴에 값을 보내면 new_value에 할당합니다. 이후 print()문까지 도달하여 출력합니다. 
  2. yield ema: 그리고나서 업데이트된 ema을 반환하여 50.5가 출력됩니다. 

 

코루틴을 초기화해주면 데코레이터: coroutine

반응형

요약


  • 딥러닝에서의 stride: 컨볼루션 연산에서 커널의 이동 범위를 나타냅니다.
  • numpy 배열에서의 stride: 배열의 각 차원에서 다음 요소로 이동하기 위한 메모리 상의 바이트 수를 나타냅니다.

 

보통 딥러닝하시는 분들이면 딥러닝의 CNN layer에서 stride을 들어보셨을 텐데, numpy 에서의 stride가 어떤 개념인지 정리해보겠습니다.


numpy 배열에서의 Stride: 배열의 각 차원에서 다음 요소로 이동하기 위한 메모리 상의 바이트 수

numpy 배열에서의 stride는 메모리에서 다음 요소로 이동하기 위해 건너뛰어야 하는 바이트 수를 나타냅니다. numpy 배열은 연속된 메모리 블록을 사용하여 데이터를 저장하며, stride는 배열의 각 차원을 따라 이동할 때의 바이트 수를 지정합니다. 배열이 슬라이싱되거나 뒤집힐 때 stride가 음수가 될 수 있습니다. 이는 배열이 역순으로 참조되고 있음을 의미합니다.

예를 들어, numpy.array을 3, 3 shape의 unit8 타입의 행렬을 만들었다고 가정해보겠습니다. 여기서 [0,0]의 위치에서 그 다음인 [0, 1]을 읽으려면 uint8이기 때문에 8bit(1byte)을 이동해서 읽어야합니다. 한편, 그 다음행인 [1, 0]을 읽어야하면, 한 행이 3개의 원소를 담고 있기에, 3*8bit(3byte)을 이동해서 읽어야합니다. Axis 1이 컬럼이며, Axis 0이 행이기에, Axis[0](행)을 이동하려면 3bytes (stride=3)으로 표시되어야하며, Axis[1](열)을 이동하려면 1byte (stride=1)을 이동해야합니다.

stride: https://stackoverflow.com/questions/53097952/how-to-understand-numpy-strides-for-layman

 

아래와 같이 3, 3 정수형의 uint8을 생성해보겠습니다. 이 배열의 `.strides`속성은 축이 2개이기에, 2개의 값이 튜플로 (3, 1)로 반환됩니다. 그리고, 위에서 언급했던 것처럼 stride가 (3, 1)입니다.

>>> import numpy as np

>>> arr = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
], dtype=np.uint8)

>>> print("배열:")
>>> print(arr)
>>> print("Stride:", arr.strides)

배열:
[[1 2 3]
 [4 5 6]
 [7 8 9]]
Stride: (3, 1)

 

Stride은 음수도 될 수 있습니다. 이 행렬을 뒤집는 경우, 다음 원소를 읽기위해서는 이전의 메모리를 참조해야합니다. 예를 들어, 행렬을 한번 뒤짚은 경우라면, `array[0]`이후에 `array[1]`을 읽으려면 +로 바이트를 이동시켜야하는게 아니라 (-)으로 바이트를 이동시켜야합니다.

>>> import numpy as np

>>> arr = np.array([1, 2, 3, 4, 5])
>>> reversed_arr = arr[::-1]

>>> print("원래 배열:", arr)
>>> print("뒤집힌 배열:", reversed_arr)
>>> print("원래 배열의 stride:", arr.strides)
>>> print("뒤집힌 배열의 stride:", reversed_arr.strides)
원래 배열: [1 2 3 4 5]
뒤집힌 배열: [5 4 3 2 1]
원래 배열의 stride: (8,)
뒤집힌 배열의 stride: (-8,)

 

딥러닝에서의 Stride: 컨볼루션 연산에서 커널의 이동 범위

딥러닝에서의 stride는 주로 컨볼루션 연산에서 사용됩니다. 컨볼루션 연산에서 stride는 커널이 입력 이미지 위를 얼마나 많이 움직이는지를 의미합니다. 예를 들어, stride가 1이면 커널이 한 픽셀씩 이동하며 연산을 수행하고, stride가 2이면 두 픽셀씩 건너뛰면서 연산을 수행합니다.

 

Filter(kernel)이 image에서 2칸씩 이동하며 연산하면 stride가 2입니다. src: https://medium.com/machine-learning-algorithms/what-is-stride-in-convolutional-neural-network-e3b4ae9baedb



연관 에러: ValueError: At least one stride in the given numpy array is negative, and tensors with negative strides are not currently supported. (You can probably work around this by making a copy of your array with array.copy().)

이미지를 전처리하는 과정에서 flip 등의 연산이 들어가게되면, 위의 예시처럼 stride가 음수가 되는 경우, 위의 에러가 발생합니다. "적어도 하나 이상의 stride가 음수가 됨을 의미합니다". 따라서 이 경우에는 flip한 array을 다시 메모리에 재할당하여(=copy)하여 처리하면 해결이됩니다.

# 변경전

transform = A.Compose(
    [
        A.RandomCrop(height=224, width=224, p=1),
        A.Resize(224, 224),
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.RandomRotate90(p=0.5), 
        A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
        ToTensorV2(),
    ]    
)


train_dataset = dataset(train_images, train_masks, transform)
train_dataset[0] # 에러 발생

# 변경

반응형

 

if else구문이 아닌, 구문에서의 else의 사용

1. for - else 구문:

for loop가 정상적으로 완료되면 else 구문이 실행됩니다. break으로 중단되는 경우 else 블록은 실행되지 않습니다.

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    if number == 3:
        print("Found 3!")
        break
else:
    print("3 is not in the list.")

 

2. try-except-else:

try-except에서 예외가 발생하지 않을 때, else 블록이 실행됩니다.

이거 왜 쓰냐는 질문이 종종있는데요. 아래의 예시를 들어보겠습니다. 아래의 try-except구문에서 FileNoteFoundError을 open()함수에서 발생할 수 있다는 것을 경험적으로 알 수 있습니다. 

try:
    file = open('example.txt', 'r')
    content = file.read()
    print(content)
    file.close()
except FileNotFoundError:
    print("File not found.")

이는 open()에서 자주 예외가 발생할 수 있는데, 만일 open이 아니라 my_fun1(), my_fun2()라면 어느구문에서 해당예외가 발생한다고 예측할 수 있을까요? 즉, 실제로 예외가 발생하지 않는 경우에도 try 블록에 많은 코드가 포함되어있어서 디버깅이 어렵습니다. try-except구문에서는 예외가 발생할 수 있는 함수의 호출을 최소한의 블록으로 담아야 더 명시적입니다. 

try:
    my_func1()
    my_func2()
except FileNotFoundError:
    print("file not found)

 

따라서, 이를 처리하기위해 아래와 같이 try-except-else 구문으로 처리해볼 수 있습니다. 

try:
    file = open('example.txt', 'r')  #open()에 대해서만 예외처리
except FileNotFoundError:
    print("File not found.")
else:
    content = file.read()
    print(content)
    file.close()

이는 EAFP(Easier to ask for forgiveness than permission, 허락보다 용서가 쉽다) 코딩스타일로 `if os.path.exist()`등으로 처리가 가능하나, 파이썬에서는 EAFP스타일을 사용을 많이합니다.

 

with 구문

with 구문은 크게 2가지로 구성됩니다.

  1. `__enter__`: with 구문의 실행 (진입)
  2. `__exit__`: with 구문의 종료. + with 구문에 끝에 finally 절의 역할이 수행됩니다.

 

with 구문은 컨텍스트 관리자 객체를 사용하여, 코드블록의 진입종료 시에 특별한 동작을 합니다. 컨텍스트 관리자 객체는 __enter__와 __exit__ 메서드를 구현하여, 코드 블록 진입 시에 __enter__ 메서드를, 종료 시에 __exit__ 메서드를 호출하게 합니다. 즉, 컨텍스트 관리자를 이용하면, 자원 해제, 예외 처리 등을 신뢰성 있게 관리할 수 있습니다.

아래의 예시를 하나 들어보겠습니다. 아래의 예시에서는 파일 객체를 반환하고,

  1. with구문으로 실행될때 (__enter__)가 실행되고,
  2. with 구문이 종료되면 (__exit__)이 종료되는 것 과 같습니다. 
with open('example.txt', 'r') as file:
    content = file.read()
    print(content)

실제로 `__enter__`, `__exit__`이 있는지 살펴보겠습니다.

In [1]: # open 함수로 반환된 파일 객체의 __enter__와 __exit__ 메서드 확인
   ...: with open('example.txt', 'r') as file:
   ...:     print(hasattr(file, '__enter__'))  # True 출력
   ...:     print(hasattr(file, '__exit__'))   # True 출력
   ...:     content = file.read()
   ...:     print(content)
   ...:
True
True

 

또 다른 예시로는 사용자 정의 컨테스트를 만들 수 있습니다. 이 때도, 마찬가지로 `__enter__`, `__exit__`의 메직메서드의 구현이 되어있어야합니다.

class Resource:
    def __enter__(self):
        print('Resource allocated')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('Resource released')

# 컨텍스트 관리자 사용
with Resource() as resource:
    print('Using the resource')

Resource allocated  # with 구문이 실행될 때
Using the resource  # print구문
Resource released   # with 구문의 종료

 

sqlite3나 mysqlDB등을 이용할때의 db connection을 명시적으로 종료하지않고 with구문으로 컨텍스트매니저로 사용할 수 있습니다.

import sqlite3

with sqlite3.connect('example.db') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    results = cursor.fetchall()
    print(results)

 

 

contextlib: python 표준라이브러리

1. @contextmanager: 이 데코레이터는 컨텍스트 메니저를 생성할때 코드를 줄여줍니다.

@contextmanager 데코레이터는 컨텍스트 관리자를 생성할 때 사용됩니다. 이 데코레이터를 사용하면 __enter__와 __exit__ 메서드를 명시적으로 구현하지 않고도 컨텍스트 관리자를 만들 수 있습니다. 대신, 함수 내부에서 yield를 사용하여 진입 및 종료 시 실행할 코드를 정의합니다.

아래와 같은 예시를 들어보겠습니다. 

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print(f'Elapsed time: {end - start} seconds')

# 사용 예시
with timer():   # start = time.time()이 서브루틴에서 할당됨. yield로 메인루틴으로 전환
    time.sleep(2)  # yield 이후 메인루틴의 실행, with 구문종료시 서브루틴의 finally 구문으로 전환

 

또 다른 예시로,DB의  FastAPI의 contextmanager을 디팬던시로 사용하는 경우입니다. 

FastAPI에서 contextmanager역할로 `__enter__`, `__exit__`메서드를 구현했지만, contextlib 내의 contextmangager 데코레이터를 이용하면 그럴필요는 없습니다.

 

아래와 같이 구현이 가능합니다.

from contextlib import contextmanager

@contextmanager
def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

# FastAPI dependency 사용 예시
@app.get("/items/")
async def read_items(db: DBSession = Depends(get_db)):
    # 데이터베이스 작업
    items = db.execute("SELECT * FROM items").fetchall()
    return items
반응형

 

객체표현: __repr__, __str__ 차이

파이썬에서 객체의 설명을 보려면, `repr()`, `str()`함수가 필요합니다. 이 두 함수는  `__repr__`와 `__str__`이 정의되어야 확인할 수 있습니다.  이 두 함수의 차이점은 아래와 같습니다.

공통점

  • 두 함수 모두 반환되는 형태가 `str` 입니다.

차이점

  • `repr`, `__repr__`은 주로 개발자을 위해 사용되며, 내부 디버깅을 위해 사용되는 표현방식입니다. 주로 `obj`을 직접 호출할 때 사용되고, 문자열 그 자체로 객체가 생성되도록 작성을 합니다.
  • `str`, `__str__`은 주로 사용자를 위해 사용되며, 보기쉬운 형태로 전환하거나 타입 캐스팅을 하는 용도로 사용됩니다. 또한, `__str__`이 정의되어있지 않다면, `__rerp__`이 호출되기도 합니다.
from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출 - __str__이 정의되지 않았으므로 __repr__이 대신 호출됩니다.
print(str(obj))  # MyClass(attribute1=10, attribute2='example')

 

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출 - __repr__이 정의되지 않았으므로 기본 구현이 호출됩니다.
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출
print(str(obj))  # MyClass with attribute1: 10 and attribute2: example

 

위의 코드에서 추가로 `MyClass(attribute1=10, attribute2='example')`을 그대로 호출해보면 객체가 생성이 가능합니다.

a = MyClass(attribute1=10, attribute2='example')
a
MyClass(attribute1=10, attribute2='example')

 

그 외에도, byte 시퀀스로 호출하려면 `byte()`함수를 호출하여 사용이 가능합니다. 이 때도, `__bytes__`매직메서드가 구현되어있어야합니다.

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

    def __bytes__(self):
        # 객체를 바이트 시퀀스로 변환
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2})".encode('utf-8')

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")
print(bytes(obj))  # b'MyClass(attribute1=10, attribute2=example)'
b'MyClass(attribute1=10, attribute2=example)'

 

객체 생성의 방법: @classmethod을 이용한 팩토리패턴

아래의 코드는 @classmethod에서 자기 자신의 클레스의 인스턴스를 생성 패턴입니다. 생성자를 꼭 만들필요없고, 대안적으로 아래와 같은 디자인 패턴이 가능합니다.

@dataclass
class MulticlassMetrics:
    accuracy: float
    auroc: float
    prauc: float

    @classmethod
    def calculate(
        cls, labels: np.ndarray, confidences: np.ndarray
    ) -> MulticlassMetrics:
        pred_label = np.argmax(confidences, axis=1)
        accuracy = (pred_label == labels).mean()
        auroc = calculate_auroc_ovr(confidences, labels)
        prauc = calculate_prauc_ovr(confidences, labels)

        return cls(accuracy, auroc, prauc)

    def to_dict(self, prefix=str()) -> dict:
        return {
            prefix + metric_name: round(value, 5)
            for metric_name, value in asdict(self).items()
        }

 

클레스 해시하기

파이썬 Set과 같은 기본자료구조에 넣으려면, 객체는 모두 hashable해야합니다. 즉, 해시값에 대해서 유일해야합니다. 이 내부적으로 hash()함수를 이용해서 객체를 해시하는데, 이를 이용하려면 `__hash__`을 정의해야합니다. 

이 디자인의 예시는 아래와 같습니다. HPO라는 온톨로지(개념)의 데이터클레스입니다. HPO의 id가 유일하므로 이 hash가 다르면 id가 내용이 각각 다름이 보장됩니다. 따라서, `__hash__`을 id을 기준으로 hash합니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

 

클레스 내 속성 보호하기: read only attributes : @property

파이썬에서는 비공개 속성(private attribute)을 생성하는 방법은 없지만, 수정을 막는 방법은 있습니다. @property을 이용하여 readonly로 작성할 수 있습니다.

아래의 예제 코드를 보면, all_subclasses을 `self._all_subclasses`로 저장해두고 @property로 읽어오려고합니다. 그리고, `_all_subclasses`을 `all_subclasses `을 통해서 볼 수는 있지만 직접 수정을 막기위해서, 감쌌습니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

    @property
    def all_subclasses(self) -> List[HPO]:
        """현재 클래스를 포함하여 모든 하위 클래스를 찾아 반환
        Return
            List[HPO]: 모든 하위 클래스의 ID 목록
        Example:
            >>> self.all_subclasses
            [
                HPO(...),
                HPO(...),
                ...
                HPO(...),
            ]
        """
        if hasattr(self, "_all_subclasses"):
            return self._all_subclasses

        container = set()
        self._get_subclasses_recursively(self, container)

        self._all_subclasses = [HPO(hpo_id) for hpo_id in container]
        return self._all_subclasses

 

__slot__ 으로 클레스 속성 메모리를 효율화 하기

파이썬에서는 객체의 속성이 생기면 `__dict__`라는 딕셔너리형 속성에 저장합니다. 따라서, 매우 빠르게 속성을 접근할 수 있습니다. 하지만, 반대로 속성이 매우 많은 객체라면, 메모리부담이 매우 커지게됩니다. 

아래와 같이 `__slots__`이라는 정의하면, 인스턴스에 `__dict__`라는 딕셔너리 속성이 저장되지않고, 고정된 튜플로만 관리합니다. 따라서, 동적으로 속성을 추가할 수는 없지만, dict객체를 따로 들고있지 않아도되니 속도가 빠르거나 메모리가 효율적으로 동작할 수 있습니다.

class Person:
    # __slots__를 사용하여 객체의 속성을 제한합니다.
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 객체 생성
person1 = Person('John', 30)

# 속성에 접근
print(person1.name)  # 출력: John
print(person1.age)   # 출력: 30

# 새로운 속성 추가 시 에러 발생
# person1.address = '123 Street'  # 'Person' object has no attribute 'address'

# __dict__를 사용하여 객체의 속성을 확인할 때 에러 발생
# print(person1.__dict__)  # AttributeError: 'Person' object has no attribute '__dict__'
반응형

 

개념정리

  • 코드 포인트(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']
반응형

+ Recent posts