python #2 객체지향 프로그래밍, 병렬처리

python #2 객체지향 프로그래밍, 병렬처리 #

#2025-08-13


1. 객체지향 프로그래밍 #

#1 property & dataclass (p.139-140)

image image

@property

  • diameter 메서드는 사실 _radius * 2라는 계산을 수행하지만 외부에선 c.diameter라고 쓰면 바로 10이라는 결과를 얻을 수 있다.
  • @diameter.setter를 사용하면 c.diameter = 20 형태로 diameter을 수정할수있고
    • 내부에서는 diameter을 받아 _radius=10으로 변환 저장한다.
  • fastapi에서 젤많이쓰는 기능이 속성화이다.

@dataclass

  • 보통 클래스를 만들면 __init__으로 생성자, __repr__으로 객체 출력 형식, __eq__로 동등성 비교 등을 직접 정의해야 하는데 @dataclass를 붙이면 이런 메서드들이 자동 생성된다.
  • Point 클래스는 x, y 좌표만 Point(1,2)로 정의했는데 이 상태로 객체 p1, p2를 생성하고 출력하면 Point(x=1, y=2)처럼 형식맞춰 나온다.
    • 그리고 == 비교 시 자동으로 True도 나온다.

#

#2 @property

# Order 클래스: 주문 내역 저장
class Order: 
    __slots__ = ("beverage", "quantity") # 인스턴스 속성을 beverage와 quantity로 고정하여 메모리 절약
    def __init__(self, beverage: Beverage, quantity: int) -> None: # beverage 타입 명시(Beverage), quantity 타입 명시 (int)
        self.beverage = beverage # self 객체에 beverage 객체 저장
        self.quantity = quantity # 주문 수량 quantity 저장
    @property
    def total_price(self) -> float: # total_price 프로퍼티: 주문금액 = 음료 가격 × 수량 자동 계산
        return self.beverage.price * self.quantity
  • class Order
    • 주문 정보를 저장하는 클래스
    • 속성: beverage, quantity
      • __slots__를 사용해 이 두 속성만 인스턴스에 저장할 수 있도록 제한했기 때문에 메모리 사용량이 줄고 실수로 다른 속성을 추가하는 것도 방지함.
    • total_price 메서드
      • @property로 정의됨
      • 주문 금액을 계산하는 로직을 담고 있지만 속성 접근처럼 쓸 수 있다 즉 order.total_price()가 아니라 order.total_price로 쓸수있다.

cf) @property 안썼으면?

# Order 클래스 1: @property 사용
class OrderWithProperty:
    @property
    def total_price(self) -> float:
        return self.beverage.price * self.quantity

# Order 클래스 2: @property 미사용
class OrderWithoutProperty:
    def total_price(self) -> float:
        return self.beverage.price * self.quantity

# 테스트
coffee = Beverage("아메리카노", 3000, ["커피", "아메리카노"])

order1 = OrderWithProperty(coffee, 2)
order2 = OrderWithoutProperty(coffee, 2)

print("=== @property 사용 ===")
print(order1.total_price)   # 괄호 없이 속성처럼 접근
# print(order1.total_price())  # 이렇게 하면 TypeError 발생

print("=== @property 미사용 ===")
print(order2.total_price()) # 반드시 괄호를 붙여 메서드 호출
print(order2.total_price)   # 괄호 없이 접근하면 메서드 객체가 출력됨
=== @property 사용 ===
6000
  • @property 사용하면 order.total_price로 괄호 없이 접근했을때, 내부에서 계산된 결과가 바로 반환되고
=== @property 미사용 ===
6000
<bound method OrderWithoutProperty.total_price of <__main__.OrderWithoutProperty object at 0x...>>
  • @property 사용 안하면 order.total_price()로 호출하면 6000이 나오고 괄호없이 호출하면 메서드 객체 참조만 나온다.

#

#3 @dataclass

