반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 6.3의 내용을 읽고 요약한 글입니다. 

6.3.1 메모리 관점에서 입출력

  • 단순히 메모리의 복사(copy)일 뿐, 그 이상도 이하도 아님
  • 그럼 파일 내용을 읽는 것을 예로 들면, 데이터는 잔치에서 프로세스 주소 공간으로 어떻게 복사될까?

6.3.2 read 함수는 어떻게 파일을 읽을까?

  • 단일 코어 CPU 시스템에 프로세스 A와 프로세스 B 두개가 있고, 프로세스 A가 현재 실행중이라고 가정
1. (속에) put (sth in/into sth); (끼우다) insert; (채우다) pack, stuff
2. (손에) get, obtain, come by
3. (추가하다) add, include

 

  • 프로세스 A에는 파일을 읽는 코드가 있고, 먼저 데이터를 저장하는 버퍼를 정의 후 다음과 같이 read 계열의 함수를 호출
char buffer[LEN];
read(buffer);
  • 저수준 계층에서 시스템 호출을 이용하여 운영체제에 파일 읽기 요청을 보냄
  • 요청은 커널에서 디스크가 이해할 수 있는 명령어로 변환되어 디스크로 전송
  • CPU가 명령어를 실행하는 속도에 비해 디스크 입출력은 매우 느려 운영체제는 귀중한 계산 리소스를 불필요한 대기에 낭비할 수 없음
  • 운영체제는 현재 프로세스(프로세스 A)의 실행을 일시 중지하고 입출력 블로킹 대기열에 넣음

  • 운영체제는 이미 디스크에 입출력 요청을 보낸 상태이며 데이터를 특정 메모리 영역으로 복사하는 작업 시작

 

프로세스 B는 준비 완료 대기열에 들어감

 

  • CPU 코어수에 비해 프로세스의 수가 훨씬 많음. 그래서 이미 다시 실행될 준비가 된 프로세스가 있더라도 바로 CPU가 할당되지 못할 수 있기 때문에 준비 완료 대기열에 들어감

프로세스 A가 블로킹 입출력 요청을 시작해서 일시 중지되더라도 CPU는 쉬지 않고 준비 완료 대기열에서 실행할 수 있는 프로세스를 찾아 실행

  • 프로세스 B는 CPU가 실행하고 있으며 디스크는 프로세스 A의 메모리 공간에 데이터를 쓰고 있음

 

  • 이후 데이터가 프로세스 A의 메모리에 복사하는 작업이 완료되면, 디스크는 CPU에 인터럽트 신호를 보냄
  • CPU는 인터럽트 신호를 받은 후 처리가 중단되었던 함수로 점프, 디스크의 입출력 처리가 완료되었음을 알게 됨
  • 운영체제는 프로세스 A를 입출력 블로킹 대기열에서 꺼내 다시 준비 완료 대기열에 넣음

  • 운영체제는 CPU를 프로세스 A에 할당할지, B에 할당할 지 결정해야 함 → 프로세스 B에 할당된 CPU 시간이 아직 남아 계속 B를 실행하는 것으로 가정
  • 프로세스 A는 계속 대기하고, 일정 시간 후 타이머 인터럽트 신호가 발생하면 운영체제는 프로세스 B를 준비 완료 대기열로 넣음과 동시에 프로세스 A를 준비 완료 대기열에서 꺼내 CPU를 할당

반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 6.2의 내용을 읽고 요약한 글입니다. 

  • 최신 컴퓨터 시스템의 경우, 사실 디스크가 입출력을 처리할 때 CPU개입이 필요하지 않음. 이유는? 아래의 장치 제어기, 직접 메모리 접근, 인터럽트의 관계를 이해해야 함
  • 디스크 입출력 요청을 처리하는 동안 운영 체제는 CPU가 다른 작업을 수행하도록 스케쥴링함

상황

  • CPU가 스레드 1을 실행하기 시작하고 일정 시간 지난 후 파일 읽기와 같은 디스크 관련 입출력을 요청한다고 가정
  • 디스크 입출력은 CPU 속도에 비해 매우 느리기 때문에 입출력 요청의 처리가 완료되기 전까지 스레드 1은 앞으로 나아갈 수 없음
  • 쓰레드 1의 실행은 일시 중지 되고 CPU가 준비 완료 상태인 다른 스레드(스레드 2)에 할당됨
  • 스레드 2가 실행되고, 스레드 1의 디스크 입출력 요청 처리가 완료되면 CPU는 스레드 1을 이어서 실행

6.2.1 장치 제어기

  • 디스크와 같은 입출력 장치는 대체로 기계 부분과 전자 부분으로 나눌 수 있음

기계 부분

출처: https://babytiger.netlify.app/posts/hdd/

  • 플래터(Platter): 실질적으로 데이터가 저장되는 곳, 동그란 원판 모양
  • 스핀들(Spindle): 플래터를 회전시키는 구성요소
  • 헤드(R/W Head): 플래터 위에 미세하게 떠있어 데이터를 읽고 씀
  • Actuator Arm:헤드를 원하는 위치로 이동시킴

입출력 요청이 왔을 때 헤드를 통해 데이터를 읽고 쓰는데 데이터가 헤드가 위치한 트랙에 없을 가능성이 있음

  • 트랙: 플래터를 동심원으로 나누었을 때 그중 하나의 원
  • 이때 헤드가 특정 트랙으로 이동해야 하는데 이 과정을 탐색(seek)이라고 하며 매우 많은 시간이 소모
 

전자 부분

  • 전자 부품으로 구성되어 장치 제어기(device controller)라고 함
  • 자체적인 프로세서와 펌웨어, 버퍼, 레지스터를 갖추고 있어 CPU가 직접 도와주지 않는 상황에서도 복잡한 작업을 할 수 있음
  • 장치드라이버(device driver): 운영체제에 속한 코드인데 반해, 장치 제어기는 장치 드라이버에서 명령을 받아 외부 장치를 제어하는 하드웨어

⇒ 장치 제어기는 운영 체제에 해당하는 장치 드라이버와 외부 장치를 연결하는 다리에 해당

 

6.2.2 CPU가 직접 데이터를 복사해야 할까?

  • 디스크에서 자체 버퍼로 데이터를 읽었다면, 이후 CPU는 직접 데이터 전송 명령어를 실행하여 장치 제어기 버퍼에 있는 데이터를 따로 메모리로 복사해야 할까? 그렇지 않음

  • CPU 개입이 없는 상황에서 직접 장치와 메모리 사이에 데이터를 전송할 수 있는 작동방식을 직접 메모리 접근(direct memory access, DMA)라고 부름

 

6.2.3 직접 메모리 접근

  • CPU가 관여하지 않고, 메모리와 외부 장치 사이에서 데이터를 이동시키는 역할을 하는 것이 직접 메모리 접근
  • CPU가 직접 데이터를 복사하지 않지만, 반드시 어떻게 데이터를 복사할지 알려주는 명령어를 DMA에 전달해야 함
  • DMA는 bus arbitration, 즉 버스 사용권한을 요청한 후 장치를 작동시킴
  • 디스크에서 데이터를 읽을 때 장치 제어기의 버퍼에서 데이터를 읽으면 DMA가 지정된 메모리 주소에 데이터를 쓰는 방식으로 데이터 복사가 완료됨
  • DMA가 데이터 전송을 완료하면 인터럽트 작동 방식을 사용하여 CPU에 알려줌

 

6.2.4 전 과정 정리

스레드 1, 2가 있다고 가정

  • CPU로 실행되는 스레드 1이 시스템 호출로 입출력 요청을 시작하면, 운영 체제는 스레드 1의 실행을 일시 중지하고 CPU를 스레드 2에 할당 후 실행됨
  • 디스크가 동작하여 데이터 준비가 되면, DMA 작동 방식이 직접 장치와 메모리 사이에서 데이터를 전송
  • 데이터 전송이 완료되면 인터럽트 작동 방식을 이용하여 CPU에 알리고 CPU는 스레드 2를 중지 후 스레드 1이 다시 실행

⇒ 핵심은 디스크가 입출력 요청을 처리할 때 CPU가 그 자리에서 기다리지 않고 운영 체제의 스케쥴링에 따라 다른 스레드를 실행한다는 것

반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 6.1의 내용을 읽고 요약한 글입니다. 

 

6.1.5 배달 음식 주문과 중단 처리

  • 배달 음식을 주문 후 즐겁게 게임을 한다고 가정
  • 게임 중 배달 음식이 도착하면 게임을 일시 중지한 후 배달 음식을 받고 자리로 돌아와 게임을 이어할 수 있음 → 인터럽트 처리 과정

 

컴퓨터 시스템에서 기초적인 인터럽트 처리 구조

 
  • CPU가 특정 프로세스(프로세스 A)의 기계 명령어를 실행할 때 새로운 이벤트가 발생
    • 네트워크 카드에 새로운 데이터가 들어오면 외부 장치가 인터럽트 신호를 보내고
    • CPU는 실행 중인 현재 작업의 우선순위가 인터럽트 요청보다 높은지 판단
    • 인터럽트의 우선순위가 더 높다면 현재 작업 실행으 일시 중지하고 인터럽트를 처리하고 다시 현재 작업으로 돌아옴
  • 프로그램은 계속 끊임없이 실행되는 것이 아니라 언제든지 장치에 의해 실행이 중단될 수 있음
  • 하지만 이 과정은 프로그래머에게 드러나지 않으며 중단 없이 실행되고 있는 것처럼 느끼게 만듦

 

6.1.6. 인터럽트 구동식 입출력

인터럽트 발생 시 CPU가 실행하는 명령어 흐름

프로그램 A의 기계 명령어 n 실행
프로그램 A의 기계 명령어 n + 1 실행
프로그램 A의 기계 명령어 n + 2 실행
프로그램 A의 기계 명령어 n + 3 실행
인터럽트 신호 감지
프로그램 A의 실행 상태 저장
인터럽트 처리 기계 명령어 m 실행
인터럽트 처리 기계 명령어 m + 1 실행
인터럽트 처리 기계 명령어 m + 2 실행
인터럽트 처리 기계 명령어 m + 3 실행
프로그램 A의 실행 상태 복원
프로그램 A의 기계 명령어 n + 4 실행
프로그램 A의 기계 명령어 n + 5 실행
프로그램 A의 기계 명령어 n + 6 실행
프로그램 A의 기계 명령어 n + 7 실행
  • 폴링 방식보다 효율적으로 시간을 낭비하지 않음
    • 실제는 약간의 시간을 낭비하는데 주로 프로그램 A의 실행 상태를 저장하고 복원하는데 사용

프로그램 A의 관점에서 CPU가 실행하는 명령어 흐름은 아래와 같음

프로그램 A의 기계 명령어 n 실행
프로그램 A의 기계 명령어 n + 1 실행
프로그램 A의 기계 명령어 n + 2 실행
프로그램 A의 기계 명령어 n + 3 실행
프로그램 A의 기계 명령어 n + 4 실행
프로그램 A의 기계 명령어 n + 5 실행
프로그램 A의 기계 명령어 n + 6 실행
프로그램 A의 기계 명령어 n + 7 실행
  • CPU는 마치 중단된 적이 없는 것처럼 자신의 명령어를 계속 실행 → 프로그램 A의 실행 상태를 저장하고 복원하는 작업이 필요한 이유로 입출력을 비동기로 처리하는 방법을 인터럽트 구동식 입출력이라고 함 (interrupt driven input and output)

6.1.7 CPU는 어떻게 인터럽트 신호를 감지할까?

CPU가 기계 명령어를 실행하는 과정

  • 명령어 인출(instruction fetch)
  • 명령어 해독(instruction decode)
  • 실행(execute)
  • 다시 쓰기(write back)
  • CPU가 하드웨어의 인터럽트 신호를 감지하는 단계

인터럽트 신호가 발생하면 이 이벤트를 처리할지 여부를 반드시 결정해야 함

6.1.8 인터럽트 처리와 함수 호출의 차이

  • 함수를 호출하기 이전에 반환 주소, 일부 범용 레지스터의 값, 매개변수 등 정보 저장이 필요
  • 인터럽트 처리 점프는 서로 다른 두 실행 흐름을 포함하므로 함수 호출에 비해 저장해야 할 정보가 훨씬 많음

6.1.9 중단된 프로그램의 실행 상태 저장과 복원

 

  • 프로그램 A가 실행 중일 때 인터럽트가 발생하면 A의 실행은 중단되고, CPU는 인터럽트 처리 프로그램(interrupt handler) B로 점프
  • CPU가 인터럽트 처리 프로그램 B를 실행할 때 다시 인터럽트가 발생하면 B는 중단되고 CPU는 인터럽트 처리 프로그램 C로 점프
  • CPU가 인터럽트 처리 프로그램 C를 실행할 때 도 다시 인터럽트가 발생하면 C의 실행은 중단되고 CPU는 인터럽트 처리 프로그램 D로 점프
  • D의 실행이 완료되면 프로그램 C, B, A 순서대로 반환됨

