리스트 평탄화(flattening)란 리스트에서 차원을 하나 제거하는 과정을 말합니다. 여기서 차원이란 리스트 안의 특정 요소에 접근하기 위해 필요한 추가 좌표를 의미합니다. Python에서는 리스트 컴프리헨션(list comprehension), 중첩 for 루프, itertools.chain() 메서드 세 가지 방법으로 리스트를 평탄화할 수 있습니다.
Python 리스트는 여러 개의 차원을 가질 수 있습니다. 즉, 리스트 안에 또 다른 리스트가 들어 있는 구조입니다. 이러한 리스트를 흔히 '중첩 리스트(nested list)'라고 부르며, 이를 다시 일반적인 단일 리스트로 변환할 수 있습니다.
즉, 리스트 안의 리스트에 있는 모든 값을 하나의 리스트로 옮기는 것이며, 이 과정을 '리스트 평탄화'라고 합니다.
Python 리스트 평탄화의 3가지 방법
Python에서 리스트를 평탄화하는 방법은 크게 세 가지입니다.
- 리스트 컴프리헨션(list comprehension) 사용
- 중첩 for 루프(nested for loop) 사용
- itertools.chain() 메서드 사용
이 글에서는 위 세 가지 방법을 각각 살펴보고, 실제 예제 코드와 함께 어떻게 동작하는지 자세히 알아보겠습니다.
방법 1: 리스트 컴프리헨션으로 평탄화하기
Python의 리스트 컴프리헨션은 기존 리스트의 내용을 기반으로 새로운 리스트를 정의하는 문법입니다. 조건이나 변환 규칙을 지정하여 새 리스트에 들어갈 각 요소를 원하는 대로 가공할 수 있습니다.
컴프리헨션은 본질적으로 for 루프를 사용해 리스트를 순회하고 새 리스트를 생성하는 과정을 축약한 '문법적 설탕(syntactic sugar)'입니다. 따라서 for 루프와 근본적으로 같은 방식으로 작동하지만 더 간결한 문법을 사용합니다.
다음은 리스트 컴프리헨션의 기본 문법입니다.
numbers = [1, 2, 3] new_numbers = [number * 2 for number in numbers]
위 코드는 'numbers' 리스트의 각 숫자에 2를 곱한 새로운 리스트를 생성합니다.
예제: 리스트 컴프리헨션 활용
레스토랑에서 판매하는 샌드위치 속 재료를 담은 리스트가 있다고 가정해 보겠습니다. 현재 리스트 구조는 다음과 같습니다.
foods = [ ["Tomato and Cucumber", "Hummus, Beetroot, and Lettuce"], ["Cheese", "Egg"], ["Ham", "Bacon", "Chicken Club", "Tuna"] ]
첫 번째 리스트는 비건(vegan) 샌드위치 재료, 두 번째 리스트는 베지테리언(vegetarian) 샌드위치 재료, 세 번째 리스트는 고기가 포함된 샌드위치 재료를 담고 있습니다.
우리는 이 리스트를 하나의 단일 리스트로 변환하고 싶습니다. 일반적으로 1차원 리스트가 2차원 리스트보다 다루기 훨씬 쉽기 때문입니다. 이 경우 굳이 두 개의 차원을 유지할 필요가 없습니다. 리스트 컴프리헨션을 사용하면 두 번째 차원을 손쉽게 제거할 수 있습니다.
new_foods = [food for sublist in foods for food in sublist] print(new_foods)
이 리스트 컴프리헨션은 'foods' 변수의 모든 하위 리스트를 순회하면서, 각 값을 하나의 메인 리스트에 추가합니다. 리스트가 생성되면 콘솔에 출력됩니다.
코드를 실행해 결과를 확인해 보겠습니다.
['Tomato and Cucumber', 'Hummus, Beetroot, and Lettuce', 'Cheese', 'Egg', 'Ham', 'Bacon', 'Chicken Club', 'Tuna']
중첩 리스트가 성공적으로 하나의 평탄한 리스트로 변환되었습니다. 모든 재료가 세 개의 리스트가 아닌 하나의 리스트 안에 담겨 있습니다.
방법 2: 중첩 for 루프로 평탄화하기
동일한 결과는 중첩 for 루프를 통해서도 얻을 수 있습니다. '중첩 for 루프'란 말 그대로 'for 루프 안에 또 다른 for 루프가 있는 구조'를 의미합니다. 앞서 작성한 리스트 컴프리헨션은 사실상 아래의 for 루프를 다른 방식으로 표현한 것입니다.
new_foods = [] for sublist in foods: for food in sublist: new_foods.append(food) print(new_foods)
이 코드는 두 개의 for 루프를 사용해 원래 리스트의 각 하위 리스트와 그 안의 모든 요소를 순회합니다. 실행 결과를 확인해 보겠습니다.
['Tomato and Cucumber', 'Hummus, Beetroot, and Lettuce', 'Cheese', 'Egg', 'Ham', 'Bacon', 'Chicken Club', 'Tuna']
리스트 컴프리헨션과 완전히 동일한 결과가 출력됩니다. 다만 대부분의 경우 중첩 for 루프보다 리스트 컴프리헨션이 더 나은 선택입니다. 코드가 더 짧고, 읽기도 더 쉽기 때문입니다.
방법 3: itertools.chain()으로 평탄화하기
itertools는 Python 표준 라이브러리에 포함된 모듈로, 반복 가능한 객체(iterable)와 제너레이터(generator)를 쉽게 다룰 수 있도록 다양한 메서드를 제공합니다.
여기서는 그중 chain() 메서드만 살펴보겠습니다. 이 메서드는 리스트의 리스트를 인자로 받아 평탄화된 리스트를 반환합니다.
먼저 import 문을 사용해 itertools 모듈을 코드에 불러옵니다.
import itertools
다음으로 리스트의 리스트를 정의하고, chain() 메서드를 사용해 리스트를 평탄화합니다.
foods = [ ["Tomato and Cucumber", "Hummus, Beetroot, and Lettuce"], ["Cheese", "Egg"], ["Ham", "Bacon", "Chicken Club", "Tuna"] ] new_foods = itertools.chain(*foods)
코드에서 *(별표) 기호는 리스트 언패킹(unpacking)을 수행합니다. 이를 통해 리스트가 chain() 메서드가 처리할 수 있는 개별 함수 인자로 변환됩니다.
chain() 메서드는 itertools.chain 객체를 반환합니다. 따라서 평탄화된 리스트를 실제로 확인하려면 이 객체를 list() 메서드로 변환해야 합니다.
print(list(new_foods))
코드를 실행해 보겠습니다.
['Tomato and Cucumber', 'Hummus, Beetroot, and Lettuce', 'Cheese', 'Egg', 'Ham', 'Bacon', 'Chicken Club', 'Tuna']
리스트가 성공적으로 평탄화되었습니다.
itertools는 리스트 평탄화에 효과적인 도구이지만, 앞서 소개한 두 가지 방법보다는 진입 장벽이 다소 높습니다. 코드에 새로운 의존성(dependency)을 추가하기 위해 itertools를 임포트해야 하고, chain() 메서드에 사용된 언패킹 개념은 초보자에게 다소 어렵게 느껴질 수 있기 때문입니다.
마무리: 어떤 방법을 선택해야 할까?
Python 리스트는 리스트 컴프리헨션, 중첩 for 루프, itertools.chain() 메서드 세 가지 방식으로 평탄화할 수 있습니다. 그중 리스트 컴프리헨션이 가장 '파이썬답다(Pythonic)'고 평가받는 방법으로, 대부분의 상황에서 권장됩니다.
중첩 for 루프도 충분히 효과적이지만, 리스트 컴프리헨션보다 더 많은 코드 줄을 소모합니다. itertools.chain() 역시 강력한 방법이지만 초보자가 이해하기에는 다소 어려울 수 있습니다.
Python이 기본적으로 필요한 도구를 모두 제공하는 상황에서, 굳이 새로운 라이브러리인 itertools를 임포트할 필요는 없습니다. 특별한 이유가 없다면 리스트 컴프리헨션을 우선적으로 사용하는 것이 좋습니다.
Python 학습을 더 깊이 있게 진행하고 싶다면, 공식 문서와 함께 다양한 실습 예제를 통해 리스트 컴프리헨션과 반복문 활용 능력을 꾸준히 연습해 보시길 권장합니다.