상위 목록: 하위 목록: 작성 날짜: 읽는 데 11 분 소요

간소화(Comprehension)

간소화(Comprehension)란 반복 가능한 객체(iterable)를 바탕으로 새로운 자료형을 축약한 형태로 생성하는 방법입니다.

대표적으로 리스트(List), 집합(Set), 사전(Dict) 간소화와 제너레이터 표현식(Generator Expression)이 있습니다.

즉, List나 Dict 등을 한 줄로 생성하여 간소화할 수 있습니다.

제 15강에서 배운 반복문으로 새 목록을 만들려면 빈 목록을 만들고, 반복하면서, append()로 하나씩 추가하는 세 단계가 필요합니다. 이런 “기존 데이터의 각 요소를 변환하거나 걸러서 새 컬렉션을 만드는” 작업은 매우 자주 등장하기 때문에, Python은 이를 한 줄로 표현하는 전용 문법을 제공합니다.

반복 가능한 객체들을 간소화하게 되면 코드 구성이 깔끔해져 더 읽기 쉬운 코드를 구성할 수 있습니다.

또한, 조건문 등을 추가하여 코드를 간략화 할 수 있습니다.

간소화 구문은 공통적으로 [식 for 변수 in 반복값 if 조건]의 형태를 가지며, “반복값의 각 변수에 대해, 조건을 만족하면, 식의 결과를 모은다”라고 읽습니다. 괄호의 종류에 따라 만들어지는 자료형이 달라집니다.

이번 강좌에서는 리스트 간소화의 실행 순서를 반복문과 비교하며 살펴보고, 조건 추가, 중첩 간소화, 제너레이터 표현식, 집합·사전 간소화, 간소화 변수의 범위를 차례대로 다룹니다.



List Comprehension

L = [i ** 2 for i in range(10)]

print(L)
결과
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

[리스트 값 for 변수 in 범위]를 이용하여 범위만큼 반복하여 변수를 할당해 리스트에 리스트 값을 채웁니다.

range(10)은 0~9의 값을 지니고 있으며 i에서 각각의 값들을 호출합니다.

for문을 통해 반복하게 되어 리스트 값을 조건에 맞게 채우게됩니다.

코드는 왼쪽의 i ** 2가 먼저 쓰여 있지만, 실제 실행은 for 부분부터 시작됩니다. i에 0이 할당되면 i ** 2인 0이 목록에 추가되고, i가 1이면 1, 2이면 4가 추가되는 식으로 9까지 반복되어 10개의 요소가 만들어집니다.

위 코드는 다음 반복문과 같은 결과를 반환합니다.

L = []
for i in range(10):
    L.append(i ** 2)

print(L)
결과
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

두 코드를 비교하면, 간소화 구문은 반복문에서 L = []과 L.append()를 생략하고 추가할 값(i ** 2)을 맨 앞으로 옮긴 형태임을 알 수 있습니다.

간소화 구문은 반복문보다 짧을 뿐만 아니라, append() 메서드를 매번 찾아 호출하는 과정이 없어 일반적으로 더 빠르게 실행됩니다. 또한 “이 줄은 목록을 만든다”는 의도가 한눈에 드러납니다.


words = ["apple", "Banana", "cherry"]
upper = [w.upper() for w in words]

print(words)
print(upper)
결과
[‘apple’, ‘Banana’, ‘cherry’]
[‘APPLE’, ‘BANANA’, ‘CHERRY’]

간소화 구문의 식 자리에는 연산뿐만 아니라 메서드나 함수 호출 등 값을 만드는 모든 표현식을 사용할 수 있습니다.

간소화 구문은 항상 새로운 목록을 만들어 반환하며, 원본 words는 바뀌지 않습니다. 원본을 변환한 결과로 바꾸고 싶다면 words = [w.upper() for w in words]처럼 다시 할당합니다.


result = [print(x) for x in ["a", "b"]]

print(result)
결과
a
b
[None, None]

간소화 구문을 목록을 만들 목적이 아닌 곳에 사용하는 것은 자주 하는 실수입니다.

print(x)는 출력은 하지만 반환값이 None이므로, 출력을 위한 반복인데도 쓸모없는 [None, None] 목록이 함께 만들어집니다. 출력이나 파일 저장처럼 동작 자체가 목적이라면 일반 for 반복문을 사용합니다.



Comprehension + if

L1 = [i ** 2 for i in range(10) if (i > 5)]

print(L1)