# Beverage 클래스: 음료 데이터 정의
@dataclass 
class Beverage: 
    name: str # 음료 이름 속성
    price: float # 가격 속성
    tags: List[str] # 분류 태그 속성
  • class Beverage
    • 음료 정보를 저장하는 데이터 전용 클래스
    • 속성: name(문자열), price(실수형), tags(문자열 리스트)
    • @dataclass로 자동으로 __init__(생성자), __repr__(객체를 보기 좋게 출력), __eq__(값 비교) 같은 기본 메서드가 생성.

cf) @dataclass 안썼으면?

from typing import List

class Beverage:
    def __init__(self, name: str, price: float, tags: List[str]):
        self.name = name
        self.price = price
        self.tags = tags

    def __repr__(self):
        return f"Beverage(name={self.name!r}, price={self.price!r}, tags={self.tags!r})"

    def __eq__(self, other):
        if not isinstance(other, Beverage):
            return False
        return (self.name, self.price, self.tags) == (other.name, other.price, other.tags)
  • __init__ : 매개변수를 받아 속성을 초기화 / __repr__ : 객체를 보기 좋게 문자열로 표현 / __eq__ : 객체 간 동등성 비교 로직 작성 이렇게 하나하나 추가해야한다.

#

#4 decorator & closer (p.168-169)

image image

decorator

  • 데코레이터 (timer)
    • 함수 실행시간을 자동으로 측정
    • 내부에 wrapper 함수를 정의해서 slow function을 감싼다.
  • 흐름
    • wrapper는 시작시간기록, slow function 실행결과를 result에 저장하고 종료시간 기록, 걸린시간 계산, result를 반환
    • @timer -> slow function을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 slow function이 호출 -> 2초 대기 -> 작업완료 출력 -> 실행시간 result 출력
  • 의의
    • 함수를 호출하기 전후에 원하는 로직을 끼워 넣어 원래 함수의 기능은 그대로 두고 부가적인 기능을 쉽게 추가할 수 있게.

closure

  • outer()가 실행되면?
    • x = 10이 만들어지고 inner 함수가 정의됨
    • outer()는 inner 함수를 그 자체로 반환함 (inner의 결과를 반환하는게 아니고)
  • closure = outer()?
    • closure에 inner 함수가 저장
    • 이때 inner 함수는 자신이 정의될 당시의 환경(= x=10이 있던 outer의 스코프)을 함께 기억함
    • 그래서 outer가 끝나서 x 변수가 사라진 것처럼 보여도 closure()를 실행하면 여전히 x = 10에 접근 가능.
  • closure는 decorator처럼 @문법을 붙이지 않아도 적용된다.

#

#5 decorator와 closure 함께사용하기

# timeit을 사용하여 실행 성능 측정
def measure_time(func):
    def wrapper(*args, **kwargs):
        elapsed_time = timeit.timeit(lambda: func(*args, **kwargs), number=100)
        return elapsed_time
    return wrapper

@measure_time # 데코레이터: run_typed 실행시 자동으로 실행
def run_typed(test_data): 
    sum_of_squares_typed(test_data)

decorator

  • 데코레이터 measure_time
    • 실행 시간 측정
    • 내부에 wrapper 함수를 정의해서 run_typed을 감싼다.
  • 흐름
    • wrapper는 run_typed 실행결과를 elapsed_time에 저장한 뒤 반환
    • @measure_time -> run_typed을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 run_typed가 호출 -> 실행시간 elapsed_time 출력

closure

  • measure_time이 실행되면?
    • wrapper 함수가 정의됨, measure_time은 wrapper 함수를 그 자체로 반환함 (wrapper의 결과를 반환하는게 아니고)
  • run_typed에 @measure_time이 적용되면?
    • run_typed 함수 객체가 measure_time의 매개변수 func로 전달
    • measure_time 안에서 정의된 wrapper 함수는 자신이 정의될 당시의 환경(자기 바깥 함수의 지역 변수인 func)를 기억
    • 그래서 measure_time이 종료되어 원래 지역 변수 func가사라진 것처럼 보여도 wrapper 함수 내부에는 여전히 func에 대한 참조가 살아 있다.

#

2. 병렬처리 #

#1 multithreading (p.189)

