2020.06.06 리스트를 단 한줄로 만들자, List Comprehension과 리스트 생성의 성능 비교

이번 일일 파이썬 주제는 List Comprehension 입니다.


The Python Logo | Python Software Foundation


파이썬을 배우시는 분 들이라면 한 번쯤 배워본 문법일 테고,
리스트를 단 한 줄 코드로 생성하는 깔끔한 특성 때문에
많이들 애용하고 계실 겁니다.


사실 저는 List Comprehension(리스트 컴프리헨션)을 그렇게 많이 사용하는 편은 아닙니다.


물론 코드의 행을 줄일 수록 전체적인 코드가 간결 해진다는 장점도 있을 것이며,
리스트 컴프리헨션을 사용한 행을 보고 '아 이 코드는 리스트를 생성하는 코드 구나'
쉽게 알아볼 수 있기 때문에 좋긴 하지만,


문제는 그 한 줄이 너무나도 길어질 수 있고, 그 한 줄을 해석하는 데
영겁의 시간이 걸리는 경우가 잦아지는,
즉 코드의 복잡도를 증가시키는 문제가 있었기 때문에(물론 이건 실력이 부족한 저의 문제...)


기어이 고집을 부려가면서,
강제로 for문 몇개 집어 넣어 가면서, append로 꾸역꾸역 데이터를 집어넣으면서
리스트를 만들고 그랬죠.


만화 '아따아따'는 실화를 바탕으로 한 것이었고, 캐릭터들은 실존인물이었다
리스트 컴프리헨션 안쓸꺼야!!!


하지만 최근에 미디움을 돌아다니면서, 리스트 컴프리헨션이 성능이 굳이 반복문 돌리고 append 함수 사용하는 것보다 월등히 뛰어나다는 것을 알게 되면서...


부랴부랴 코딩할 때 리스트 컴프리헨션을 공부하기 시작했고,
이 내용은 공유하면 좋겠다고 생각하여
일일 파이썬 주제로 사용하게 되었습니다.


제가 참조한 미디움 글은 아래의 글입니다.






See what list actions are the fastest in Python to speed up your programs.
이 글은 제가 이제부터 소개 드릴 리스트 생성의 성능 개선 파트 말고도, 리스트를 정렬하는 방법 중 가장 성능이 좋은 방법에 대해서도 big O 표기법를 통해 설명하고 있으니


좀 더 성능 좋은 파이썬 코드를 만들어 보고 싶다! 하시는 분들은
읽어 보시는 것을 추천 드리겠습니다.


그럼 본격적으로 리스트 컴프리헨션에 대해 알아보도록 하죠.





List Comprehension



리스트 컴프리헨션은 리스트를 한 줄로 만들 수 있는 파이썬 문법 중 하나입니다.


파이썬에서는 리스트 말고도 Set, Dictionary 타입의 데이터도
컴프리헨션을 제공합니다.
이들 또한 아래의 코드로 한번 구현해 보겠습니다.


기존에 리스트를 만드는 방법은 아래와 같았습니다.
단순히 넣고 싶은 데이터를 나열하고,
대 괄호를 씌운 다음 변수에 대입하면 끝이었습니다.



ls = [1,2,3,4,5]
print(ls)

[1, 2, 3, 4, 5]


물론 데이터가 양이 적으면 모를까,
수백 수천 수만 개의 데이터가 담긴 리스트를 만들 때는
하나하나 나열하는 방식으로 리스트 구축 하기엔 한계가 있죠.


데이터가 많아졌을 때,
우리는 이런 방법을 사용했을 겁니다.


ls = []
for i in range(1, 101):
    ls.append(i)
print(ls[-10:])


[91, 92, 93, 94, 95, 96, 97, 98, 99, 100]


하지만
리스트 컴프리헨션을 사용하면
아래의 코드를 한 줄로 구현 할 수 있습니다. 아래 처럼요.


ls = [i for i in range(1, 101)]
print(ls[-10:])


[91, 92, 93, 94, 95, 96, 97, 98, 99, 100]


리스트 컴프리헨션은 위의 코드를 보시는 것 처럼
그렇게 어려운 문법은 아닙니다.


