"파이썬이 너무 느리다", "파이썬의 성능이 좋지 않다"는 말을 자주 듣게 됩니다. 하지만 몇 가지 프로그래밍 트릭을 활용하면 파이썬의 속도를 획기적으로 높일 수 있습니다.
지금 바로 파이썬의 성능을 향상시킬 수 있는 9가지 팁을 살펴보세요!
문자열 연결 팁
처리 대기 중인 문자열이 많은 경우 문자열 연결은 파이썬의 병목 현상이 될 수 있습니다.
일반적으로 파이썬에는 두 가지 유형의 문자열 접합이 있습니다:
- join() 함수를 사용하여 문자열 목록을 하나의 문자열로 병합합니다.
- 또는 += 기호를 사용하여 각 문자열을 다음과 같이 추가합니다.
그렇다면 어느 쪽이 더 빠를까요? 함께 살펴보겠습니다.
mylist = ["Yang", "Zhou", "is", "writing"]
# Using '+'
def concat_plus():
result = ""
for word in mylist:
result += word + " "
return result
# Using 'join()'
def concat_join():
return " ".join(mylist)
# Directly concatenation without the list
def concat_directly():
return "Yang" + "Zhou" + "is" + "writing"
import timeit
print(timeit.timeit(concat_plus, number=10000))
# 0.002738415962085128
print(timeit.timeit(concat_join, number=10000))
# 0.0008482920238748193
print(timeit.timeit(concat_directly, number=10000))
# 0.00021425005979835987
위에서 볼 수 있듯이 join() 메서드는 문자열 목록을 연결할 때 for 루프에서 문자열을 하나씩 추가하는 것보다 빠릅니다.
그 이유는 간단합니다. 한편으로 문자열은 파이썬에서 불변 데이터이므로 += 연산을 할 때마다 새 문자열이 생성되고 이전 문자열의 복사본이 생성되어 오버헤드가 매우 높아질 수 있습니다.
반면에 .join() 메서드는 문자열 시퀀스를 조인하는 데 특별히 최적화되어 있습니다. 이 메서드는 결과 문자열의 크기를 미리 계산한 다음 한 번에 모두 빌드합니다. 따라서 루프에서 += 연산과 관련된 오버헤드를 피할 수 있으므로 속도가 빨라집니다.
그러나 실제로는 +로 문자열을 직접 연결하는 것이 가장 빠른 것으로 나타났습니다:
- 파이썬 인터프리터는 컴파일 시 문자열을 단일 문자열로 변환하여 문자열 연결을 최적화할 수 있습니다. 루프 반복이나 함수 호출이 없기 때문에 매우 효율적인 작업입니다.
- 모든 문자열은 컴파일 시점에 알 수 있으므로 파이썬은 루프의 런타임 조인이나 .join() 메서드를 최적화하는 것보다 훨씬 빠르게 이 작업을 수행할 수 있습니다.
간단히 말해, 문자열 목록을 조인해야 하는 경우 join()을 선택하고, 문자열을 직접 조인하려면 +를 사용하면 됩니다.
목록 작성을 위한 팁
파이썬에서 목록을 만드는 두 가지 일반적인 방법은 다음과 같습니다:
- 使用函数 list()
- [] 직접 사용
다음 두 가지 방법의 성능을 살펴보세요.
import timeit
print(timeit.timeit('[]', number=10 ** 7))
# 0.1368238340364769
print(timeit.timeit(list, number=10 ** 7))
# 0.2958830420393497
결과는 목록() 함수를 실행하는 것이 []를 직접 사용하는 것보다 느리다는 것을 보여줍니다.
이는 [] 리터럴 구문이고 list()가 생성자 호출이기 때문입니다. 함수를 호출하는 데 시간이 더 걸리는 것은 의심의 여지가 없습니다.
마찬가지로 사전을 만들 때는 dict() 대신 {}를 사용해야 합니다.
회원 관계 테스트를 위한 팁
멤버십 테스트의 성능은 기본 데이터 구조에 따라 크게 달라집니다.
import timeit
large_dataset = range(100000)
search_element = 2077
large_list = list(large_dataset)
large_set = set(large_dataset)
def list_membership_test():
return search_element in large_list
def set_membership_test():
return search_element in large_set
print(timeit.timeit(list_membership_test, number=1000))
# 0.01112208398990333
print(timeit.timeit(set_membership_test, number=1000))
# 3.27499583363533e-05
위의 코드에서 볼 수 있듯이 컬렉션의 멤버십 테스트는 목록의 멤버십 테스트보다 훨씬 빠릅니다.
왜 그럴까요?
- 파이썬 목록에서 멤버십 테스트는 원하는 요소를 찾거나 목록의 끝에 도달할 때까지 각 요소를 순회하는 방식으로 수행됩니다. 따라서 이 작업의 시간 복잡도는 O입니다.
- Python의 컬렉션은 해시 테이블로 구현됩니다. 멤버십을 확인할 때 Python은 평균적으로 시간 복잡도가 O인 해싱 메커니즘을 사용합니다.
여기서 소개하는 팁의 핵심은 프로그램을 작성할 때 기본 데이터 구조를 신중하게 고려하는 것입니다. 올바른 데이터 구조를 활용하면 코드 속도를 크게 높일 수 있습니다.
루프 대신 파생상품 사용
파이썬에는 리스트, 딕셔너리, 집합, 제너레이터의 네 가지 파생형이 있습니다. 이들은 상대 데이터 구조를 만드는 데 더 깔끔한 구문을 제공할 뿐만 아니라 루프에 사용하는 것보다 더 나은 성능을 제공합니다.
파이썬의 C 구현에 최적화되어 있기 때문입니다.
import timeit
def generate_squares_for_loop():
squares = []
for i in range(1000):
squares.append(i * i)
return squares
def generate_squares_comprehension():
return [i * i for i in range(1000)]
print(timeit.timeit(generate_squares_for_loop, number=10000))
# 0.2797503340989351
print(timeit.timeit(generate_squares_comprehension, number=10000))
# 0.2364629579242319
위 코드는 목록 파생형과 루프에 대한 간단한 속도 비교입니다. 결과에서 볼 수 있듯이 목록 파생이 더 빠릅니다.
로컬 변수에 대한 빠른 액세스
파이썬에서는 전역 변수나 객체의 프로퍼티에 액세스하는 것보다 로컬 변수에 액세스하는 것이 더 빠릅니다.
import timeit
class Example:
def __init__(self):
self.value = 0
obj = Example()
def test_dot_notation():
for _ in range(1000):
obj.value += 1
def test_local_variable():
value = obj.value
for _ in range(1000):
value += 1
obj.value = value
print(timeit.timeit(test_dot_notation, number=1000))
# 0.036605041939765215
print(timeit.timeit(test_local_variable, number=1000))
# 0.024470250005833805
원리는 간단합니다. 함수를 컴파일할 때 함수 내부의 로컬 변수는 알 수 있지만 다른 외부 변수는 검색하는 데 시간이 걸립니다.
기본 제공 모듈 및 라이브러리 우선순위 지정하기
파이썬에 대해 논의할 때는 일반적으로 파이썬 언어의 기본 구현이자 가장 널리 사용되는 구현이기 때문에 CPython을 언급합니다.
대부분의 내장 모듈과 라이브러리가 훨씬 빠르고 낮은 수준의 언어인 C로 작성되어 있으므로 내장 라이브러리를 활용하고 라이브러리 중복을 피해야 합니다.
import timeit
import random
from collections import Counter
def count_frequency_custom(lst):
frequency = {}
for item in lst:
if item in frequency:
frequency[item] += 1
else:
frequency[item] = 1
return frequency
def count_frequency_builtin(lst):
return Counter(lst)
large_list = [random.randint(0, 100) for _ in range(1000)]
print(timeit.timeit(lambda: count_frequency_custom(large_list), number=100))
# 0.005160166998393834
print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100))
# 0.002444291952997446
위 프로그램은 목록에 있는 요소의 빈도를 계산하는 두 가지 방법을 비교합니다. 보시다시피 컬렉션 모듈에 내장된 카운터를 사용하는 것이 루프에 대해 직접 작성하는 것보다 빠르고 깔끔하며 더 낫습니다.
캐시 데코레이터 사용
캐싱은 중복 계산을 방지하고 프로그램 속도를 높이기 위한 일반적인 기술입니다.
다행히도 대부분의 경우 파이썬은 기본 제공 데코레이터인 @functools.cache를 제공하기 때문에 직접 캐시 처리 코드를 작성할 필요가 없습니다.
예를 들어 다음 코드는 두 개의 피보나치 수 생성기 함수를 실행하는데, 하나는 캐시 데코레이터를 사용하고 다른 하나는 캐시 데코레이터를 사용하지 않습니다:
import timeit
import functools
def fibonacci(n):
if n in (0, 1):
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@functools.cache
def fibonacci_cached(n):
if n in (0, 1):
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
# Test the execution time of each function
print(timeit.timeit(lambda: fibonacci(30), number=1))
# 0.09499712497927248
print(timeit.timeit(lambda: fibonacci_cached(30), number=1))
# 6.458023563027382e-06
functools.cache 데코레이터가 코드를 더 빠르게 실행하는 방법을 확인할 수 있습니다.
캐시 버전은 이전 계산 결과를 캐시하기 때문에 훨씬 빠릅니다. 따라서 각 피보나치 수를 한 번만 계산하고 캐시에서 동일한 인수를 사용하여 후속 호출을 검색합니다.
while 1 VS while True
무한 동안 루프를 만들려면 동안 True 또는 동안 1 을 사용합니다.
성능 차이는 일반적으로 무시할 수 있는 수준입니다. 하지만 흥미롭게도 1번이 약간 더 빠릅니다.
이는 1 리터럴이기 때문이지만 True는 파이썬의 전역 범위에서 조회해야 하는 전역 이름입니다. 따라서 1의 오버헤드는 작습니다.
import timeit
def loop_with_true():
i = 0
while True:
if i >= 1000:
break
i += 1
def loop_with_one():
i = 0
while 1:
if i >= 1000:
break
i += 1
print(timeit.timeit(loop_with_true, number=10000))
# 0.1733035419601947
print(timeit.timeit(loop_with_one, number=10000))
# 0.16412191605195403
보시다시피 1이 약간 더 빠른 것은 사실입니다.
하지만 최신 파이썬 인터프리터는 고도로 최적화되어 있으며 이 차이는 대개 무시할 수 있는 수준입니다. 따라서 이 미미한 차이에 대해 걱정하지 마세요. 참이 동안 1보다 더 잘 읽힌다는 것은 말할 것도 없습니다.
온디맨드 파이썬 모듈 가져오기
Python 스크립트를 시작할 때 모든 모듈을 가져오는 것은 누구나 하는 작업인 것 같지만 실제로 모든 모듈을 가져올 필요는 없습니다. 모듈이 너무 크다면 필요에 따라 모듈을 임포트하는 것이 좋습니다.
def my_function():
import heavy_module
# rest of the function
위의 코드에서 볼 수 있듯이 heavy_module은 함수에서 가져옵니다. 이것은 "지연된 로딩" 아이디어로, my_function이 호출될 때만 모듈을 가져옵니다.
이 접근 방식의 장점은 스크립트 실행 중에 my_function이 호출되지 않으면 heavy_module이 로드되지 않아 리소스를 절약하고 스크립트 시작 시간을 단축할 수 있다는 점입니다.
기사 재인쇄 출처:
원본 문서 링크:
체험:





