본문 바로가기
프로그래밍/Python

[Python] 시퀀스 (Sequences) - (2)

by 별준 2022. 3. 12.

References

  • Fluent Python

Contents

  • 시퀀스에서의 +, *, +=, *= 연산자
  • list.sort() and sorted()
  • bisect 모듈
  • 리스트 타입이 정답이 아닌 경우 : array.array
  • Memory Views (memoryview)
  • Deques and Other Queues

[Python] 시퀀스 (Sequences) - (1)

이전 포스팅에 이어서 시퀀스에 대해 알아보도록 하겠습니다.

 


Using + and * with Sequences

파이썬에서는 시퀀스가 당연히 +와 * 연산자를 지원한다고 알고 있습니다. 일반적으로 덧셈의 경우 피연산자 두 개가 같은 타입이어야 하며, 둘 다 변경되지 않으면서 동일한 타입의 시퀀스로 새로 생성됩니다.

 

하나의 시퀀스를 여러 번 연결하려면 정수를 곱해서 표현합니다. 이때도 새로운 시퀀스가 생성됩니다.

덧셈/곱셈 연산자는 언제나 새로운 객체를 생성하고, 피연산자를 변경하지 않습니다.

a가 가변 항목을 담고 있을 때, a * n과 같은 표현식을 사용하려면 주의해야 합니다. 원치 않은 결과가 나올 수 있는데, 예를 들어, 리스트의 리스트를 초기화할 때 my_list = [[]] * 3으로 초기화하면 동일한 내부 리스트에 대한 참조 3개를 가진 리스트가 만들어지므로, 원치 않는 결과가 나올 수 있습니다.

 

Building Lists of Lists

원소로 리스트를 가진 리스트를 초기화하는 경우가 종종 있습니다. 예를 들어, 학생들을 팀별로 묶어서 리스트를 만들거나 게임 보드판에 정사각형을 표현하는 경우가 있습니다. 이런 리스트를 초기화할 때는 리스트 컴프리헨션(list comprehension)을 사용하는 것이 가장 좋습니다.

 

아래처럼 작성할 수도 있는데, 아래 코드는 잘못된 코드입니다.

위의 코드는 본질적으로 다음과 같이 동작합니다.

row = ['_'] * 3
board = []
for i in range(3):
    board.append(row)

즉, 동일한 row가 board 리스트에 3번 추가되는 것과 같습니다.

 

반면, 처음에 살펴봤던 정상적인 코드는 다음과 같이 동작합니다.

board = []
for i in range(3):
    row = ['_'] * 3
    board.append(row)

 


Augmented Assignment with Sequences

augmented argument를 보통 '증가 할당'으로 번역하는 경우가 많습니다. 하지만 엄밀히 따지면 'augment'의 의미는 '증가'가 아닌 '보완'을 의미하고, augmented assignment는 '연산과 할당의 결합'을 의미합니다. Fluent Python에서는 이 연산자의 올바른 의미를 한글로 전달하기 위해 '증가 할당'이 아닌 '복합 할당'으로 변역하고 있습니다.

+=와 *= 등의 복합 할당 연산자는 첫 번째 피연산자에 따라 상당히 다르게 동작합니다.

 

+= 연산자가 동작하도록 만드는 스페셜 메소드는 __iadd__() 입니다. 그런데 __iadd__() 메소드가 구현되어 있지 않으면, 파이썬은 __add__()를 호출합니다. 다음 코드를 살펴보겠습니다.

a += b

a에 __iadd__() 메소드가 구현되어 있으면, 해당 메소드가 호출됩니다. a가 list, bytearray, array.array 등 가변 시퀀스인 경우 a의 값이 변경됩니다(이 과정은 a.extend(b)와 비슷합니다.). 그런데 a에 __iadd__() 메소드가 구현되지 않은 경우, a += b 표현식은 a = a + b가 되어 먼저 a + b를 평가하고, 객체를 새로 생성한 후 a에 할당됩니다. 즉, __iadd__() 메소드 구현 여부에 따라 a 변수가 가리키는 객체의 identity가 바뀔 수도 있고 바뀌지 않을 수도 있습니다.

 

일반적으로 가변 시퀀스에 대해서는 __iadd__() 메소드를 구현해서 += 연산자가 기존 객체의 내용을 변경하게 만드는 것이 좋습니다. 불변 시퀀스의 경우에는 이 연산을 수행할 수 없습니다.

 

+= 연산자에 대한 내용은 *= 연산자에도 동일하게 적용되는데, *= 연산자의 경우에는 __imul__() 메소드를 통해 구현됩니다.

*= 연산자를 가변 시퀀스와 불변 시퀀스에 적용한 예제 코드는 다음과 같습니다.

위에서 보다시피, 곱셈 연산을 수행한 후 새로운 항목이 추가된 리스트 객체는 기존 객체와 같다는 것을 볼 수 있고, 곱셈 연산을 수행한 후의 튜플은 기존 튜플이 아닌 새로 생성된 튜플이라는 것을 확인할 수 있습니다.

 

새로운 항목을 추가하는 대신 항목이 추가도니 시퀀스 전체를 새로 만들어 타겟 변수에 저장하므로, 불변 시퀀스에 반복적으로 연결 연산을 수행하는 것은 비효율적입니다. (단, str 객체의 동작 방식은 다릅니다. += 연산자를 이용해서 문자열을 만드는 작업언 CPython에서 최적화되어 있습니다. str 객체는 메모리 안에 여분의 공간을 가지고 할당되므로 str 객체를 연결할 때 매번 전체 문자열을 다시 생성하지 않습니다.)

 

A += Assignment Puzzler

t = (1, 2, [30, 40])
t[2] += [50, 60]

다음 중 위의 두 표현식을 평가한 결과는 무엇일까요?

  1. t는 (1, 2, [30, 40, 50, 60])이 된다.
  2. TypeError가 발생한다.
  3. 1, 2 둘 다 틀리다.
  4. 1, 2 둘 다 맞다.

아마 2번이라고 생각할 수 있지만, 실제 정답은 4번입니다. 즉, 1번과 2번이 모두 맞습니다.

Python 3.8에서 위 코드를 실행한 결과는 다음과 같습니다.

 

이는 s[a] += b 표현식에 대해 파이썬이 생성한 바이트코드를 보면 어떻게 이런 현상이 발생하는지 명확히 알 수 있습니다.

위에서 출력된 내용을 살펴보면 BINARY_SUBSCR에서 TOS(top of stack)에 s[a]의 값을 위치시킵니다. 그리고 INPLACE_ADD에서 TOS += b 연산을 수행합니다. 여기서 TOS가 가변 객체를 가리키면 이 연산은 성공합니다. STORE_SUBSCR에서는 TOS를 s[a]에 할당합니다. 여기서 s가 불변 객체라면 이 연산은 실패하게 됩니다.

 

이러한 예제는 상당히 드문 상황이긴 합니다. 다만 이러한 코드를 통해 다음과 같은 내용을 알 수 있습니다.

  • 튜플에 가변 시퀀스를 넣는 것은 좋지 않다.
  • 복합 할당은 atomic 연산이 아니다.

 


list.sort() and sorted() Build-In Functions

list.sort() 메소드는 사본을 만들지 않고 리스트 내부를 변경해서 정렬합니다. sort() 메소드는 타겟 객체를 변경하고 새로운 리스트를 생성하지 않았음을 알려주기 위해서 None을 리턴하며, 이는 파이썬 API의 관례입니다. random.suffle() 함수도 이와 동일하게 동작합니다.

 

이와 반대로 sorted() 내장 함수는 새로운 리스트를 생성해서 반환합니다. 사실, sorted() 함수는 불변 시퀀스 및 제너레이터를 포함하여 iterable한 모든 객체를 인수로 받을 수 있습니다. 입력받은 반복 가능한 객체의 타입과 무관하게 sorted() 함수는 언제나 새로 생성한 리스트를 반환합니다.

 

list.sort() 메소드와 sroted() 함수는 모두 옵셔너라로 다음 2개의 키워드를 인수로 받습니다.

  • reverse : 이 키워드가 True라면 비교 연산을 반대로 수행해서 내림차순으로 리턴합니다. 기본값은 False 입니다.
  • key : 정렬에 사용할 키를 생성하기 위해 각 항목에 적용할 함수를 인수로 받습니다. 예를 들어, 문자열의 리스트를 정렬할 때, key=str.lower로 지정하면 대소문자를 구분하지 않고 정렬하며, key=len으로 지정하면 문자열의 길이에 따라 문자열을 정렬합니다. 키를 지정하지 않으면 항목 자체를 비교합니다.

 

아래 코드는 이를 어떻게 사용하는지 보여줍니다.

 