L2 = [i ** 2 for i in range(10) if (i > 5) and (i % 2 == 0)]

print(L2)

L3 = [i ** 2 if i < 5 else i for i in range(10)]

print(L3)
결과
[36, 49, 64, 81]
[36, 64]
[0, 1, 4, 9, 16, 5, 6, 7, 8, 9]

[리스트 값 for 변수 in 범위 if (조건)]을 이용하여 if (조건)을 추가해 조건식에 맞는 경우의 리스트를 생성할 수 있습니다.

if (i > 5)는 6 ~ 9의 값만 통과시키므로 i가 6일 때부터 리스트 값이 채워집니다.

실행 순서는 for로 i를 꺼냄 → if 조건 검사 → 참이면 i ** 2를 계산해 추가 → 다음 i의 순서입니다. 조건이 거짓인 0부터 5까지는 i ** 2가 계산조차 되지 않고 건너뜁니다.

조건에는 and나 or 등을 포함하여 조건식을 추가할 수 있습니다.

L2는 6부터 9 중에서 짝수인 6, 8만 통과하므로 [36, 64]가 됩니다.

또한, 간소화 구문에는 삼항 연산자(Ternary operators)를 적용할 수 있습니다.

참 값 if 조건 else 거짓 값의 형태로 값이 반환되므로, 리스트 값 영역에 참 값 또는 거짓 값을 할당할 수 있습니다.

L3는 걸러내는 조건이 없으므로 0부터 9까지 10개 모두 목록에 들어가며, i < 5인 경우만 제곱값이, 나머지는 i 그대로가 들어갑니다. 즉, L1과 L2는 요소의 개수를 줄이고, L3는 개수는 그대로 두고 요소의 값을 바꿉니다.

  • Tip : for 뒤에 오는 if는 값을 걸러내는 조건이며 else를 사용할 수 없습니다. for 앞에 오는 if-else는 값을 선택하는 삼항 연산자이므로 반드시 else가 필요합니다.


print([i ** 2 if i < 5 for i in range(10)])
결과
SyntaxError: expected ‘else’ after ‘if’ expression

위 Tip의 실수를 그대로 보여주는 예제입니다. for 앞에 if만 작성하면 Python은 이를 조건 표현식으로 해석하므로 else가 없다는 SyntaxError가 발생합니다.

i < 5인 값만 남기려는 의도였다면 [i ** 2 for i in range(10) if i < 5]처럼 if를 for 뒤로 옮겨야 합니다.



Multiple Comprehension

L = [['a', 'b', 'c'], ['d', 'e', 'f']]

flatten = [j for i in L for j in i]

print(flatten)

extend = [[j + j for j in i] for i in L]

print(extend)
결과
[‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’]
[[‘aa’, ‘bb’, ‘cc’], [‘dd’, ‘ee’, ‘ff’]]

간소화 구문은 여러 번 사용하여 반복할 수 있습니다.

L은 2차원 리스트로 구성되어 있어 1차원 리스트로 변환한다면 두 번의 반복문의 구성을 필요로 합니다.

여기서, 간소화 구문을 두 번 반복하여 1차원 리스트로 간단하게 변경할 수 있습니다.

기본적인 간소화 구문은 [i for i in L]의 형태를 갖습니다.

하지만, 이 구문은 [‘a’, ‘b’, ‘c’]와 [‘d’, ‘e’, ‘f’]를 반복하므로 한 번 더 반복해야 합니다.

그러므로 i에 대한 반복문을 추가로 구성합니다.

[j for j in i]의 구성이 필요합니다. 여기서, 앞 부분이 아닌 뒷 부분에 연결합니다.

그러므로, [i for i in L]에서 반환 해줄 리스트 값은 j가 되므로 [j for i in L]로 먼저 변경합니다.

이후에, L 뒷 부분에 for j in i를 이어 붙여 [j for i in L for j in i] 형태로 구성합니다.

여러 개의 for를 이어 붙이면 왼쪽의 for가 바깥쪽 반복문, 오른쪽의 for가 안쪽 반복문이 됩니다. 즉, 다음 중첩 반복문과 같은 순서로 실행됩니다.

L = [['a', 'b', 'c'], ['d', 'e', 'f']]

flatten = []
for i in L:
    for j in i:
        flatten.append(j)

print(flatten)
결과
[‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’]

