PYTHON

파이썬 리스트 컴프리헨션 — for 네 줄이 한 줄로, 조건은 앞과 뒤가 다릅니다

PANCO_IT
#파이썬#python#리스트컴프리헨션#listcomprehension#for문#if else#이중for문#파이썬기초#파이썬독학

종이 상자 안에 연둣빛 사과가 가지런히 담겨 있다

이미지 출처: Pexels · Tobi and Chris · Pexels License

시험 점수 다섯 개에서 80점 넘는 것만 뽑습니다. 시즌 1에서 배운 대로 쓰면 이렇습니다.

scores = [88, 72, 95, 60, 81]

passed = []
for s in scores:
    if s >= 80:
        passed.append(s)

print(passed)
[88, 95, 81]

같은 일을 한 줄로 쓴 것이 오늘의 주인공입니다.

passed = [s for s in scores if s >= 80]

결과는 똑같이 [88, 95, 81]입니다. 다섯 줄이 한 줄이 됐는데, 줄 수보다 눈여겨볼 것은 읽는 방향입니다. 위 코드는 위에서 아래로 읽고, 아래 코드는 가운데부터 읽습니다. 이 글은 그 한 줄이 어디서 왔는지부터 시작해, 조건이 붙는 자리 둘, 그리고 어디까지 줄여도 되는지까지 갑니다.

리스트 컴프리헨션이 뭔가요 — 벨트 위에서 담는 새 상자

파이썬 공식 자습서는 리스트 컴프리헨션을 리스트를 만드는 간결한 방법이라고 정의합니다. 이미 있는 리스트나 범위를 한 바퀴 돌면서, 각 원소를 바꾸거나 골라서 새 리스트를 하나 만드는 문법입니다. 있는 리스트를 고치는 것이 아니라 새로 만드는 것이라는 점이 먼저입니다.

언제 쓰나. 리스트 하나를 재료로 다른 리스트를 만들 때입니다. 점수에서 합격만, 이름에서 대문자로, 가격에서 세금 붙여서. 세 경우 모두 재료 리스트를 돌면서 하나씩 처리해 새 리스트에 담는 일입니다.

성질은 넷입니다.

성질
새 리스트를 돌려준다재료 리스트는 그대로 두고 결과가 새 리스트로 나온다
대괄호 안에 다 들어간다[표현식 for 변수 in 재료]가 기본 꼴이고 if가 뒤에 붙을 수 있다
안에서 쓴 변수가 밖에 남지 않는다for문은 돌고 나면 s가 남지만, 컴프리헨션은 남기지 않는다
표현식이 튜플이면 괄호가 필요하다[(x, y) for ...]처럼 감싸야 한다

이 편에서 쓰는 말은 이렇게 정합니다.

용어이 글에서의 뜻
재료for 뒤에 오는, 한 바퀴 돌 리스트나 range
표현식맨 앞에 오는, 새 리스트에 담을 값을 만드는 식
골라내는 if맨 뒤에 붙어 담을지 말지를 정하는 조건
바꿔 넣는 if else맨 앞 표현식 안에 들어가 무엇을 담을지를 고르는 조건

비유는 정의 뒤에 붙입니다. 재료 리스트는 벨트 위를 지나가는 과일이고, 컴프리헨션은 벨트 옆에 선 사람 한 명입니다. 지나가는 것을 하나씩 집어 그대로 담거나, 다듬어 담거나, 상자에 안 넣고 보냅니다. 벨트 위의 과일 자체는 손대지 않습니다.

컨베이어 벨트 위에 점수 다섯 개가 지나가고 벨트 옆에 선 사람이 80 이상만 집어 새 상자에 담는 장면. 벨트 위에는 88, 72, 95, 60, 81이 순서대로 있고 72와 60은 그대로 지나가며 새 상자에는 88, 95, 81이 담겨 있다. 벨트 아래에 원래 리스트는 그대로라는 글씨가 있다

재료는 벨트 위를 그대로 지나가고, 담긴 것만 새 상자가 됩니다.

for 네 줄 시절 — 무엇이 번거로웠나