일단 시퀀스를 정렬한 후에는 아주 효율적으로 검색할 수 있습니다. 다행히 파이썬 표준 라이브러리의 bisect 모듈에서 이미 표준 binary search 알고리즘을 제공합니다. 아래에서 정렬된 시퀀스의 정렬 상태를 유지한 채로 항목을 추가하는 bisect.insort() 함수 등에 대해서 알아보도록 하겠습니다.

 


Managing Ordered Sequences with bisect

bisect 모듈은 bisect()와 insort() 함수를 제공합니다. bisect()는 binary search 알고리즘을 이용해서 시퀀스를 검색하고, insort()는 정렬도니 시퀀스 안에 항목을 삽입합니다.

 

Searching with bisect()

bisect(haystack, needle)은 정렬된 시퀀스인 haystack 안에서 오름차순 정렬 상태를 유지한 채로 needle을 추가할 수 있는 위치를 찾아냅니다. 즉, 해당 위치 앞에는 needle보다 같거나 작은 항목이 옵니다. bisect(haystack, needle)의 결과값을 인덱스로 사용해서 haystack.insert(index, needle)을 호출하면 needle을 추가할 수 있지만, insort() 함수는 이 두 과정을 더 빨리 처리합니다.

 

아래 예제 코드는 needle을 사용해서 bisect가 반환한 위치를 보여줍니다. 

import bisect
import sys

HAYSTACK = [1, 4, 5, 6, 8,12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]

ROW_FMT = '{0:2d} @ {1:2d}    {2}{0:<2d}'

def demo(bisect_fn):
    for needle in reversed(NEEDLES):
        position = bisect_fn(HAYSTACK, needle)
        offset = position * '  |'
        print(ROW_FMT.format(needle, position, offset))

if __name__ == '__main__':
    if sys.argv[-1] == 'left':
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect_right
    
    print('DEMO:', bisect_fn.__name__)
    print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
    demo(bisect_fn)

위 그림은 코드를 실행한 결과이며, needle을 삽입할 위치를 보여줍니다.

 

bisect의 동작은 두 가지 방법으로 조절할 수 있습니다.

첫째, 선택적 인수인 lo와 hi를 사용하면 삽입할 때 검색할 시퀀스의 영역을 좁힐 수 있습니다. lo의 기본값은 0, hi의 기본값은 시퀀스의 len() 입니다.

둘째, bisect는 실제로는 bisect_right() 함수의 앨리어스이며, 이 함수의 형제 함수로 bisect_left()가 있습니다. 이 두 함수는 단지 리스트 안의 항목이 needle과 값이 같을 때만 차이가 납니다. bisect_right()는 기존 항목 바로 뒤를 삽입 위치로 반환하며, bisect_left()는 기존 항목 위치를 삽입 위치로 반환하므로 기존 항목 바로 앞에 삽입됩니다. int와 같은 단순한 타입의 경우에는 차이가 없지만, 객체를 담고 있는 시퀀스 안에서는 동일하다고 판단되지만 서로 다른 객체가 들어 있으므로 이 두 함수 간에 약간의 차이가 있습니다. 예를 들어, 1과 1.0은 다르지만 1 == 1.0은 True입니다.

 

bitsect_left() 함수를 적용한 결과는 다음과 같습니다.

 

bisect를 사용하면 수치형 값으로 테이블을 참조할 수 있으므로 아래 코드처럼 시험 점수를 등급으로 변환할 수 있습니다.

위 코드는 bisect — Array bisection algorithm — Python 3.10.2 documentation 에 있으며, 이 문서에는 정렬된 긴 숫자 시퀀스를 검색할 때 index() 대신 더 빠른 bisect() 함수를 사용하는 것을 보여줍니다. 

 

 

테이블을 참조하도록 사용할 때, bisect_left는 조금 다른 결과를 생성합니다.

 

Inserting with bisect.insort()

정렬은 cost가 큰 연산이므로 시퀀스를 일단 정렬한 후에는 정렬 상태를 유지하는 것이 좋습니다. 이러한 이유로 bisect.insort() 함수가 만들어졌씁니다.

 

insort(seq, item)은 seq를 오름차순으로 유지한 채로 item을 seq에 삽입합니다. 아래 코드를 살펴보겠습니다.

import bisect
import random

SIZE = 7

random.seed(1729)

my_list = []
for i in range(SIZE):
    new_item = random.randrange(SIZE*2)
    bisect.insort(my_list, new_item)
    print('%2d ->' % new_item, my_list)

