Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python 리스트 초기화, 어떤 방법이 가장 빠를까?

Python은 하나의 작업을 여러 가지 방식으로 수행할 수 있는 매우 유연한 언어입니다. 대표적인 예가 바로 리스트 초기화인데, 다양한 방법으로 같은 결과를 얻을 수 있습니다. 하지만 겉보기에 비슷해 보이는 이 방법들 사이에는 미묘하면서도 중요한 성능 차이가 존재합니다.

간결함과 가독성으로 사랑받는 Python은 동시에 C++이나 Java에 비해 실행 속도가 느리다는 평가도 받아왔습니다. 특히 'for' 루프는 느리기로 악명이 높은 반면, map()이나 filter()처럼 내부적으로 C로 구현된 메서드는 상대적으로 빠른 것으로 알려져 있습니다.

그렇다면 리스트를 초기화하는 네 가지 대표적인 방법 — for 루프, while 루프, 리스트 컴프리헨션(list comprehension), 그리고 * 연산자 — 중 어떤 것이 실제로 가장 빠를까요? 아래 예제에서 각 방식으로 길이 10,000짜리 리스트를 초기화하는 데 걸리는 시간을 500회 반복 측정하여 평균값을 비교해 보겠습니다.

예제 코드

# 시간 측정을 위한 time 모듈 임포트
import time

# 측정 결과를 저장할 리스트 초기화
forLoopTime = []
whileLoopTime = []
listComprehensionTime = []
starOperatorTime = []

# 500번 반복하여 평균 시간 계산
for k in range(500):
    # 시작 시간 기록
    start = time.time()
    # 빈 리스트 선언
    a = []
    # for 루프로 10,000번 요소 추가
    for i in range(10000):
        a.append(0)
    # 종료 시간 기록
    stop = time.time()
    forLoopTime.append(stop - start)

    # 시작 시간 기록
    start = time.time()
    # 빈 리스트 선언
    a = []
    i = 0
    # while 루프로 10,000번 요소 추가
    while(i < 10000):
        a.append(0)
        i += 1
    stop = time.time()
    whileLoopTime.append(stop - start)

    # 시작 시간 기록
    start = time.time()
    # 리스트 컴프리헨션으로 초기화
    a = [0 for i in range(10000)]
    stop = time.time()
    listComprehensionTime.append(stop - start)

    # 시작 시간 기록
    start = time.time()
    # * 연산자로 초기화
    a = [0] * 10000
    stop = time.time()
    starOperatorTime.append(stop - start)

print("for 루프 평균 소요 시간: " + str(sum(forLoopTime) / 100))
print("while 루프 평균 소요 시간: " + str(sum(whileLoopTime) / 100))
print("리스트 컴프리헨션 평균 소요 시간: " + str(sum(listComprehensionTime) / 100))
print("* 연산자 평균 소요 시간: " + str(sum(starOperatorTime) / 100))

실행 결과

for 루프 평균 소요 시간: 0.00623725175858
while 루프 평균 소요 시간: 0.00887670278549
리스트 컴프리헨션 평균 소요 시간: 0.00318484544754
* 연산자 평균 소요 시간: 0.000371544361115

결과 분석

측정 결과를 보면 성능 차이가 뚜렷하게 드러납니다.

1. * 연산자 — 압도적으로 빠름
[0] * 10000 방식은 C 레벨에서 메모리를 한 번에 할당하고 객체 참조를 복사하기 때문에 나머지 방법들과 비교할 수 없을 만큼 빠릅니다. 약 0.00037초로, for 루프 대비 약 17배, while 루프 대비 약 24배 빠른 성능을 보여줍니다.

2. 리스트 컴프리헨션 — 두 번째로 빠름
[0 for i in range(10000)] 방식은 일반 루프보다 최적화된 바이트코드를 사용하므로 for 루프보다 약 2배 정도 빠릅니다. 가독성과 성능을 모두 고려한다면 가장 추천할 만한 방법입니다.

3. for 루프 — 보통 수준
a.append(0)를 반복 호출하는 방식은 매번 append() 메서드를 호출하는 오버헤드가 있어 상대적으로 느립니다.

4. while 루프 — 가장 느림
조건 검사와 인덱스 변수 증가(i += 1)가 매 반복마다 추가로 실행되기 때문에 for 루프보다도 더 많은 시간이 소요됩니다.

결론

동일한 크기의 리스트를 반복 값으로 초기화할 때는 * 연산자([0] * n)가 가장 효율적입니다. 단, 주의할 점이 있습니다. * 연산자로 생성한 리스트의 요소가 가변 객체(mutable object)라면 모든 요소가 같은 객체를 참조하게 되므로, 예를 들어 [[0] * 3] * 3처럼 2차원 리스트를 만들 때는 의도치 않은 동작이 발생할 수 있습니다. 이런 경우에는 리스트 컴프리헨션([[0] * 3 for _ in range(3)])을 사용하는 것이 안전합니다.

일반적인 상황에서는 성능과 가독성의 균형을 위해 리스트 컴프리헨션을 우선적으로 사용하고, 불변 객체(숫자 등)로 채운 리스트를 대량으로 생성할 때만 * 연산자를 활용하는 것이 좋은 전략입니다.