Python 성능 최적화 필수 팁
Emily Parker
Product Engineer · Leapcell

##x Python 코드 성능 최적화 종합 가이드
Python은 동적 타입의 인터프리터 언어로서 정적 타입의 컴파일 언어인 C보다 실행 속도가 느릴 수 있습니다. 그러나 특정 기술과 전략을 통해 Python 코드의 성능을 크게 향상시킬 수 있습니다.
이 기사에서는 Python 코드를 최적화하여 더 빠르고 효율적으로 실행하는 방법을 살펴봅니다. Python의 timeit
모듈을 사용하여 코드의 실행 시간을 정확하게 측정합니다.
참고: 기본적으로 timeit
모듈은 측정 결과의 정확성과 안정성을 보장하기 위해 코드 실행을 백만 번 반복합니다.
def print_hi(name): print(f'Hi, {name}') if __name__ == '__main__': # print_hi('leapcell') 메서드 실행 t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")') t.timeit()
Python 스크립트 실행 시간 계산 방법
time
모듈에서 time.perf_counter()
는 짧은 시간 간격을 측정하는 데 적합한 고정밀 타이머를 제공합니다. 예를 들어:
import time # 프로그램 시작 시간 기록 start_time = time.perf_counter() # 코드 로직 #... # 프로그램 종료 시간 기록 end_time = time.perf_counter() # 프로그램 실행 시간 계산 run_time = end_time - start_time print(f"프로그램 실행 시간: {run_time} 초")
I. I/O 집약적 작업
I/O 집약적 작업(Input/Output Intensive Operation)은 실행 시간의 대부분을 입력/출력 작업이 완료되기를 기다리는 데 소비하는 프로그램 또는 작업을 의미합니다. I/O 작업에는 디스크에서 데이터 읽기, 디스크에 데이터 쓰기, 네트워크 통신 등이 포함됩니다. 이러한 작업은 일반적으로 하드웨어 장치와 관련되어 있으므로 실행 속도는 하드웨어 성능과 I/O 대역폭에 의해 제한됩니다.
그 특징은 다음과 같습니다.
- 대기 시간: 프로그램이 I/O 작업을 실행할 때 외부 장치에서 메모리로 또는 메모리에서 외부 장치로 데이터가 전송될 때까지 기다려야 하는 경우가 많으며, 이로 인해 프로그램 실행이 차단될 수 있습니다.
- CPU 활용 효율: I/O 작업의 대기 시간으로 인해 CPU가 이 기간 동안 유휴 상태가 되어 CPU 활용률이 낮아질 수 있습니다.
- 성능 병목 현상: I/O 작업 속도는 특히 데이터 양이 많거나 전송 속도가 느린 경우 프로그램 성능의 병목 현상이 되는 경우가 많습니다.
예를 들어, I/O 집약적 작업인 print
를 사용하여 백만 번 실행하는 경우:
import time import timeit def print_hi(name): print(f'Hi, {name}') return if __name__ == '__main__': start_time = time.perf_counter() # print_hi('leapcell') 메서드 실행 t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")') t.timeit() end_time = time.perf_counter() run_time = end_time - start_time print(f"프로그램 실행 시간: {run_time} 초")
실행 결과는 3초입니다.
I/O 작업을 사용하지 않고 메서드를 실행하는 경우, 즉 print()
를 사용하지 않고 print_hi('xxxx')
빈 메서드를 호출하면 프로그램이 훨씬 빨라집니다.
def print_hi(name): # print(f'Hi, {name}') return
I/O 집약적 작업에 대한 최적화 방법
파일 읽기 및 쓰기와 같이 코드에 필요한 경우 다음 방법을 사용하여 효율성을 향상시킬 수 있습니다.
- 비동기 I/O:
asyncio
와 같은 비동기 프로그래밍 모델을 사용하면 프로그램이 I/O 작업이 완료되기를 기다리는 동안 다른 작업을 계속 실행하여 CPU 활용률을 높일 수 있습니다. - 버퍼링: 버퍼를 사용하여 데이터를 임시로 저장하고 I/O 작업 빈도를 줄입니다.
- 병렬 처리: 여러 I/O 작업을 병렬로 실행하여 전체 데이터 처리 속도를 높입니다.
- 데이터 구조 최적화: 적절한 데이터 구조를 선택하여 데이터 읽기 및 쓰기 횟수를 줄입니다.
II. 생성기를 사용하여 목록 및 사전 생성
Python 2.7 및 이후 버전에서는 목록, 사전 및 집합 생성기에 대한 개선 사항이 도입되어 데이터 구조 구성 프로세스가 더 간결하고 효율적입니다.
1. 전통적인 방법
def fun1(): list=[] for i in range(100): list.append(i) if __name__ == '__main__': start_time = time.perf_counter() t = timeit.Timer(setup='from __main__ import fun1', stmt='fun1()') t.timeit() end_time = time.perf_counter() run_time = end_time - start_time print(f"프로그램 실행 시간: {run_time} 초") # 출력 결과: 프로그램 실행 시간: 3.363 초
2. 생성기로 코드 최적화
참고: 다음 콘텐츠의 편의를 위해 main 함수 main
의 코드 부분은 생략됩니다.
def fun1(): list=[ i for i in range(100)] # 프로그램 실행 시간: 2.094 초
위의 파생 공식 프로그램에서 볼 수 있듯이 더 간결하고 이해하기 쉬울 뿐만 아니라 더 빠릅니다. 이로 인해 이 방법은 목록 및 루프 생성에 선호되는 방법이 됩니다.
III. 문자열 연결 방지, join()
사용
join()
은 Python에서 시퀀스의 요소를 문자열로 연결(또는 결합)하는 데 사용되는 문자열 메서드로, 일반적으로 특정 구분 기호를 사용합니다. 그 장점은 일반적으로 다음과 같습니다.
- 높은 효율성:
join()
은 특히 많은 수의 문자열을 처리할 때 문자열을 연결하는 효율적인 방법입니다. 일반적으로+
연산자 또는%
서식 지정을 사용하는 것보다 빠릅니다. 많은 수의 문자열을 연결할 때join()
메서드는 일반적으로 하나씩 연결하는 것보다 더 많은 메모리를 절약합니다. - 간결성:
join()
은 코드를 더 간결하게 만들고 반복적인 문자열 연결 작업을 방지합니다. - 유연성: 모든 문자열을 구분 기호로 지정할 수 있으므로 문자열 결합에 큰 유연성을 제공합니다.
- 넓은 응용: 문자열뿐만 아니라 목록 및 튜플과 같은 시퀀스 유형에도 사용할 수 있습니다. 요소를 문자열로 변환할 수 있는 한 가능합니다.
예를 들어:
def fun1(): obj=['hello','this','is','leapcell','!'] s="" for i in obj: s+=i # 프로그램 실행 시간: 0.35186 초
join()
을 사용하여 문자열 연결을 달성합니다.
def fun1(): obj=['hello','this','is','leapcell','!'] "".join(obj) # 프로그램 실행 시간: 0.1822 초
join()
을 사용하면 함수의 실행 시간이 0.35초에서 0.18초로 줄어듭니다.
IV. 루프 대신 Map
사용
대부분의 시나리오에서 기존의 for
루프는 더 효율적인 map()
함수로 대체할 수 있습니다. map()
은 Python에 내장된 고차 함수로, 목록, 튜플 또는 문자열과 같은 다양한 반복 가능한 데이터 구조에 지정된 함수를 적용할 수 있습니다. map()
을 사용하는 주요 장점은 명시적인 루프 코드를 작성하지 않고도 더 간결하고 효율적인 데이터 처리 방법을 제공한다는 것입니다.
전통적인 루프 방법
def fun1(): arr=["hello", "this", "is", "leapcell", "!"] new = [] for i in arr: new.append(i) # 프로그램 실행 시간: 0.3067 초
map()
함수를 사용하여 동일한 기능 수행
def fun2(x): return x def fun1(): arr=["hello", "this", "is", "leapcell", "!"] map(fun2,arr) # 프로그램 실행 시간: 0.1875 초
비교 후 map()
을 사용하면 시간이 거의 절반으로 줄어들고 실행 효율성이 크게 향상됩니다.
V. 올바른 데이터 구조 선택
적절한 데이터 구조를 선택하는 것은 Python 코드의 실행 효율성을 향상시키는 데 중요합니다. 다양한 데이터 구조는 특정 작업에 최적화되어 있습니다. 합리적인 선택은 데이터 검색, 추가 및 제거를 가속화하여 프로그램의 전체 작업 효율성을 향상시킬 수 있습니다.
예를 들어, 컨테이너의 요소를 판단할 때 사전의 조회 효율성은 목록보다 높지만 이는 많은 양의 데이터의 경우입니다. 적은 양의 데이터의 경우에는 그 반대입니다.
적은 양의 데이터로 테스트
def fun1(): arr=["hello", "this", "is", "leapcell", "!"] 'hello' in arr 'my' in arr # 프로그램 실행 시간: 0.1127 초 def fun1(): arr={"hello", "this", "is", "leapcell", "!"} 'hello' in arr 'my' in arr # 프로그램 실행 시간: 0.1702 초
numpy
를 사용하여 100개의 정수 임의로 생성
import numpy as np def fun1(): nums = {i for i in np.random.randint(100, size=100)} 1 in nums # 프로그램 실행 시간: 14.28 초 def fun1(): nums = {i for i in np.random.randint(100, size=100)} 1 in nums # 프로그램 실행 시간: 13.53 초
적은 양의 데이터의 경우 list
의 실행 효율성이 dict
보다 높지만 많은 양의 데이터의 경우에는 dict
의 효율성이 list
보다 높습니다.
추가 및 삭제 작업이 빈번하고 추가 및 삭제되는 요소 수가 많은 경우 list
의 효율성이 높지 않습니다. 이때 collections.deque
를 고려해야 합니다. collections.deque
는 스택과 큐의 특징을 모두 가지고 있으며 양쪽 끝에서 $O(1)$의 복잡도로 삽입 및 삭제 작업을 수행할 수 있는 양방향 큐입니다.
collections.deque
사용법
from collections import deque def fun1(): arr=deque()# 빈 데크 생성 for i in range(1000000): arr.append(i) # 프로그램 실행 시간: 0.0558 초 def fun1(): arr=[] for i in range(1000000): arr.append(i) # 프로그램 실행 시간: 0.06077 초
list
의 조회 작업은 또한 매우 시간이 많이 걸립니다. list
에서 특정 요소를 자주 조회하거나 정렬된 방식으로 이러한 요소에 액세스해야 하는 경우 bisect
를 사용하여 list
객체의 순서를 유지하고 이진 검색을 수행하여 조회 효율성을 높일 수 있습니다.
VI. 불필요한 함수 호출 방지
Python 프로그래밍에서 함수 호출 수를 최적화하는 것은 코드 효율성을 향상시키는 데 중요합니다. 과도한 함수 호출은 오버헤드를 증가시킬 뿐만 아니라 추가 메모리를 소비하여 프로그램 실행 속도를 늦출 수도 있습니다. 성능을 향상시키려면 불필요한 함수 호출을 줄이고 여러 작업을 하나로 결합하여 실행 시간과 리소스 소비를 줄여야 합니다. 이러한 최적화 전략은 더 효율적이고 빠른 코드를 작성하는 데 도움이 됩니다.
VII. 불필요한 import
방지
Python의 import
문은 비교적 빠르지만 각 import
는 모듈을 찾고 (아직 실행되지 않은 경우) 모듈 코드를 실행하고 모듈 객체를 현재 네임스페이스에 넣는 작업을 포함합니다. 이러한 작업에는 모두 일정량의 시간과 메모리가 필요합니다. 불필요하게 모듈을 가져오면 이러한 오버헤드가 증가합니다.
VIII. 전역 변수 사용 방지
import math size=10000 def fun1(): for i in range(size): for j in range(size): z = math.sqrt(i) + math.sqrt(j) # 프로그램 실행 시간: 15.6336 초
많은 프로그래머는 처음에 Python 언어로 몇 가지 간단한 스크립트를 작성합니다. 스크립트를 작성할 때 일반적으로 위의 코드와 같이 전역 변수로 직접 작성하는 데 익숙합니다. 그러나 전역 변수와 지역 변수의 구현 방식이 다르기 때문에 전역 범위에서 정의된 코드는 함수에서 정의된 코드보다 훨씬 느리게 실행됩니다. 스크립트 문을 함수에 넣으면 일반적으로 15% - 30%의 속도 향상을 얻을 수 있습니다.
import math def fun1(): size = 10000 for i in range(size): for j in range(size): z = math.sqrt(i) + math.sqrt(j) # 프로그램 실행 시간: 14.9319 초
IX. 모듈 및 함수 속성 액세스 방지
import math # 권장되지 않음 def fun2(size: int): result = [] for i in range(size): result.append(math.sqrt(i)) return result def fun1(): size = 10000 for _ in range(size): result = fun2(size) # 프로그램 실행 시간: 10.1597 초
.
(속성 액세스 연산자)를 사용할 때마다 __getattribute__()
및 __getattr__()
과 같은 특정 메서드가 트리거됩니다. 이러한 메서드는 사전 작업을 수행하므로 추가 시간 오버헤드가 발생합니다. from import
문을 사용하면 속성 액세스를 제거할 수 있습니다.
from math import sqrt # 권장: 필요한 모듈만 가져오기 def fun2(size: int): result = [] for i in range(size): result.append(sqrt(i)) return result def fun1(): size = 10000 for _ in range(size): result = fun2(size) # 프로그램 실행 시간: 8.9682 초
X. 내부 for
루프에서 계산 줄이기
import math def fun1(): size = 10000 sqrt = math.sqrt for x in range(size): for y in range(size): z = sqrt(x) + sqrt(y) # 프로그램 실행 시간: 14.2634 초
위의 코드에서 sqrt(x)
는 내부 for
루프에 있으며 루프가 실행될 때마다 다시 계산되어 불필요한 시간 오버헤드가 추가됩니다.
import math def fun1(): size = 10000 sqrt = math.sqrt for x in range(size): sqrt_x=sqrt(x) for y in range(size): z = sqrt_x + sqrt(y) # 프로그램 실행 시간: 8.4077 초
Leapcell: Python 앱 호스팅을 위한 최고의 서버리스 플랫폼
마지막으로, Python 애플리케이션 배포를 위한 최고의 플랫폼인 Leapcell을 소개합니다.
1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발하십시오.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청도 없고 요금도 없습니다.
3. 타의 추종을 불허하는 비용 효율성
- 미사용 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 전혀 없습니다. 구축에만 집중하면 됩니다.
Leapcell Twitter: https://x.com/LeapcellHQ