위 코드를 실행한 결과는 다음과 같습니다.

bisect 함수와 마찬가지로 insort 함수도 선택적으로 lo와 hi 인수를 받아 시퀀스 안에서 검색할 범위를 제한합니다. 그리고 삽입 위치를 검색하기 위해 bisect_left() 함수를 사용하는 insort_left() 함수도 있습니다.

 

이때까지 설명한 내용들은 리스트나 튜플뿐만 아니라 시퀀스에도 적용됩니다. 리스트가 너무 사용하기 편리하기 때문에 리스트를 남용하기도 합니다. 하지만 만약 숫자드라로 구성된 리스트를 다루고 있다면 배열을 사용하는 것이 더 좋습니다.

 


When a List Is Not the Answer

리스트 타입은 flexible하고 사용하기 편하지만, 특정 요구사항에 따라서 더 좋은 옵션들이 있습니다. 예를 들어, floating-point 값을 천만 개 저장해야 할 때는 배열(array)이 훨씬 더 효율적입니다. 배열은 모든 기능을 갖춘 float 객체 대신 C 언어의 배열과 마찬가지로 기계가 사용하는 형태로 표현된 바이트 값만 저장하기 때문입니다.

다른 예로 리스트의 양쪽 끝에 항목을 계속 추가하거나 삭제하면서 FIFO나 LIFO 데이터 구조를 구현할 때는 deque가 더 빠릅니다.

 

Arrays

방금 언급했듯이 리스트 안에 숫자만 들어있다면 배열(array.array)이 리스트보다 훨씬 더 효율적입니다. 배열은 pop(), insert(), extend() 등을 포함해서 가변 시퀀스가 제공하는 모든 연산을 지원하며, 빠르게 파일에 저장하고 읽어올 수 있는 frombytes()와 tofile() 메소드도 추가로 제공합니다.

 

파이썬의 배열은 C언어의 배열만큼 가볍습니다. float 값을 저장하는 배열은 float 인스턴스를 가지고 있는 것이 아닌 기계값을 표현하는 패킹된 바이트만 가지고 있습니다. 이는 C 언어에서의 double 타입의 배열과 유사합니다. 배열을 생성할 때는 배열에 저장되는 각 항목의 C 기반 타입을 결정하는 문자, 타입코드(typecode)를 지정합니다. 예를 들어, 'b'는 signed char에 대한 타입코드입니다. array('b')를 생성하면, 각 항목은 하나의 바이트로 저장되고, 이는 -127에서 128까지의 정수로 해석됩니다. 숫자가 아주 많이 들어 있는 시퀀스의 경우, 배열에 저장하면 메모리가 많이 절약됩니다. 그리고 파이썬은 배열형에 맞지 않는 숫자는 저장할 수 없도록 합니다.

 

아래 예제 코드는 천만 개의 무작위 실수를 가진 배열을 생성하고, 이 값을 읽는 방법을 보여줍니다.

from array import array
from random import random

# Create an array of double-precision floats(typecode 'd')
floats = array('d', (random() for i in range(10**7)))
# Inspect the last number in the array
print(floats[-1])

# Save the array to a binary file
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()

# Read 10 million numbers from binary file
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()

# Inspect the last number in the array
print(floats2[-1])
print(floats2 == floats) # Verify that the contents match

실행 결과는 다음과 같습니다.

위 코드에서 알 수 있는 것처럼 array.tofile()과 array.fromfile() 메소드는 매우 사용하기 쉽습니다. 또한, 예제 코드를 실행해보면 아주 빠르다는 것을 알 수 있습니다. 간단히 실행해보면 array.fromfile() 메소드가 array.tofile() 메소드로 생성한 바이너리 파일에서 double-presision 실수 천만개를 로드하는 데 0.1초 정도의 시간이 걸립니다. 이 속도는 float() 내장 함수를 이용해서 텍스트 파일에서 파싱하는 것보다 약 60배정도 빠릅니다. array.tofile() 메소드로 저장하는 것은 각 행마다 실수 하나씩 텍스트 파일에 저장하는 것보다 약 7배 빠릅니다. 게다가 실수 천만 개를 저장한 바이너리 파일의 크기는 80,000,000,000바이트(실수 하나에 8바이트씩이며, 오버헤드가 전혀 없습니다.)인 반면, 동일한 데이터를 저장한 텍스트 파일의 크기는 181,515,739바이트라고 합니다.

 