그저 대괄호 안에 반복문을 집어넣은 형태죠. 주로 for 문이 들어갑니다.


코드 리뷰를 간단히 하자면,

1) range로 0 부터 99 까지의 값을 생성하고, i로 하나씩 빼온다.

2) 그 i를 리스트에 입력한다.

이렇게 되겠습니다.




List Comprehension 조건식 추가



그럼 리스트 컴프리헨션에 반복문을 넣고,
조건식 걸어서 특정 값만 넣을 수는 없냐,
혹은 특정 값들을 수식을 넣어 가공 할 수는 없냐,

즉, 반복문으로 가져오는 값들 전부를 넣어야 되는거냐
라고 한다면 또 그렇지는 않습니다.


반복문과 함께 조건문을 같이 넣어 줄 수 있으며,
아니면 함수랑 연동 시켜서 복잡한 조건을 거쳐갈 수도 있고,
그것도 아니면 람다함수를 넣어서 함수 선언도 안 하게 만들 수도 있죠.
(람다함수 넣으면 코드가 너무 길어져, 저는 별로 좋아하지 않습니다 ㅎㅎ..)


이번에는 if문을 통해 리스트 생성에 조건을 달아주는 것만 확인하겠습니다.


먼저 한 번 1부터 100까지 넣되,
조건문을 넣어서 짝수는 두 배, 홀수는 음수로 만들어 보죠.




ls = [i*2 if i%2==0 else i*-1 for i in range(1, 101)]
print(ls[:5])


[-1, 4, -3, 8, -5]



한 줄이 더욱 길어졌네요. 간단히 리뷰를 하자면 다음과 같습니다.

1) 맨 처음 부분이 기본 적으로 어떤 값을 뺄 지 입니다. 여기선 i*2라고 했는데,
그냥 for문만 넣으면 모든 값이 두 배가 되어서 들어가겠죠.

2) 그 다음 부분이 조건식 파트입니다. 만약 i가 2로 나눈 나머지가 0일 때(즉 짝수 일 때)
라는 조건이 걸려있습니다. 다만 뭘 할 지는 나와있지 않는데,
그 뭘 할지 조건이 바로 1번에서 설명한 부분, i를 두 배로 키워라가 되겠습니다.

3)그 다음 else가 나오는데, else 다음에 나오는 i * -1은 i가 짝수가 아닐 때 작동하는 코드입니다. 반복문은 기존 컴프리헨션과 설명이 동일하기에 생략합니다.


그런데 조건문의 위치가 다르다면 또 이야기가 달라집니다.
아래 코드는 1부터 100 중에 짝수만 가져오는 코드입니다.


ls = [i for i in range(1, 101) if i%2==0]
print(ls[:10])


[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]



이번엔 위 코드와 다르게 조건문이 뒤로 갔습니다.
반복문을 돌려서 값을 하나 씩 리스트에 넣되, 뒤의 조건식에 충족할 경우에만 넣어라,
이런식으로 적용됩니다.


조건식이 위치에 따라 다른 의미로 적용되니 잘 지켜야 하겠습니다!
(지키지 않으면 Syntax Error를 주구장창 보게 됩니다...)




Set Comprehension, Dict Comprehension



파이썬에서는
set 타입과 Dictionary 타입의 데이터도
컴프리헨션으로 생성할 수 있도록 기능을 제공해주고 있습니다.


Dictionary를 만들 때는 <key : Value> 형태로 넣어야 한다는 것 빼고는
리스트 컴프리헨션과 동일하기 때문에,
샘플 코드로 가볍게 설명하고 넘어가도록 하겠습니다.


먼저 1과 100의 값을 Set에 넣되,
짝수면 1을 넣고, 홀수면 0을 넣도록 코드를 만들어 보겠습니다.


ls = {1 if i%2==0 else 0 for i in range(1, 101)}
print(ls)


{0, 1}


예상은 하셨겠지만, 결과는 0과 1만 들어간 Set 데이터입니다.
50개의 0과 50개의 1이 들어갔겠지만,
중복값을 허용하지 않는  set 특성상 모두 날려버린 경우가 되겠습니다.


