Posts Golang에서 python generator 구현하기
Post
Cancel

Golang에서 python generator 구현하기

개요

  • Golang으로 작업 중 엄청 큰 텍스트 파일을 읽을 일이 생겼는데
    python에서 generator로 읽어왔던 기억이 나서 비슷하게 구현하여 기록한다.

python에서 파일 읽기

  • 기본적으로 python에서 파일을 읽을 때는 fileObject.read()함수를 쓸 수 있다.
  • fileObject.read() 함수는
    fileObject가 바이너리일 경우 현재 위치에서 몇 바이트까지 읽을지를 인자로 받는다.
    fileObject가 텍스트일 경우 현재 위치에서 몇개의 문자열까지 읽을지를 인자로 받는다.
    인자를 입력하지 않을 경우, 전체 파일을 읽는다(이때 너무 파일이 크면 메모리 부족 에러가 발생한다).
  • 예시 텍스트 파일
    1
    2
    3
    
      # test.txt
      first_line
      second_line
    
  • 한 글자씩 불러오기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      with open("test.txt") as f:
          i = 1
          while True:
              # 1 문자열 읽기(바이너리 파일일 경우 1byte 읽기)
              chunk = f.read(1)
              print(chunk)
    
              # chunk가 빈 문자열이면 종료
              if chunk == "":
                  break
    
              # 파일 시작 부분에서 i 문자열 뒤로 이동
              f.seek(i)
              i += 1
    
  • 한 줄씩 불러오기
    • 보통 텍스트 파일 로드 시 개행(newline) 단위로 구분하여 불러온다.
    • python에서는 fileObject를 for loop로 돌리면 개행 별로 구분하여 한 줄 씩 리턴한다.
      1
      2
      3
      
        with open("test.txt") as f:
            for row in f:
                print(row)
      

python에서 generator

  • iterator와 같이 행동하는 함수(for loop에서 하나씩 꺼내기 가능)를 generator 함수라고 한다.
  • 보통 한 번에 메모리에 올리기에는 너무 큰 파일을 불러오거나,
    로깅과 같은 미들웨어를 구현할 때 많이 사용한다.
  • return 대신 yield를 쓰면 알아서 매 iteration 마다 element를 하나씩 리턴한다.
  • 1
    2
    3
    4
    5
    6
    7
    
      def read_file_gen():
          with open("test.txt") as f:
              for row in f:
                  yield row
        
      for row in read_file_gen():
          print(row)
    

golang에서 파일 읽기

  • golang에서 주로 쓰는 파일 읽기 함수는 bufio.NewSwcanner()이다.
  • 파일을 읽어온 후 행 구분 방법을 선택하고, 한 줄 씩 스캔한다.
    (예시에서는 개행문자(\n)로 행을 구분한다(bufio.ScanLines))
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      file, err := os.Open(filePath)
      if err != nil {
          fmt.Println("error: ", err.Error())
          return
      }
    
      # 여기서는 개행 별로 구분
      scanner := bufio.NewScanner(file)
      scanner.Split(bufio.ScanLines)
      for scanner.Scan() {
          fmt.Println(scanner.Text())
      }
    

golang에서 generator와 비슷하게 구현하기

  • goroutine과 channel을 이용하여 python generator와 비슷하게 구현한다.
  • generator 역할을 하는 함수 내부에서 익명함수를 goroutine으로 실행하고 실행결과를 channel에 저장한다.
  • generator 역할을 하는 함수의 return 값은 channel이며,
    main 함수에서 generator 함수를 for … range로 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      // ExptGen : "1","2","3","4","5"를 리턴하는 함수
      func ExptGen() (c chan string) {
          c = make(chan string, 1)
          go func() {
              defer close(c)
              testInts := []int{1, 2, 3, 4, 5}
              for i, val := range testInts {
                  c <- strconv.Itoa(val)
              }
          }()
          return c
      }
    
      for value := range ExptGen() {
          fmt.Println("value", value)
      }
    
  • 여러 goroutine을 돌리면서 결과를 하나의 channel로 리턴하고 싶을 때에는 sync.WaitGroup을 사용한다.
  • 예시로 두 goroutine을 사용하여 하나의 channel에 결과를 저장하는 generator 함수를 구현한다.
  • 이 구현에서는 두 goroutine이 동기적으로 실행되지 않기 때문에 순서가 보장되지 않는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
      // ExptGen : 두 goutine을 사용한 generator 예시
      func ExptGen() (c chan string) {
          c = make(chan string, 1)
    
          var wg sync.WaitGroup
          wg.Add(1)
          go func() {
              defer wg.Done()
              testInts := []int{1, 2, 3, 4, 5}
              for i, val := range testInts {
                  c <- strconv.Itoa(val)
              }
          }()
          wg.Add(1)
          go func() {
              defer wg.Done()
              testInts := []int{6, 7, 8, 9, 10}
              for i, val := range testInts {
                  c <- strconv.Itoa(val)
              }
          }()
    
          go func() {
              wg.Wait()
              close(c)
          }()
    
          return c
      }
    
      for value := range ExptGen() {
          fmt.Println("value", value)
      }
    

golang에서 generator로 파일 읽기

  • golang에서 for … range 방식 구문을 쓸때
    range 뒤에 channel을 받을 경우, 오직 channel만 단독으로 받아야한다.
  • generator 함수에서 channel, error를 같이 리턴했을 때 for … range에서 error를 받을 수 없다.
  • 때문에 channel와 error를 구조체로 묶어 이를 받는 channel을 만든 뒤 리턴하여 처리한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
      type FileLine struct {
          Line string
          Err  error
      }
    
      func ReadLines(filePath string) (fileLineChan chan FileLine) {
          fileLineChan = make(chan FileLine, 1)
          go func() {
              var fileLine FileLine
              defer close(fileLineChan)
    
              file, err := os.Open(filePath)
              if err != nil {
                  fileLine.Err = err
                  fileLineChan <- fileLine
                  return
              }
    
              scanner := bufio.NewScanner(file)
              scanner.Split(bufio.ScanLines)
              for scanner.Scan() {
                  fileLine.Line = scanner.Text()
                  fileLineChan <- fileLine
              }
    
          }()
          return fileLineChan
      }
    
      var i = 0
      for fileLine := range ReadLines("test.txt") {
          if fileLine.Err != nil {
              fmt.Println(fileLine.Err.Error())
              break
          }
          fmt.Println("fileLine.Line", fileLine.Line)
          fmt.Println("fileLine.Err", fileLine.Err)
      }
    

참고

This post is licensed under CC BY 4.0 by the author.