상태 저장 순서

  1. 프로그램 A의 상태 저장
  2. 프로그램 B의 상태 저장
  3. 프로그램 C의 상태 저장

상태 복원 순서

  1. 프로그램 C의 상태 복원
  2. 프로그램 B의 상태 복원
  3. 프로그램 A의 상태 복원

상태가 먼저 저장될수록 상태 복원은 더 나중에 됨 → 스택를 사용해 구현하고, 스택에는 다음 기계 명령어 주소와 프로그램의 상태가 저장됨

 

 

반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 6.1의 내용을 읽고 요약한 글입니다. 

CPU는 어떻게 입출력 작업을 처리할까?

  • CPU 내부에 레지스터가 있는 것과 마찬가지로, 장치에도 자체적인 레지스터인 장치 레지스터(device register)가 있음
    1. 데이터를 저장하는 레지스터: 사용자가 키보드의 키를 누르면 그 정보는 이런 레지스터에 저장됨
    2. 제어 정보와 상태 정보를 저장하는 레지스터: 읽고 쓰는 작업을 이용하여 장치를 제어하거나 상태를 볼 수 있음

6.1.1 전문적으로 처리하기: 입출력 기계 명령어

Q. 어떤 장치 레지스터를 읽고 써야 하는지 어떻게 알 수 있을까?

A. 장치마다 고유한 주소가 부여되며, 입출력 명령어에 장치 주소를 지정함

6.1.2 메모리 사상 입출력

메모리 사상 입출력(memory mapping input and output): 주소 공간의 일부분을 장치에 할당하여 메모리를 읽고 쓰는 것처럼 장치를 제어하는 방법

  • LOAD와 STORE 명령어 그 자체만으로는 메모리인지, 장치 레지스터를 읽고 쓰는것인지 구분할 수 없으며 추가 적인 정보가 필요 → 메모리 주소 공간(memory address space)

메모리 주소 공간과 실제 메모리 주소는 서로 다른 개념

  • 기계 명령어 관점에서 CPU에 보이는 것은 주소 공간으로 해당 주소의 데이터가 어디에서 오는지 관심을 가질 필요 없음 → 주소 공간의 일부를 장치에 할당할 수 있음

6.1.3 CPU가 키보드를 읽고 쓰는 것의 본질

키보드의 레지스터가 주소 공간의 0xfe00에 매핑되어있다고 가정하면, CPU가 키보드를 읽는 기계 명령어는 아래와 같음

Load R1 0xFE00
  • 주소 공간 oxfe00값을 CPU 레지스터에 적재하는 이 명령어를 이용하여 CPU가 어떻게 키보드에서 데이터를 가져오는지 확인할 수 있음

하지만 문제는 사용자가 언제 키보드를 누를지 확실하지 않음. 어떻게 데이터를 언제 읽어야 할지 알 수 있을까?

  • 최신 CPU의 클럭 주파수는 2~3GHz. 2GHz라고 가정하면, 하나의 클럭 주기는 1/2,000,000,000 = 0.5ns
  • 하나의 기계 명령어가 하나의 클럭 주기마다 실행된다고 가정했을 때, CPU의 속도에 키보드로 입력하는 글자의 속도를 맞춘다면 1초에 20억개를 입력해야 함 → 불가능

⇒ CPU는 특정한 방법을 사용하여 장치의 작업 상태를 얻어야 하는데 이것이 바로 장치 상태 레지스터의 역할

6.1.4 폴링: 계속 검사하기

키보드에서 누른 키의 데이터를 저장하는 레지스터는 0xfe01, 상태 레지스터는 0xfe00위치에 매핑 되어있다고 가정

START
  Load R1 0xFE00        (1)
  BLZ START             (2)
  LOAD R0 0xFE01        (3)
  BL   OTHER_TASK       (4)

(1) Load R1 0xFE00: 현재 키보드 상태를 읽음

(2) BLZ START: 분기 점프 명령어로, 키보드 상태 레지스터 값이 0(아무도 키보드를 누르지 않은 상태)이면, 시작 위치(START)로 점프하여 키보드의 현재 상태 확인, 1이면 아래로 진행

(3) LOAD R0 OxFE01: 키보드의 데이터는 R0 레지스터에 저장

