Python for AI, Embedded/Python: Core & Automation

파이썬(Python) 리스트 고급 기능 총정리: 컴프리헨션부터 메모리 최적화까지

임베디드 친구 2025. 6. 22. 14:38
반응형

이전 포스팅을 통해 파이썬 리스트의 기본적인 개념과 생성, 그리고 요소를 다루는 기초 메서드들을 살펴보았습니다. 파이썬의 리스트는 단순한 배열을 넘어 그 자체로 매우 강력하고 유연한 기능을 품고 있습니다. 데이터의 규모가 커지고 프로그램의 성능을 고민해야 하는 단계에 이르면, 이 리스트를 얼마나 '우아하고 효율적으로' 다루느냐가 시니어와 주니어 개발자를 가르는 기준이 되기도 합니다. 이번 포스팅에서는 코드 한 줄로 고성능을 내는 리스트 컴프리헨션 심화 과정부터, 대용량 데이터 처리 시 메모리를 획기적으로 아껴주는 제너레이터, 그리고 실무에서 유용하게 쓰이는 내장 모듈들까지 깊이 있게 파헤쳐 보겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  • 단순 변환이나 조건 필터링은 map이나 filter 함수보다 리스트 컴프리헨션을 쓰는 것이 대개 가독성과 성능 면에서 유리합니다.
  • 데이터 수백만 개를 다룰 때는 리스트 대신 소괄호()를 사용하는 제너레이터 표현식으로 메모리 낭비를 완벽히 차단할 수 있습니다.
  • 정렬 상태를 유지하며 데이터를 삽입할 때는 bisect 모듈을, 값의 존재 여부를 초고속으로 확인할 때는 리스트를 set으로 변환해 활용합니다.

📊 파이썬 리스트 고급 기능 및 최적화 도구 비교

본격적인 설명에 앞서, 오늘 다룰 고급 테크닉들의 핵심 작동 메커니즘과 권장 스타일을 표로 정리했습니다.

기능/모듈 명칭 주 목적 데이터 처리 방식 시간 복잡도 / 메모리 특징
리스트 컴프리헨션 직관적인 가공 및 필터링 새로운 리스트를 즉시 메모리에 생성 일반적인 루프문보다 실행 속도가 빠름
제너레이터 표현식 대용량 데이터 메모리 최적화 요소를 한 번에 하나씩 필요할 때만 생성 메모리 점유율 최소화 ($O(1)$ 공간 복잡도)
itertools 모듈 반복 구조 효율화, 무한 루프 이터레이터 프로토콜 기반 연결 및 반복 메모리를 아끼며 대량의 데이터 세트 병합
set (집합) 변환 특정 요소의 존재 유무 초고속 판별 해시 테이블(Hash Table) 기반 탐색 탐색 시간 복잡도 $O(1)$ 달성 (리스트는 $O(n)$)
bisect 모듈 순서에 맞는 정렬 상태 유지 삽입 이진 탐색(Binary Search) 알고리즘 기반 매번 다시 정렬(sort())할 필요 없어 비용 절감

1. 리스트 컴프리헨션 고도화 및 map / filter 비교

파이썬에서 기존 리스트의 데이터를 변환하거나 필터링할 때, 리스트 컴프리헨션은 가독성을 높여주는 최고의 무기입니다.

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

# 1. 모든 요소 제곱하기 (단순 가공)
squares = [x ** 2 for x in numbers]  # [1, 4, 9, 16, 25]

# 2. 짝수만 골라내어 제곱하기 (필터링 추가)
even_squares = [x ** 2 for x in numbers if x % 2 == 0]  # [4, 16]

많은 분이 파이썬의 내장 함수인 map()과 filter()의 사용법과 비교하곤 합니다. map()은 모든 요소에 함수를 일괄 적용하고, filter()는 조건에 맞는 요소만 걸러내는 역할을 합니다.

Python
 
# map()과 filter()의 활용 예시
def multiply_by_two(x): return x * 2
def is_even(x): return x % 2 == 0

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

# 내장 함수 방식 (결과를 다시 list로 변환해야 함)
map_result = list(map(multiply_by_two, numbers))  # [2, 4, 6, 8, 10]
filter_result = list(filter(is_even, numbers))    # [2, 4]

보통 조건이나 변환식이 간단할 때는 리스트 컴프리헨션을 작성하는 것이 가독성도 좋고 처리 속도도 미세하게 더 빠릅니다. 다만, 이미 잘 만들어진 복잡한 외부 함수를 재사용해야 하는 상황이라면 map이나 filter에 함수의 이름만 넘겨주는 방식이 구조적으로 깔끔할 수 있습니다.


2. 리스트 슬라이싱(Slicing) 스텝과 역순 테크닉

슬라이싱 구문에 세 번째 인자인 '스텝(Step)'을 활용하면 루프문을 돌리지 않고도 리스트의 서브셋을 스마트하게 가려낼 수 있습니다.

Python
 
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 2칸씩 건너뛰며 요소를 가져옵니다 (짝수 인덱스 추출)
every_second = numbers[::2]  # [0, 2, 4, 6, 8]

# 스텝에 -1을 주면 리스트의 요소 순서가 마법처럼 뒤집힙니다
reversed_list = numbers[::-1]  # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

3. itertools 모듈로 처리하는 고급 반복 작업

파이썬의 표준 내장 라이브러리인 itertools를 활용하면 리스트 관련 복잡한 반복 로직을 간결하게 줄일 수 있습니다.

  • itertools.cycle(): 리스트의 요소를 끝없이 무한 반복하는 이터레이터를 만듭니다. 게임의 턴제 시스템이나 UI 패턴 반복에 유용합니다.
  • itertools.chain(): 여러 개의 독립된 리스트를 메모리 낭비 없이 하나의 연속된 리스트처럼 부드럽게 이어 붙여 순회하도록 돕습니다.