다음 코드는
1과 100의 값에서
짝수 면 두 배, 홀수면 음수로 바꾸되
키는 원본, value는 수정한 값인 딕셔너리를 만들어 보겠습니다.


ls = {i:i*2 if i%2==0 else i*-1 for i in range(1, 101)}
print(ls)


{1: -1, 2: 4, 3: -3, 4: 8, 5: -5, 6: 12, 7: -7, 8: 16, 9: -9,...}


코드 자체는 리스트 컴프리헨션 설명할 때 코드랑 비슷하나,
값이 들어가는 부분이 <key : Value> 형태로 들어간 것을 확인할 수 있습니다.




리스트 생성의 성능 비교


이제 이 글이 탄생하게 된 주제에 대해
본격적으로 탐구해보겠습니다.


맨 위의 미디움 글에서 나와 있듯,
리스트를 생성하는 방법은 크게 세 가지가 있습니다.

1) 빈 리스트를 만들고 append 함수로 값 추가 하기

2) 리스트에 값을 입력할 충분한 공간을 만든 다음 대입하기

3) 리스트 컴프리헨션

사실 적은 값들을 리스트로 구축할 때는 세 가지 방법 모두 큰 차이가 없으나,
수많은 값들을 리스트로 구축하게 될 시 뚜렷한 성능 차이가 나타나게 됩니다.


파이썬에서 시간을 측정하는 내장 함수인 time()을 활용해,


거대한 값을 리스트로 만들 때 각각 걸리는 시간을 계산한 후
시간을 비교하는 방식으로 성능을 체크해보겠습니다.


아래 코드는 0부터 9,999,999까지의 값(천만 개의 정수 값)을 리스트에 넣고,
거기에 걸리는 시간을 각 방식 별로 체크해본 코드입니다.


from time import time

# Append
start = time()
append_ls = []
for i in range(0, 10000000):
    append_ls.append(i)
print(f"Append : {time()-start}")

# Substitute
start = time()
sub_ls = [0] * 10000000
for i in range(0, 10000000):
    sub_ls[i] = i
print(f"Sub : {time()-start}")

# Comprehension
start = time()
comp_ls = [i for i in range(0,10000000)]
print(f"Comp : {time()-start}")


Append : 1.1632850170135498
Sub : 1.0767951011657715
Comp : 0.40447998046875



결과를 보면 극명하게 차이가 나는 것을 확인 할 수 있습니다.
append 방식과 대입 방식은 크게 차이가 안 나는 반면,
리스트 컴프리헨션 방식은 위의 두 방식과 비교했을 때
거의 두 배 이상의 효율을 보여주는 것을 확인할 수 있습니다.


제대로 리스트를 만든 거 맞냐,
리스트 컴프리헨션 만들 때 숫자 몇 개를 빼먹은 게 아니냐
라는 의심이 들어,
리스트를 비교하는 코드도 만들어 확인해봤습니다.


print(append_ls == sub_ls)
print(append_ls == comp_ls)
print(sub_ls == comp_ls)


True
True
True



전부 같다고 나오는 것을 보면
리스트 생성은 제대로 된 것 같습니다.


물론 결과 값은 컴퓨터의 성능에 따라 차이가 있을 수 있고(맥북 에어야 미안해 ...)
어떤 값을 넣느냐에 따라 또 시간이 달라질 수 있는데,


적어도 리스트 컴프리헨션이 append 방식보다,
그리고 미리 리스트를 만들고 값을 대입하는 방식보다
성능이 더 뛰어나게 나오는 것은 변함이 없었습니다.


다음 부터 코드를 설계할 때는
최대한 리스트 컴프리헨션을 활용하는 방식으로 작성 해야겠습니다.






지금까지 리스트 컴프리헨션 사용법 및 성능에 대해 알아보았습니다.


리스트 컴프리헨션이 단순 코드 자랑 용으로 쓰는 줄만 알았는데,
코드의 성능 개선을 위해 사용하는 줄은 꿈에도 몰랐었네요.


다음 번에도 이렇게 공유하면 괜찮겠다 싶은 코딩 기법이나
번뜩이는 파이썬 아이디어가 있을 때 다시 돌아오겠습니다.


읽어주셔서 감사합니다.

댓글