(4) BL OTHER_TASK: 아무론 조건 없이 다른 작업으로 점프

이 코드를 고급언어로 번역하면 다음과 같음

while (키가 눌리지 않음)
{
     ; // 키가 눌릴 때까지 대기 
}

키보드 데이터 읽기
  • 이런 입출력 구현 방법에 polling이라는 매우 직관적인 이름이 붙어있음
  • 문제는 사용자가 키를 누르지 않을 때 CPU는 항상 불필요하게 순환하며 대기하게 됨
  • 일종의 동기식 설계 방식으로 개선할 수 있는 방법은 비동기로 바꾸는 것
 
 
반응형
반응형

이글은 책 "파이썬 클린 코드" ch2의 내용을 읽고 요약 및 추가한 내용입니다. 

 

예시: R-Trie 자료 구조에 대한 노드 모델링

  • 문자열에 대한 빠른 검색을 위한 자료구조라는 정도로만 알고 넘어가기
  • 현재의 문자를 나타내는 value, 다음에 나올 문자를 나타내는 next_ 배열을 가지고 있음
  • linked list나 tree 형태와 비슷

 

from typing import List
from dataclasses import dataclass, field

R = 26

@dataclass
class RTrieNode:
    size = R
    value: int
    next_: List["RTrieNode"] = field(default_factory=lambda: [None] * R)

    def __post_init__(self):
        if len(self.next_) != self.size:
            raise ValueError(f"리스트(next_)의 길이가 유효하지 않음")

  • size는 class variable로 모든 객체가 값을 공유
  • value는 정수형이지만 기본값이 없으므로 객체 생성시 반드시 값을 정해줘야 함
  • next_는 R크기 만큼의 길이를 가진 list로 초기화
  • __post_init__은 next_가 원하는 형태로 잘 생성되었는지 확인하는 검증
from typing import List
from dataclasses import dataclass, field

R = 26  # 영어 알파벳

@dataclass
class RTrieNode:
    size = R
    value: int
    next_: List["RTrieNode"] = field(default_factory=list)

    def __post_init__(self):
        if len(self.next_) != self.size:
            raise ValueError(f"리스트(next_)의 길이가 유효하지 않음")

rt_node = RTrieNode(value=0) # ValueError: 리스트(next_)의 길이가 유효하지 않음

 

이터러블 객체

__iter__ 매직 메소드를 구현한 객체

파이썬의 반복은 이터러블 프로토콜이라는 자체 프로토콜을 사용해 동작

for e in my_object

위 형태로 객체를 반복할 수 있는지 확인하기 위해 파이썬은 고수준에서 아래 두가지 차례로 검사

  • 객체가 __next__나 __iter__ 메서드 중 하나를 포함하는지 여부
  • 객체가 시퀀스이고 __len__과 __getitem__을 모두 가졌는지 여부

For-loop에 대한 구체적인 과정

my_list = ["사과", "딸기", "바나나"]

for i in my_list:
    print(i)
  1. for 문이 시작할 때 my_list의 __iter__()로 iterator를 생성
  2. 내부적으로 i = __next__() 호출
  3. StopIteration 예외가 발생하면 반복문 종료

Iterable과 Iterator의 차이

  • Iterable: loop에서 반복될 수 있는 python 객체, __iter__() 가 구현되어있어야 함
  • Iterator: iterable 객체에서 __iter__() 호출로 생성된 객체로 __iter__()와 __next__()가 있어야하고, iteration 시 현재의 순서를 가지고 있어야 함

 

이터러블 객체 만들기

객체 반복 시 iter() 함수를 호출하고 이 함수는 해당 객체에 __iter__ 메소드가 있는지 확인

from datetime import timedelta
from datetime import date

class DateRangeIterable:
    """자체 이터레이터 메서드를 가지고 있는 iterable"""

    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date

    def __iter__(self):
        return self # 객체 자신이 iterable 임을 나타냄

    def __next__(self):
        if self._present_day >= self.end_date:
            raise StopIteration()
        today = self._present_day
        self._present_day += timedelta(days=1)

        return today

for day in DateRangeIterable(date(2024, 6, 1), date(2024, 6, 4)):
    print(day)