컴프리헨션이 없다고 치고 제곱수 열 개를 모아 보겠습니다. 공식 자습서에 나오는 그 예제입니다.

for와 append로 제곱수 모으기
squares = []
for x in range(10):
  squares.append(x**2)

print(squares)
print(x)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
9

결과는 맞게 나옵니다. 번거로운 자리는 셋입니다.

먼저 빈 리스트를 미리 만들어야 합니다. squares = [] 한 줄이 없으면 append할 곳이 없습니다. 다음으로 담는 동작을 따로 적어야 합니다. 하고 싶은 일은 "제곱해서 모으기"인데 코드에는 "제곱한 것을 append 하기"라고 적힙니다. 마지막 줄의 출력이 세 번째입니다. 루프가 끝났는데 x9밖에 남아 있습니다. 공식 자습서도 이 점을 부작용이라고 적어 두었고, 컴프리헨션은 이 부작용 없이 같은 결과를 낸다고 설명합니다.

세 가지 다 치명적이지는 않습니다. 다만 같은 모양의 네 줄을 하루에 열 번 쓰다 보면, 이 네 줄이 하나의 동작이라는 것을 문법이 알아 줬으면 하게 됩니다.

한 줄이 된 뒤 — 읽는 순서는 가운데부터

같은 일을 컴프리헨션으로 쓰면 이렇습니다.

한 줄로 제곱수 모으기
squares = [x**2 for x in range(10)]

print(squares)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

빈 리스트도, append도 없습니다. x도 밖에 남지 않습니다. 여기서 마지막 줄에 print(x)를 붙이면 NameError가 납니다. 컴프리헨션 안에서 쓴 x는 대괄호가 닫히면 사라집니다.

처음 보면 앞뒤가 뒤집힌 것처럼 읽힙니다. 읽는 순서를 따로 익히면 풀립니다. 가운데 for부터, 그다음 맨 앞, 그다음 맨 뒤입니다.

대괄호 한 줄 x2 for x in range(10) if x % 2 == 0 을 세 조각으로 나눠 읽는 순서를 표시한 그림. 가운데 for x in range(10) 조각에 1번과 "0부터 9까지 하나씩 x에", 맨 앞 x2 조각에 2번과 "그 x를 제곱해서 담는다", 맨 뒤 if x % 2 == 0 조각에 3번과 "단, 짝수일 때만"이라는 메모가 붙어 있다

1번 재료, 2번 담을 것, 3번 조건. 왼쪽부터가 아니라 가운데부터입니다.

for문으로 풀면 순서가 그대로 보입니다.

for x in range(10):        # 1. 재료
    if x % 2 == 0:         # 3. 조건
        result.append(x**2)  # 2. 담을 것

컴프리헨션은 이 세 줄을 담을 것, 재료, 조건 순서로 한 줄에 눕힌 것입니다. 공식 자습서의 표현으로는 "표현식과 그 뒤를 따르는 for 절, 그리고 없거나 여러 개의 for나 if 절"입니다.

저는 이 문법을 처음 배우는 분에게 왼쪽부터 읽지 말라고 먼저 말합니다. 왼쪽부터 읽으면 아직 정해지지도 않은 x를 제곱하라는 말이 되어 막힙니다.

골라내는 if는 뒤에 붙습니다

조건이 붙는 자리가 둘이라 검색창에 "리스트 컴프리헨션 if else"가 자주 올라옵니다. 둘을 갈라 보겠습니다.

담을지 말지를 정하는 조건은 맨 뒤입니다. 처음 코드의 if s >= 80이 이것입니다.

80점 이상만 골라 담기
scores = [88, 72, 95, 60, 81]

passed = [s for s in scores if s >= 80]
print(passed)

names = ["kim", "", "lee", "", "park"]
filled = [n for n in names if n != ""]
print(filled)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

[88, 95, 81]
['kim', 'lee', 'park']

조건에 걸리지 않은 원소는 새 리스트에 아예 들어가지 않습니다. 그래서 결과 리스트가 재료보다 짧아집니다. 두 번째 예처럼 빈 문자열을 걸러 내는 데 자주 씁니다.

여기에는 else를 붙일 수 없습니다.