반복 변수의 변화를 따라가면, 바깥 반복에서 i가 ['a', 'b', 'c']일 때 안쪽 반복이 j에 'a', 'b', 'c'를 차례대로 할당하고, 안쪽 반복이 끝나면 바깥 반복이 i를 ['d', 'e', 'f']로 바꾼 뒤 안쪽 반복을 처음부터 다시 실행합니다.

간소화 구문의 for는 반복문을 들여쓰기 없이 위에서부터 한 줄로 이어 쓴 순서 그대로라고 기억하면 됩니다.

만약, 3차원 리스트를 평평하게 만든다면 위와 같은 방식으로 맨 앞의 j 대신에 k로 변경한 다음, for k in j를 그대로 이어 붙이면 됩니다.

즉, [k for i in L for j in i for k in j]과 같은 구성이 됩니다.


다음으로는, 뒷 부분에 이어붙이는 방법이 아닌 앞 부분에 이어 붙이는 방법입니다.

이 방법은 차원을 그대로 유지한 상태로 값을 변경할 수 있습니다.

리스트에 채워질 값 자리에 다시 간소화 구문 [j + j for j in i]를 작성하여, L의 각 원소 i마다 새로운 리스트를 만드는 방법입니다.

이 경우에는 바깥쪽 간소화(for i in L)가 먼저 실행되고, 각 i마다 안쪽 간소화가 실행되어 ['aa', 'bb', 'cc'] 같은 목록 하나를 만듭니다. 바깥쪽 간소화는 이렇게 만들어진 목록들을 모으므로 결과가 2차원 목록이 됩니다.

즉, for를 뒤에 이어 붙이면 차원이 줄어들고, 간소화를 식 자리에 넣으면 차원이 유지됩니다.


table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
pairs = [(i, j) for i in range(3) for j in range(3) if i != j]