2024-06-01
2024-06-02
2024-06-03

  • for 루프에서 python은 객체의 iter() 함수를 호출하고 이 함수는 __iter__ 매직 메소드를 호출
  • self를 반환하면서 객체 자신이 iterable임을 나타냄
  • 루프의 각 단계에서마다 자신의 next() 함수를 호출
  • next 함수는 다시 __next__ 메소드에게 위임하여 요소를 어떻게 생산하고 하나씩 반환할 것인지 결정
    • 더 이상 생산할 것이 없는 경우 파이썬에게 StopIteration 예외를 발생시켜 알려줘야함

⇒  for 루프가 작동하는 원리는 StopIteration 예외가 발생할 때까지 next()를 호출하는 것과 같다

 

from datetime import timedelta
from datetime import date

class DateRangeIterable:
    """자체 이터레이터 메서드를 가지고 있는 이터러블"""

    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date

    def __iter__(self):
        return self

    def __next__(self):
        if self._present_day >= self.end_date:
            raise StopIteration()
        today = self._present_day
        self._present_day += timedelta(days=1)

        return today

r = DateRangeIterable(date(2024, 6, 1), date(2024, 6, 4))
print(next(r))  # 2024-06-01
print(next(r))  # 2024-06-02
print(next(r))  # 2024-06-03
print(next(r))  # raise StopIteration()

위 예제는 잘 동작하지만 하나의 작은 문제가 있음

max 함수 설명

  • iterable한 object를 받아서 그 중 최댓값을 반환하는 내장함수이다
  • 숫자형뿐만 아니라 문자열 또한 비교 가능
str1 = 'asdzCda'
print(max(str1)) # z

str2 = ['abc', 'abd']
print(max(str2)) # abd 유니코드가 큰 값

str3 = ['2022-01-01', '2022-01-02']
print(max(str3)) # 2022-01-02 
# 숫자로 이루어진 문자열을 비교할 때 각 문자열의 앞 부분을 비교해서 숫자가 큰 것을 출력

 

r1 = DateRangeIterable(date(2024, 6, 1), date(2024, 6, 4))

a = ", ".join(map(str, r1))  # "2024-06-01, 2024-06-02, 2024-06-03"
print(max(r1))

ValueError: max() iterable argument is empty

  • 문제가 발생하는 이유는 이터러블 프로토콜이 작동하는 방식 때문
    • 이터러블의 __iter__ 메소드는 이터레이터를 반환하고 이 이터레이터를 사용해 반복
    • 위의 예제에서 __iter__ 는 self를 반환했지만 호출될 때마다 새로운 이터레이터를 만들 수 있음
    • 매번 새로운 DateRangeIterable 인스턴스를 만들어서 해결 가능하지만 __iter__에서 제너레이터(이터레이터 객체)를 사용할 수도 있음

 

from datetime import timedelta
from datetime import date

class DateRangeIterable:
    """자체 이터레이터 메서드를 가지고 있는 이터러블"""

    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date

    def __iter__(self):
        current_day = self.start_date
        while current_day < self.end_date:
            yield current_day
            current_day += timedelta(days=1)

    def __next__(self):
        if self._present_day >= self.end_date:
            raise StopIteration()
        today = self._present_day
        self._present_day += timedelta(days=1)

        return today

r1 = DateRangeIterable(date(2024, 6, 1), date(2024, 6, 4))

a = ", ".join(map(str, r1))  # 2024-06-01, 2024-06-02, 2024-06-03
print(max(r1))  # 2024-06-03
  • 달라진 점은 각각의 for loop은 __iter__를 호출하고 이는 제너레이터를 생성

⇒ 이러한 형태의 객체를 컨테이너 이터러블(container iterable)이라고 함

 

다른 방법

  • iterable과 iterator 객체를 분리
from datetime import timedelta, date

class DateRangeIterator:
    """Iterator for DateRangeIterable."""

    def __init__(self, start_date, end_date):
        self.current_date = start_date
        self.end_date = end_date

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_date >= self.end_date:
            raise StopIteration()
        today = self.current_date
        self.current_date += timedelta(days=1)
        return today

class DateRangeIterable:
    """Iterable for a range of dates."""

    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date

    def __iter__(self):
        return DateRangeIterator(self.start_date, self.end_date)

r1 = DateRangeIterable(date(2024, 6, 1), date(2024, 6, 4))

# Using join with map
print(", ".join(map(str, r1)))  # Output: 2024-06-01, 2024-06-02, 2024-06-03