passed = [s for s in scores if s >= 80 else 0]
SyntaxError: invalid syntax

뒤에 붙는 if는 "담을까 말까"만 정하는 자리라 "아니면 이걸 담아라"가 들어갈 곳이 없습니다. else를 쓰고 싶다면 조건이 하는 일이 다른 것이고, 자리도 다릅니다.

바꿔 넣는 if else는 앞에 옵니다

무엇을 담을지를 고르는 조건은 맨 앞 표현식 안에 들어갑니다. 이때는 모든 원소가 담기고, 원소마다 값이 갈립니다.

합격 불합격으로 바꿔 담기
scores = [88, 72, 95, 60, 81]

result = ["합격" if s >= 80 else "불합격" for s in scores]
print(result)

print(len(scores), len(result))

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

['합격', '불합격', '합격', '불합격', '합격']
5 5

결과 리스트의 길이가 재료와 같습니다. 하나도 버리지 않고 값만 바꿨기 때문입니다. "합격" if s >= 80 else "불합격" 부분은 파이썬의 조건 표현식이고, 컴프리헨션 문법이 아니라 그냥 값을 만드는 식입니다. 그래서 표현식 자리인 맨 앞에 옵니다.

조건이 들어가는 두 자리를 나란히 놓은 그림. 위쪽은 s for s in scores if s >= 80 으로 맨 뒤 if 자리에 "담을까 말까"라는 라벨과 결과가 셋으로 줄어든다는 설명이 있고, 아래쪽은 "합격" if s >= 80 else "불합격" for s in scores 로 맨 앞 표현식 자리에 "무엇을 담을까"라는 라벨과 결과가 다섯 개 그대로라는 설명이 있다. 두 줄의 for s in scores 부분은 같은 파란색이다

뒤의 if는 개수를 줄이고, 앞의 if else는 개수를 지킵니다.

둘을 한 줄에 같이 쓸 수도 있습니다. 앞에서 값을 고르고, 뒤에서 담을지를 정합니다.

result = ["A" if s >= 90 else "B" for s in scores if s >= 80]
['B', 'A', 'B']

80 미만은 뒤의 if에서 빠지고, 남은 셋이 앞의 if else로 A나 B가 됩니다. 되기는 하지만 한 줄에 조건이 둘이면 읽는 사람이 두 번 멈춥니다. 이 정도가 한 줄의 한계에 가깝다고 봅니다.

elif는 없습니다. 조건 표현식에 elif가 없기 때문입니다. 등급 셋을 나누려면 "A" if s >= 90 else "B" if s >= 80 else "C"처럼 겹쳐 쓸 수는 있는데, 이쯤 되면 함수를 하나 만들어 [grade(s) for s in scores]로 쓰는 편이 읽기 쉽습니다.

이중 for는 되지만 한 줄이 두 줄보다 나은지는 따로 봅니다

for를 두 번 쓸 수도 있습니다. 공식 자습서의 예제입니다.

for 두 개로 짝 만들기
pairs = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y]
print(pairs)

combs = []
for x in [1, 2, 3]:
  for y in [3, 1, 4]:
      if x != y:
          combs.append((x, y))
print(combs)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

두 결과가 같습니다. 규칙은 하나입니다. for와 if의 순서가 for문으로 풀었을 때와 같아야 합니다. 바깥 for x가 먼저, 안쪽 for y가 다음, 조건이 마지막입니다. 표현식이 (x, y)처럼 튜플이면 괄호로 감싸야 합니다. 괄호를 빼고 [x, y for ...]라고 쓰면 파이썬이 "컴프리헨션 대상에 괄호를 잊었느냐"고 되묻는 SyntaxError가 납니다.

이 편의 함정이 여기 있습니다. 되는 것과 읽히는 것은 다릅니다. 3×4 행렬의 행과 열을 바꾸는 코드를 공식 자습서는 이렇게 씁니다.

matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