image
  • 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유
  • 예제 코드
    • print_numbers와 print_letters를 각각 thread1 thread2로 실행
    • 결과
      • 숫자 1부터 5까지와 알파벳 A부터 E까지가 1초 간격으로 번갈아 출력

#

#2 mutliprocessing (p.191)

image
  • 함수가 완전히 독립된 프로세스로 실행
  • 예제 코드
    • print_numbers와 print_letters를 독립적인 프로세스 process1 process2로 실행
    • 결과
      • 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위, 프로세스 생성 시점의 지연 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고
      • 그 결과 숫자 1-5를 전부 찍고 난 후 알파벳 A-E를 찍는 식으로 출력이 묶음 단위로 나타난다.

#

#3 multithreading & mutliprocessing

multithreading

  • 두 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유한다.
  • 예제에서 숫자를 찍는 함수와 알파벳을 찍는 함수 각각이 독립적인 스레드로 동작하지만 동일한 프로세스의 GIL(Global Interpreter Lock)을 공유하기 때문에 한 번에 한 스레드만 실제로 파이썬 바이트코드를 실행한다.
  • time.sleep(1)로 실행 권한을 번갈아 준 결과 숫자를 하나 찍고 잠시 멈춘 사이 다른 스레드가 알파벳을 찍는 식으로 출력이 교차되고 실행 타이밍에 따라 순서가 조금씩 섞여 나타난다 즉 두 작업이 거의 동시에 진행되는 것처럼 보이지만 사실은 GIL과 sleep 호출에 의해 미세하게 번갈아 실행된다.

mutliprocessing

  • 각 함수가 완전히 독립된 프로세스로 실행된다.
  • 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고 그 결과 숫자 1-5를 전부 찍고 난 후에 알파벳 A-E를 찍는 식으로, 출력이 묶음 단위로 나타나게 된다.

사실잘모르겟다…어렵다,,,,

결론

  • 멀티스레딩은 하나의 프로세스 안에서 협력적으로 실행을 나누기 때문에 출력이 교차되거나 순서가 섞이기 쉽고, 멀티프로세싱은 프로세스 단위로 완전히 병렬 실행되지만 OS 스케줄링 특성상 한쪽이 먼저 실행을 마쳐 출력이 블록처럼 모이는 경우가 많다.

#

#4 MutClust에서 mutliprocessing 코드

def get_mutInfo(target_dir, meta_df, savefilepath):
    if os.path.exists(savefilepath):
        return readPickle(savefilepath)

    # 병렬 처리를 위한 프로세스 풀 생성
    num_processes = 50  # 병렬로 처리할 프로세스 수를 조정
    with Pool(num_processes) as pool:
        mutInfo_files = [a for a in get_file_paths_recursive(target_dir) if os.path.basename(a).split('.')[0] in meta_df.index]
        results = pool.map(process_mutInfo, mutInfo_files)

    # 결과를 딕셔너리로 변환
    seq_dict = {sid: mutInfo_df for sid, mutInfo_df in results}

    total_df = pd.DataFrame().from_dict(seq_dict)

    savePickle(savefilepath, total_df)
    return total_df

MutClust 예전 utils 코드중에서 병렬처리 코드 있었던거같아서 찾아봣다

흐름은

  1. multiprocessing.Pool을 이용해 최대 50개의 프로세스를 동시에 실행할 수 있도록 풀을 생성
  2. target_dir 디렉토리 내 파일이 meta_df의 인덱스 이름에 포함되어 있는 경우만 남겨서 mutInfo_files 생성
  3. mutInfo_files를 pool.map(process_mutInfo, mutInfo_files)에 전달
    • process_mutInfo: 병렬로 process_mutInfo 함수에 의해 처리(mutInfo_files를 읽고 sid, mutInfo_df 생성)
  4. key가 sid, 값이 mutInfo_df인 딕셔너리 seq_dict로 만들고 total_df로 정리

결론

  • 변이 정보를 병렬 프로세스(50개)로 빠르게 처리하고 결과를 df로 정리해서 저장해놓고 썼다.
  • 결과파일 저장해놓은뒤로 사용한적없어서 utils에서 빠진거같고 기억에서도 빠진것같다(..)

#