아래 표는 리스트와 배열의 기능을 비교합니다.

Methods List Array  
s.__add__(s2) s + s2 - concatenation
s.__iadd__(s2) s += s2 - in-place concatenation
s.append(e) 마지막 요소 뒤에 e 추가
s.byteswap()   엔디안 변환을 위해 배열 안의 모든 요소의 바이트 순서를 바꿈
s.clear()   모든 항목을 삭제
s.__contains__(e) e in s
s.copy()   얕은 복사 수행
s.__copy__()   copy.copy 메소드를 지원
s.count(e) s 안에 e 요소가 나타나는 횟수 반환
s.__deepcopy__()   copy.deepcoy를 최적화하여 지원
s.__delitem__(p) p 위치의 요소를 삭제
s.extend(it) 반복가능한 it으로부터 항목들을 추가
s.frombytes(b)   패킹된 기계값으로 해석된 바이트 시퀀스로부터 항목들을 추가
s.fromfile(f, n)   패킹된 기계값으로 해석된 바이너리 파일 f로부터 항목들을 가져와서 추가
s.fromlist(l)   리스트로부터 항목들을 추가. TypeError가 한 번이라도 발생하면 아무 것도 추가하지 않음
s.__getitem__(p) s[p]
s.index(e) e가 처음 나타나는 위치를 찾아낸다
s.insert(p, e) p 위치에 있는 항목 앞에 e 요소를 추가
s.itemsize   각 배열 항목의 바이트 단위 크기
s.__iter__() iterator를 가져온다
s.__len__() len(s)
s.__mul__(n) s * n - n회 반복하여 연결
s.__imul__(n) s *= n - n회 반복하여 연결한 후 s에 저장
s.__rmul__(n) n * s - 역순 반복 연결 메소드
s.pop([p]) p 위치 혹은 제일 마지막 항목을 제거하고 반환
s.remove(e) e와 일치하는 항목을 제거
s.reverse() 항목들의 순서를 역으로 나열하여 다시 s에 저장
s.__reversed__()   마지막부터 처음까지 반복하는 iterator를 가져온다
s.__setitem__(p, e) s[p] = e
s.sort([key], [reverse])   (optional)key와 reverse에 따라 항목을 정렬하고 s에 저장
s.tobytes()   bytes 객체에 패킹된 기계값으로 항목을 반환
s.tofile(f)   바이너리 파일 f에 패킹된 기계값으로 항목을 저장
s.tolist()   각 항목을 수치형 객체로 변환한 리스트를 반환
s.typecode   항목의 C 타입을 나타내는 한 글자의 문자열

 


Memory Views

메모리 뷰(memoryview) 클래스는 공유 메모리 시퀀스 타입으로 bytes를 복사하지 않고 배열의 슬라이스를 다룰 수 있도록 해줍니다. 이 클래스는 Numpy 라이브러리에서 영감을 받아 만들어졌다고 합니다.

Numpy의 개발자인 Travis Oliphant는 '언제 메모리 뷰를 사용해야 하는가?'라는 질문에 다음과 같이 답변했습니다.

 

- 메모리 뷰는 본질적으로 파이썬 자체에 들어 있는 numpy 배열 구조체를 일반화한 것이다. 메모리 뷰는 PIL 이미지, SQLite 데이터베이스, Numpy 배열 등 데이터 구조체를 복사하지 않고 메모리를 공유할 수 있도록 해주며, 이는 데이터셋이 커지는 경우에 아주 중요한 기법이다.

 

array 모듈과 비슷한 표기법을 사용하는 memoryview.cast() 메소드는 바이트를 이동시키지 않고 C 언어의 형변환 연산자처럼 여러 바이트로 된 데이터를 읽거나 쓰는 방식을 바꿀 수 있게 해줍니다. memoryview.cast()는 또 다른 memoryview 객체를 반환하며 언제나 동일한 메모리를 공유합니다.

 

다음 예제 코드는 6바이트의 배열을 2x3 matrix 또는 3x2 matrix로 변환하여 볼 수 있도록 해주는 방법을 보여줍니다.

memoryview의 인덱싱에서 튜플을 사용하는 것(m2[1, 1])은 Python 3.5에서 추가되었습니다.

 

다음 예제 코드는 16비트 정수 배열에서 바이트 하나를 변경하는 방법을 보여줍니다.