transposed = [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

맞는 코드이고 한 줄입니다. 그런데 자습서는 바로 뒤에 복잡한 흐름문보다 내장 함수를 쓰라고 적고, 같은 일을 list(zip(*matrix))로 보여 줍니다. 한 줄로 쓸 수 있다는 것이 한 줄로 써야 한다는 뜻은 아닙니다.

제가 쓰는 기준은 이렇습니다. for가 하나이고 if가 하나까지면 컴프리헨션, for가 둘이거나 if가 둘이면 일단 for문으로 풀어 써 보고, 그래도 한 줄이 더 잘 읽힐 때만 다시 줄입니다. 대개 다시 줄이지 않게 됩니다.

남은 한계 — 컴프리헨션이 못 하는 것

한 줄이 만능은 아닙니다. 안 되거나 안 어울리는 자리가 셋 있습니다.

중간에 다른 일을 끼울 수 없습니다. 돌면서 print를 찍거나, 조건에 따라 break로 멈추거나, 두 리스트에 나눠 담는 일은 for문의 몫입니다. 컴프리헨션은 새 리스트 하나를 돌려주는 식이지 문장이 아닙니다.

result = [print(s) for s in scores]
print(result)
88
72
95
60
81
[None, None, None, None, None]

찍히기는 하는데 None 다섯 개짜리 리스트가 같이 만들어집니다. 쓰지도 않을 리스트를 만드는 셈이라, 출력만 하려면 for문이 맞습니다.

결과가 크면 메모리를 한 번에 씁니다. 컴프리헨션은 리스트 전체를 만들어 놓습니다. 백만 개를 만들면 백만 개가 한꺼번에 자리를 차지합니다. 하나씩 꺼내 쓰기만 할 거라면 대괄호를 소괄호로 바꾼 (x**2 for x in range(1000000)) 꼴이 따로 있는데, 그것은 이 시리즈 뒤쪽에서 따로 다루고, 여기서는 이름만 적어 둡니다.

딕셔너리와 집합도 같은 꼴로 만듭니다. 이건 한계가 아니라 덤입니다. 지난 편의 중괄호를 그대로 씁니다.

딕셔너리와 집합 컴프리헨션
names = ["kim", "lee", "park"]

lengths = {n: len(n) for n in names}
print(lengths)

initials = {n[0] for n in names}
print(initials)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

{'kim': 3, 'lee': 3, 'park': 4}
{'k', 'l', 'p'}

중괄호 안에 키: 값이면 딕셔너리, 값만 있으면 집합입니다. 3편에서 본 대로 집합은 순서를 보장하지 않으니 출력 순서는 달라질 수 있습니다.

네 줄과 한 줄, 어느 쪽을 쓸까

상황쓸 것이유
리스트에서 골라내기[x for x in 재료 if 조건]가장 흔한 꼴, 한눈에 읽힌다
리스트 원소마다 바꾸기[식 for x in 재료]append 없이 새 리스트
원소마다 값을 둘 중 하나로[a if 조건 else b for x in 재료]if else는 앞, 개수는 그대로
조건이 셋 이상함수 + [f(x) for x in 재료]elif가 없고 겹친 if else는 안 읽힌다
for가 둘for문으로 먼저 써 본다한 줄이 더 잘 읽힐 때만 줄인다
돌면서 출력·중단·두 리스트for문컴프리헨션은 새 리스트 하나를 돌려주는 식이다

기억할 것은 셋입니다. 가운데 for부터 읽습니다. 골라내는 if는 뒤, 바꿔 넣는 if else는 앞입니다. 한 줄에 forif가 둘 이상이면 한 번 풀어 써 봅니다.

한 줄만 바꾸면 결과는 어떻게 될까요

미니 퀴즈

OX 3문제 + 4지선다 5문제

참고 자료


포켓코딩(PYTHON2) 4편 · 3편(파이썬 튜플과 집합)에서 이어집니다. 예제 코드는 강사 개인 GitHub 저장소의 강의 자료와 파이썬 공식 자습서의 예제를 바탕으로 다시 썼습니다. 다음 편은 함수 심화입니다. 인자를 몇 개든 받는 *args**kwargs, 그리고 기본값에 리스트를 쓰면 왜 호출할 때마다 같은 리스트가 따라오는지를 다룹니다.

PANCO_IT

IT 지식을 정리하는 개발자

관련 글