print(table)
print(pairs)
결과
[[1, 2, 3], [2, 4, 6], [3, 6, 9]]
[(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]

중첩 간소화로 구구단 표와 같은 2차원 리스트를 만들 수 있습니다.

table은 i가 1일 때 [1 * 1, 1 * 2, 1 * 3], 2일 때 [2, 4, 6]처럼 행 하나씩을 만들어 모읍니다.

for를 이어 붙인 경우에도 마지막에 if 조건을 추가하여 원하는 조합만 걸러낼 수 있습니다.

pairs는 i가 0일 때 j가 0, 1, 2로 바뀌며 (0, 0)은 i != j가 거짓이라 제외되고 (0, 1), (0, 2)만 남습니다. 이후 i가 1, 2일 때도 같은 방식으로 같은 숫자의 쌍만 제외되어 6개의 쌍이 만들어집니다.

  • Tip : 중첩이 세 단계 이상으로 깊어지면 오히려 읽기 어려워지므로, 일반 반복문으로 작성하는 것이 좋습니다.



Tuple과 제너레이터 표현식

data = [0, 7, 6, 9, 2, 3]

G = (i for i in data)
T = tuple(i for i in data)

print(G)
print(T)
결과
<generator object <genexpr> at 0x000001CA85440E10>
(0, 7, 6, 9, 2, 3)

Tuple은 별도의 간소화 구문이 없기 때문에, 명시적으로 tuple()로 변환해야 합니다.

소괄호를 사용한 간소화는 튜플이 아닌 제너레이터 표현식(Generator Expression)입니다.

제너레이터는 값을 미리 만들어 두지 않고 필요할 때마다 하나씩 생성하는 반복자(iterator)이므로, 출력하면 <generator object ...> 형태로 표시됩니다. (메모리 주소는 실행할 때마다 달라집니다.)

그러므로, 제너레이터를 tuple()로 변경해 사용합니다.

  • Tip : 함수의 인수가 제너레이터 표현식 하나뿐이라면 tuple(i for i in data), sum(i ** 2 for i in data)처럼 소괄호를 한 번만 작성해도 됩니다.


data = [0, 7, 6, 9, 2, 3]
G = (i * 10 for i in data)

print(next(G))
print(next(G))
print(list(G))
print(list(G))
결과
0
70
[60, 90, 20, 30]
[]

제너레이터가 값을 하나씩 만드는 과정을 확인하는 예제입니다.

G를 만드는 시점에는 아무 계산도 하지 않습니다. next(G)를 호출하면 그때 data의 첫 요소 0을 꺼내 0 * 10을 계산해 반환하고, 다음 next(G)는 7 * 10인 70을 반환합니다.

list(G)는 남아 있는 값인 60, 90, 20, 30만 꺼내 목록으로 만듭니다. 이미 꺼낸 값은 다시 만들지 않으므로, 두 번째 list(G)는 꺼낼 값이 없어 빈 목록이 됩니다.

이처럼 제너레이터는 한 번만 사용할 수 있습니다. 여러 번 반복해야 한다면 리스트 간소화를 사용하거나 list()로 변환해 저장합니다.


import sys

L = [i for i in range(10000)]
G = (i for i in range(10000))

print(sys.getsizeof(L))
print(sys.getsizeof(G))
print(sum(i for i in range(10000)))
결과
85176
200
49995000

sys.getsizeof()로 객체가 차지하는 메모리 크기(바이트)를 비교한 예제입니다. 수치는 실행 환경에 따라 조금씩 다를 수 있습니다.

리스트는 10,000개의 요소를 모두 저장해야 하므로 크기가 요소 수에 비례하여 커지지만, 제너레이터는 다음 값을 계산하는 방법과 현재 위치만 기억하므로 요소 수와 관계없이 크기가 작습니다.

sum(), max(), any()처럼 값을 한 번 훑고 끝나는 함수에 전달할 때는 목록을 만들 필요가 없으므로 제너레이터 표현식을 사용하는 것이 좋습니다. 반대로 결과를 여러 번 사용하거나 인덱싱해야 한다면 리스트 간소화를 사용합니다.



Set Comprehension

text = "YUNDAEHEE"

S1 = set([i for i in text])

print(S1)

S2 = {i for i in text}

print(S2)
결과
{‘Y’, ‘D’, ‘U’, ‘A’, ‘N’, ‘H’, ‘E’}
{‘Y’, ‘D’, ‘U’, ‘A’, ‘N’, ‘H’, ‘E’}

간소화을 이용할 때, 범위를 문자열로 사용하여 문자 값을 직접 할당할 수 있습니다.

{값 for 변수 in 범위}처럼 중괄호를 사용하면 Set Comprehension이 되며, 집합이므로 중복된 값은 하나만 저장됩니다.

"YUNDAEHEE"는 9글자이지만 E가 세 번 등장하므로, 집합에는 서로 다른 7개의 문자만 저장됩니다. 집합은 원소를 추가할 때 해시로 같은 값이 있는지 확인하므로, 이미 있는 E는 다시 추가되지 않습니다.

또한 Set의 연산이 필요할 때, Set Comprehension를 이용한다면 S1에서 사용된 List → Set 구간을 줄여 전체적인 연산량을 감소시킬 수 있습니다.

S1은 중복을 포함한 9개짜리 목록을 먼저 만든 뒤 집합으로 변환하므로 중간 목록이 생기지만, S2는 처음부터 집합에 바로 추가합니다.

  • Tip : 집합은 순서가 없으므로, 문자열로 구성된 집합의 출력 순서는 실행할 때마다 달라질 수 있습니다.

  • Tip : {}는 빈 집합이 아닌 빈 사전입니다. 중괄호 안에 key: value 형태가 있으면 사전 간소화, 값만 있으면 집합 간소화가 됩니다.



Dictionary Comprehension

text = "cheese"

D = {i: text.count(i) for i in text}

print(D)
결과
{‘c’: 1, ‘h’: 1, ‘e’: 3, ‘s’: 1}

List 이외에도 Dictionary Comprehension를 이용하여 사전을 간소화할 수 있습니다.

{key: value for 변수 in 범위}를 이용하여 범위만큼 반복하여 변수를 할당해 key값과 value값을 채웁니다.

key에는 문자가 담겨있으며 value에는 문자의 개수가 담겨있습니다.

e처럼 같은 key가 여러 번 등장하면 마지막으로 할당된 값으로 덮어쓰며, 사전은 key가 처음 추가된 순서를 유지합니다.

위 코드는 다음 반복문과 같은 순서로 실행됩니다.

text = "cheese"

D = {}
for i in text:
    D[i] = text.count(i)

print(D)
결과
{‘c’: 1, ‘h’: 1, ‘e’: 3, ‘s’: 1}

i가 'c', 'h', 'e', 'e', 's', 'e'로 바뀌며 매번 D[i]에 값을 할당합니다. 'e'는 세 번 할당되지만 key가 같으므로 항목이 늘어나지 않고 같은 자리의 값만 3으로 다시 덮어씁니다.

다만, 이 방식은 'e'의 개수를 세 번 반복해서 세기 때문에 문자열이 길어지면 비효율적입니다. {i: text.count(i) for i in set(text)}처럼 중복을 제거한 뒤 반복하면 각 문자를 한 번만 셉니다. 단, 이 경우 사전의 순서는 집합의 반복 순서를 따릅니다.


scores = {"A": 90, "B": 65, "C": 80}

passed = {k: v for k, v in scores.items() if v >= 70}
swapped = {v: k for k, v in scores.items()}

print(passed)
print(swapped)
결과
{‘A’: 90, ‘C’: 80}
{90: ‘A’, 65: ‘B’, 80: ‘C’}

사전.items()를 사용하면 key와 value를 동시에 반복할 수 있어, 조건에 맞는 항목만 남기거나 key와 value를 뒤바꾼 사전을 쉽게 만들 수 있습니다.

passed는 65점인 "B"가 조건 v >= 70에서 걸러져 두 항목만 남습니다. swapped는 k: v 대신 v: k로 작성하여 점수를 key로, 이름을 value로 뒤바꿉니다.

  • Tip : key와 value를 뒤바꿀 때 원래 value에 중복이 있다면, key가 겹치므로 나중에 나온 항목만 남습니다.



간소화 변수의 범위

L = [i for i in range(3)]

try:
    print(i)
except NameError as e:
    print(e)
결과
name ‘i’ is not defined

간소화 구문에서 사용한 반복 변수(i)는 간소화 구문 안에서만 유효하며, 바깥에서 사용할 수 없습니다.

일반 for 반복문은 반복이 끝난 뒤에도 마지막 값이 변수에 남아 있으므로, 이 부분이 서로 다릅니다.

이 덕분에 간소화 구문은 바깥에 같은 이름의 변수가 있어도 값을 덮어쓰지 않습니다. 예를 들어 미리 i = 100을 할당한 뒤 간소화 구문에서 i를 사용해도, 간소화 구문이 끝나면 i는 그대로 100입니다.

  • Tip : Python 3.12 이상에서는 PEP 709에 따라 리스트·집합·사전 간소화가 별도의 함수 없이 인라인으로 실행되어 더 빨라졌습니다. 이로 인해 오류 발생 시 트레이스백에 <listcomp> 같은 별도 단계가 표시되지 않으며, 함수 안의 간소화 구문에서 locals()를 호출하면 바깥 함수의 지역 변수도 함께 포함됩니다. 반복 변수가 바깥으로 새어 나가지 않는 동작은 그대로 유지됩니다.


data = [3, 8, 1, 9]

if any((big := x) > 5 for x in data):
    print("5보다 큰 첫 값 :", big)
결과
5보다 큰 첫 값 : 8

반복 변수와 달리, 간소화 구문이나 제너레이터 표현식 안에서 제 15강의 할당 표현식(:=)으로 할당한 변수는 바깥 범위에 남습니다. (Python 3.8 이상)

any()는 참인 값을 만나는 즉시 반복을 멈추므로, x가 8일 때 (big := 8) > 5가 참이 되어 반복이 종료됩니다. 이때 할당된 big이 바깥에 남아 있으므로, 조건을 만족한 첫 번째 값을 꺼내 사용할 수 있습니다.



정리

이름 설명 반환값/특징
[식 for 변수 in 반복값] 리스트 간소화 새 list 반환, 원본은 그대로
[식 for 변수 in 반복값 if 조건] 조건으로 요소 걸러내기 조건이 거짓이면 식을 계산하지 않음
[A if 조건 else B for 변수 in 반복값] 조건으로 값 선택 요소 개수는 그대로, else 필수
[식 for i in A for j in i] 여러 for 연결 왼쪽 for가 바깥 반복, 차원이 줄어듦
[[식 for j in i] for i in A] 간소화 중첩 차원 유지
(식 for 변수 in 반복값) 제너레이터 표현식 값을 하나씩 생성, 한 번만 사용 가능, 메모리 절약
{식 for 변수 in 반복값} 집합 간소화 중복 제거, 순서 없음
{키: 값 for 변수 in 반복값} 사전 간소화 같은 key는 마지막 값으로 덮어씀

간소화 구문은 “기존 데이터로 새 컬렉션을 만든다”는 목적이 분명할 때 사용합니다. 실행 순서는 for → if → 식이며, 여러 for는 중첩 반복문을 위에서부터 한 줄로 이어 쓴 순서와 같습니다. 출력처럼 동작이 목적이거나, 중첩과 조건이 많아져 한눈에 읽기 어렵다면 일반 반복문으로 작성하는 것이 좋습니다.

댓글 남기기