위 코드에서 16비트 부호있는 정수(typecode='h') 배열로부터 memoryview를 생성합니다. 처음 memv는 배열과 동일한 5개의 항목을 보여줍니다. 그런 다음 바이트(bytecode='B')로 memv의 요소들을 캐스팅하여 memv_oct를 생성합니다. memv_oct 요소들은 10바이트의 리스트로 보여줍니다. 그 다음 5번 인덱스 항목에 4를 할당하는데, 2바이트의 unsigned int의 최상위 바이트에서 4는 1024에 해당합니다.(00000100 00000000 -> 1024)

 


Deques and Other Queues

append()와 pop() 메소드를 사용해서 리스트를 스택이나 큐로 사용할 수 있습니다. 하지만 리스트 왼쪽(0-index)에 삽입하거나 삭제하는 연산은 전체 리스트를 이동시켜야 하므로 cost가 큽니다.

 

덱(collections.deque) 클래스는 큐의 양쪽 어디에서든 빠르게 삽입 및 삭제할 수 있도록 설계된 thread-safe한 양방향 큐입니다. 덱은 최대 길이를 설정해서 제한된 항목만 유지할 수 있도록 할 수 있는데, 덱이 꽉 찬 후에는 새로운 항목을 추가할 때 반대쪽 항목을 버립니다. 아래 코드는 덱을 이용해서 수행하는 일반적인 연산들을 보여줍니다.

 

deque가 제공하는 메소드는 다음 링크를 참조하시길 바랍니다 !

collections — Container datatypes — Python 3.10.2 documentation

 

collections — Container datatypes — Python 3.10.2 documentation

collections — Container datatypes Source code: Lib/collections/__init__.py This module implements specialized container datatypes providing alternatives to Python’s general purpose built-in containers, dict, list, set, and tuple. namedtuple() factory f

docs.python.org

 

덱은 리스트 메소드 대부분을 구현할 뿐만 아니라 popleft()와 rotate()처럼 고유한 메소드를 추가로 가지고 있습니다. 하지만 숨겨진 단점도 있습니다. 덱의 중간 항목을 삭제하는 연산은 그리 빠르지 않습니다. 이는 덱이 양쪽 끝에 추가나 제거하는 연산에 최적화되어 있기 때문입니다.

 

append()와 popleft() 메소드는 atomic한 성질을 가지고 있으므로 멀티스레드 앱에서 락을 사용하지 않고도 덱을 이요해서 간단히 FIFO 큐를 구현할 수 있습니다.

 

deque 외에도 표준 라이브러리 패키지는 다음과 같은 큐를 구현하고 있습니다.

  • queue
    queue 모듈에서는 동기화된(thread-safe) Queue, LifoQueue, PriorityQueue 클래스를 제공합니다. 이 클래스들은 스레드 간에 안전하게 통신하기 위해 사용됩니다. 세 클래스 모두 0보다 큰 maxsize 인수를 생성자에 전달해서 바인딩할 수 있습니다. 하지만 덱과 달리 공간이 꽉 찼을 때 항목을 버리지 않습니다. 대신 새로운 항목의 추가를 블로킹하고 다른 스레드에서 큐 안의 항목을 제거해서 공간을 확보해줄 때까지 기다립니다.
  • multiprocessing :
    multiprocessing 모듈은 queue.Queue와 비슷하지만 프로세스 간 통신을 지원하기 위해 설계도니 고유한 Queue 클래스를 구현합니다. 태스크 관리에 특화된 multiprocessing.JoinableQueue 클래스도 제공합니다.
  • asyncio :
    파이썬 3.4부터 추가된 asyncio 모듈은 queue 및 multiprocessing 모듈에 포함된 클래스로부터 영감을 얻은 Queue, LifoQueue, PriorityQueue, JoinableQueue 클래스를 제공하지만, 비동기 프로그래밍 환경에서 작업을 관리하는 데 주안점을 두고 있습니다.
  • heapq :
    앞서 나온 모듈과는 대조적으로 heapq는 queue 클래스를 구현하지는 않지만, 가변 시퀀스를 힙 큐나 우선순위 큐로 사용할 수 있게 해주는 heappush()와 heappop() 등의 함수를 제공합니다.

 


지난 포스팅과 이번 포스팅을 통해서 리스트 이외의 타입 및 시퀀스 타입에 대해 간략하게 살펴봤습니다. str과 바이너리 시퀀스에 대해서는 다음에 다루어보도록 하겠습니다 !

댓글