Python
 
import itertools

# 리스트 체인 연결 예시
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined = list(itertools.chain(list1, list2))  # [1, 2, 3, 4, 5, 6]

4. 대용량 데이터의 구원수, 제너레이터(Generator) 표현식

만약 처리해야 할 데이터가 수백만 개, 수천만 개라면 리스트 컴프리헨션은 위험할 수 있습니다. 조건에 맞는 모든 결과물을 한꺼번에 메모리에 다 올려두고 대기하기 때문인데요. 이때 대괄호[]를 소괄호()로 바꾸어 주기만 하면 제너레이터 표현식(Generator Expression)이 되어 메모리 문제를 깔끔하게 해결해 줍니다.

Python
 
# 100만 개의 데이터를 다룰 때
numbers = range(1, 1000000)

# 제너레이터는 결과를 미리 다 만들지 않고, 준비 태세만 갖춥니다.
squares = (x ** 2 for x in numbers) 

# next()를 호출하거나 루프를 돌 때 비로소 값을 하나씩 생성합니다.
print(next(squares))  # 출력: 1 (메모리에는 오직 이 값 하나만 존재)
print(next(squares))  # 출력: 4

모든 데이터를 메모리에 한 번에 적재하지 않고 필요한 순간에만 하나씩 꺼내 쓰기 때문에 데이터 과학이나 대형 로그 파일 분석 시 큰 힘을 발휘합니다.


5. 데이터 탐색 속도 극대화와 정렬 상태 유지

  • set을 활용한 탐색 최적화: 리스트의 길이가 길어질 때 if 값 in 리스트: 구문은 처음부터 끝까지 값을 다 뒤져야 하므로 속도가 선형적으로 느려집니다($O(n)$). 이때 리스트를 set() 구조로 변환한 뒤 in 검사를 수행하면 데이터의 크기와 무관하게 순식간에($O(1)$) 탐색을 끝냅니다.
  • bisect 모듈을 통한 순서 유지 삽입: 리스트가 이미 오름차순 정렬된 상태를 유지해야 할 때, 새로운 요소를 무작정 넣고 다시 전체 정렬(sort())을 돌리는 것은 비용이 너무 큽니다. bisect.insort()를 사용하면 이진 탐색 기법을 통해 들어갈 올바른 틈새 위치를 알아서 찾아 쏙 들어가므로 매우 효율적입니다.
Python
 
import bisect

sorted_list = [1, 3, 4, 7, 9]
bisect.insort(sorted_list, 5)  # 4와 7 사이의 정확한 위치에 5 삽입
print(sorted_list)  # [1, 3, 4, 5, 7, 9]

6. 개발을 위한 팁

  • 가독성이 깨진다면 일반 for문으로 과감히 회귀하세요: 리스트 컴프리헨션은 강력하지만 중첩해서 사용하거나 복잡한 if-else 조건문이 가미되면 오히려 다른 개발자가 해독하기 힘든 암호문이 되어 버립니다. "한 줄 코딩"의 유혹에 빠져 가독성을 해치지 않도록, 조건이 복잡해지면 과감히 일반 for문이나 별도의 헬퍼 함수로 분리해 가독성을 챙겨야 합니다.
  • 불변성이 보장될 땐 리스트 대신 튜플로 슬라이싱 리턴: 서브셋 데이터를 가져온 뒤 추가적인 가공(삽입, 삭제)이 필요 없고 단순 조회만 한다면 가벼운 튜플 형태로 데이터를 전달하는 것이 보수적인 메모리 관리 관점에서 더 안정적인 팁이 될 수 있습니다.

7. 흔히 하는 실수

  • 제너레이터의 '일회성' 특성 간과: 제너레이터 표현식으로 만든 변수는 데이터를 한 번 끝까지 순회하고 나면(소진되면) 속이 텅 비어버립니다. 재사용하려고 다시 루프를 돌려도 아무런 값도 나오지 않으므로, 데이터를 반복해서 여러 번 사용해야 하는 로직이라면 제너레이터가 아닌 리스트 구조를 채택해야 안전합니다.
  • 리스트 원본과 참조값 복사의 가짜 복사 실수: 리스트의 복사본을 만든답시고 new_list = old_list라고 대입하면, 실제 데이터가 복사되는 것이 아니라 같은 데이터 주소를 가리키는 '참조 값'만 복사됩니다. 이 상태에서 new_list를 수정하면 old_list까지 덩달아 변하는 참사가 벌어집니다. 온전한 별개의 복사본을 만들고 싶다면 슬라이싱 기법을 이용해 new_list = old_list[:]로 복사하거나 copy 모듈의 deepcopy()를 써야 독립된 방이 완성됩니다.

💡 맺음말

이번 포스팅에서는 파이썬 코딩의 질을 높여줄 리스트의 고급 응용 기술과 최적화 테크닉들을 다각도로 살펴보았습니다. 데이터 구조를 어떻게 다루느냐에 따라 내 코드가 몇 배나 빨라질 수도 있고, 서버의 메모리를 비약적으로 아낄 수도 있다는 점이 흥미롭지 않으신가요? 상황에 맞는 가장 알맞은 내장 툴을 선택해 내 코드에 적용하는 연습을 반복해 보세요.

오늘 다룬 제너레이터나 컴프리헨션 예제를 직접 실습해 보시다가 결과가 이상하게 나오거나 막히는 부분이 생기면 언제든 아래 댓글로 편하게 질문을 던져주세요. 함께 해결해 드리겠습니다. 감사합니다!

반응형