# Using max
print(max(r1))  # Output: 2024-06-03

  • DateRangeIterable 에서 __iter__가 호출될 때 마다 새로운 Iterator 를 생성할 수도 있음

 

시퀀스 만들기

객체에 __iter__ 메소드를 정의하지 않았지만 반복하기를 원하는 경우도 있음

객체에 __iter__ 가 정의되어 있지 않으면 __getitem__을 찾고 없으면 TypeError를 발생시킴

시퀀스는 __len__과 __getitem__을 구현하고 첫번째 인덱스0부터 시작하여 포함된 요소를 한 번에 하나씩 가져올 수 있어야 함

이터러블 객체는 메모리를 적게 사용한다는 장점이 있음

  • n번째 요소를 얻고 싶다면 도달할 때까지 n번 반복해야하는 단점이 있음 (시간복잡도: O(n))

⇒CPU 메모리 사이의 trade-off

__iter__, __getitem__ 모두 없는 경우

from datetime import timedelta, date

class DateRangeSequence:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._range = self._create_range()

    def _create_range(self):
        days = []
        current_day = self.start_date
        while current_day < self.end_date:
            days.append(current_day)
            current_day += timedelta(days=1)
        return days

    # def __getitem__(self, day_no):
    #     return self._range[day_no]

    def __len__(self):
        return len(self._range)

s1 = DateRangeSequence(date(2022, 1, 1), date(2022, 1, 5))
for day in s1:
    print(day)

TypeError: 'DateRangeSequence' object is not iterable

 

__getitem__있는 경우

from datetime import timedelta, date

class DateRangeSequence:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._range = self._create_range()

    def _create_range(self):
        days = []
        current_day = self.start_date
        while current_day < self.end_date:
            days.append(current_day)
            current_day += timedelta(days=1)
        return days

    def __getitem__(self, day_no):
        return self._range[day_no]

    def __len__(self):
        return len(self._range)

s1 = DateRangeSequence(date(2022, 1, 1), date(2022, 1, 5))
for day in s1:
    print(day)

2022-01-01
2022-01-02
2022-01-03
2022-01-04

  • __iter__ 없어도 for loop에 사용할 수 있음

 

컨테이너 객체

__contains__ 메서드를 구현한 객체. 일반적으로 boolean 값을 반환하고 이 메서드는 파이썬에서 in 키워드가 발견될 때 호출됨

element in container

위 코드를 파이썬은 아래와 같이 해석 (잘활용하면 가독성이 정말 높아짐)

container.__contains_(element)

 

def mark_coordinate(grid, coord):
    if 0<= coord.x < grid.width and 0<= coord.y < grid.height:
        grid[coord] = MARKED
  • grid내에 coord 좌표가 포함되는지 여부를 확인하는 코드

Grid 객체 스스로 특정 좌표가 자신의 영역안에 포함되는지 여부를 판단할 수는 없을까? 더 작은 객체 (Boundaries)에 위임하면 어떨까?

  • 컴포지션을 사용하여 포함관계를 표현하고 다른 클래스에 책임을 분배하고 컨테이너 매직 메소드를 사용
class Boundaries:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __contains__(self, coord):
        x, y = coord
        return 0 <= x < self.width and 0 <= y < self.height

class Grid:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.limits = Boundaries(width, height)

    def __contains__(self, coord):
        return coord in self.limits

 

Composition 관계 사용 전

def mark_coordinate(grid, coord):
    if 0<= coord.x < grid.width and 0<= coord.y < grid.height:
        grid[coord] = MARKED

Composition 관계 사용 후

def mark_coordinate(grid, coord):
    if coord in grid:
        grid[coord] = MARKED

 

객체의 동적인 생성

__getattr__ 매직 메소드를 사용하면 객체가 속성에 접근하는 방법을 제어할 수 있음

myobject.myattribute 형태로 객체의 속성에 접근하려면 instance의 속성 정보를 가지고 __dict__에 myattribute가 있는지 검색.

  • 해당 이름의 속성이 있으면 __getattribute__메소드를 호출
  • 없는 경우 조회하려는 속성(myattribute) 이름을 파라미터로 __getattr__ 호출
class DynamicAttributes:
    def __init__(self, attribute):
        self.attribute = attribute

    def __getattr__(self, attr):
        if attr.startswith("fallback_"):
            name = attr.replace("fallback_", "")
            return f"[fallback resolved] {name}"
        raise AttributeError(f"{self.__class__.__name__}에는 {attr} 속성이 없음")

dyn = DynamicAttributes("value")
print(dyn.attribute)  # value

print(dyn.fallback_test)  # [fallback resolved] test

dyn.__dict__["fallback_new"] = "new value" # dict로 직접 인스턴스에 추가
print(dyn.fallback_new)  # new value 

print(getattr(dyn, "something", "default"))  # default

호출형 객체(callable)

  • 함수처럼 동작하는 객체를 만들면 데코레이터 등 편리하게 사용 가능
    • __call__ 매직 메소드가 호출됨
from collections import defaultdict

class CallCount:
    def __init__(self):
        self._counts = defaultdict(int)

    def __call__(self, argument):
        self._counts[argument] += 1
        return self._counts[argument]

cc = CallCount()
print(cc(1))  # 1
print(cc(2))  # 1
print(cc(1))  # 2
print(cc(1))  # 3
print(cc("something"))  # 1
print(callable(cc))  # True

매직 메소드 요약

사용 예 매직 메서드 비고

사용예 매직 메소드 비고
obj[key]
obj[i:j]
obj[i:j:k]
__getitem__(key) 첨자형(subscriptable) 객체
with obj: ... __enter__ / __exit__ 컨텍스트 관리자
for i in obj: ... __iter__ / __next__
__len__ / __getitem__
이터러블 객체
시퀀스
obj.<attribute> __getattr__ 동적 속성 조회
obj(*args, **kwargs) __call__(*arg, **kwargs) 호출형(callable) 객체
  • 이러한 매직 메소드를 올바르게 구현하고 같이 구현해야 하는 조합이 뭔지 확인하는 가장 좋은 방법은 collections.abc 모듈에서 정의된 추상클래스를 상속하는 것

파이썬에서 유의할 점

mutable 파라미터의 기본 값

def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}):
    name = user_metadata.pop("name")
    age = user_metadata.pop("age")

    return f"{name} ({age})"

2가지 문제 존재

  1. 변경 가능한 기본 값을 사용한 것. 함수의 본문에서 수정 가능한 객체의 값을 직접 수정하여 부작용 발생
  2. 기본 인자
    1. 함수에 인자를 사용하지 않고 호출할 경우 처음에만 정상 동작
    2. 파이썬 인터프리터는 함수의 정의에서 dictionary를 발견하면 딱 한번만 생성하기 때문에 pop하는 순간 해당 key, value는 없어짐
print(wrong_user_display())  # John (30)
print(wrong_user_display())  # KeyError: 'name'

참고 링크

수정방법은?

  • 기본 초기 값을 None으로 하고 함수 본문에서 기본 값을 할당
def wrong_user_display(user_metadata: dict = None):
    user_metadata = user_metadata or {"name": "John", "age": 30}
    name = user_metadata.pop("name")
    age = user_metadata.pop("age")

    return f"{name} ({age})"

 

내장(built-in) 타입 확장

  • 내장 타입을 확장하는 올바른 방법은 list, dict 등을 직접 상속받는 것이 아니라 collections 모듈을 상속받는 것
    • collections.UserDict
    • collections.UserList
  • 파이썬을 C로 구현한 CPython 코드가 내부에서 스스로 연관된 부분을 모두 찾아서 업데이트 해주지 않기 때문
class BadList(list):
    def __getitem__(self, index):
        value = super().__getitem__(index)
        if index % 2 == 0:
            prefix = "짝수"
        else:
            prefix = "홀수"
        return f"[{prefix}] {value}"

b1 = BadList((0, 1, 2, 3, 4, 5))
print(b1)
print(b1[0])  # [짝수] 0
print(b1[1])  # [홀수] 1
print("".join(b1)) # TypeError: sequence item 0: expected str instance, int found
from collections import UserList

class BadList(UserList):
    def __getitem__(self, index):
        value = super().__getitem__(index)
        if index % 2 == 0:
            prefix = "짝수"
        else:
            prefix = "홀수"
        return f"[{prefix}] {value}"

b1 = BadList((0, 1, 2, 3, 4, 5))
print(b1)
print(b1[0])  # [짝수] 0
print(b1[1])  # [홀수] 1
print("".join(b1))  #  [짝수] 0[홀수] 1[짝수] 2[홀수] 3[짝수] 4[홀수] 5

 

반응형

+ Recent posts