Python은 코드 베이스가 커짐에 따라 성능 문제의 위험도 커집니다. 비효율적인 Python 코드는 답답할 정도로 느릴 수 있으며 종종 병목 현상을 정확히 찾아내기 어려울 수 있음. Python 코드 성능을 최적화하기 위한 몇 가지 유용한 팁과 요령을 살펴보고, 코드를 디버깅하기 전에 먼저 Python 코드의 속도와 효율성을 개선방법을 알아보기.
1. 내장 함수 및 라이브러리
- Python은 복잡한 작업을 효율적으로 수행할 수 있는 다양한 내장 함수와 라이브러리를 제공.
- map()함수를 사용하여 목록의 모든 요소에 함수 적용
- pandas라이브러리를 사용하여 DataFrame의 데이터 조작
# map() 함수를 사용하여 리스트의 각 요소에 함수를 적용하는 예제
lst = [1, 2, 3, 4, 5]
# lambda 함수를 사용하여 리스트의 각 요소를 2배하고 새로운 리스트로 변환
new_lst = list(map(lambda x: x * 2, lst))
# 각 요소가 2배로 곱해진 새 리스트 출력
print(new_lst)
# 출력: [2, 4, 6, 8, 10]
2. 전역 변수 피하기
- 전역 변수는 프로그램 전체에서 액세스 가능하며 해당 값은 프로그램의 모든 부분에서 변경 가능.
- 이로 인해 예기치 않은 결과가 발생하고 프로그램을 디버그하기 더 어려워질 수 있음.
- 대신 가능하면 함수 내에서 지역 변수를 사용하는 것이 좋음.
# 함수 내에서 지역 변수를 사용하는 예제
def add_numbers(num1, num2):
result = num1 + num2 # 지역 변수 result에 두 숫자의 합 저장
return result # 지역 변수 result 반환
# add_numbers 함수의 반환 값을 sum에 저장
sum_result = add_numbers(10, 20)
print(sum_result)
# 출력: 30
3. 제너레이터 사용
- 생성기는 Python에서 반복자를 만드는 메모리 효율적인 방법.
- 한 번에 하나의 값을 생성하고 이전 상태를 유지.
- 메모리에 저장하지 않고도 대규모 데이터 세트를 반복할 수 있음.
- `read_file` 코드 예시
- 파일을 한 줄씩 읽는 함수를 정의.
- 생성기를 사용하여 읽을 때 각 줄을 생성.
- `with` 명령문을 사용하여 파일이 완료되었을 때 제대로 닫히는지 확인.
- 파일 이름을 인수로 사용하여 함수를 호출하면 생성기 객체 반환.
- `for` 루프를 사용하여 생성기가 반환한 각 줄을 반복하고 콘솔에 인쇄.
# 큰 데이터셋을 반복하여 읽기 위해 제너레이터 사용 예제
def read_file(filename):
with open(filename) as file:
for line in file:
yield line.strip() # 파일의 각 라인을 읽고 양쪽 공백을 제거한 후 하나씩 반환
# 'data.txt' 파일에서 read_file 함수를 사용하여 각 라인을 출력
for line in read_file("data.txt"):
print(line)
4. Set 과 Dictionary 컨프리핸션 사용
- 집합 및 사전 이해는 목록 이해와 유사하나 목록 대신 집합 및 사전을 생성.
- Python에서 집합과 사전을 만드는 간결하고 효율적인 방법.
# SET Comprehension을 사용하여 새로운 집합 생성하는 예제
lst = [1, 2, 3, 4, 5]
new_set = {x * 2 for x in lst} # 리스트의 각 요소를 2배하여 새로운 집합 생성
# 리스트의 각 요소에 2를 곱하여 새로운 집합 생성
print(new_set)
# 출력: {2, 4, 6, 8, 10}
# Dictionary Comprehension을 사용하여 새로운 딕셔너리 생성하는 예제
lst = [("apple", 2), ("banana", 3), ("orange", 4)]
new_dict = {k: v for k, v in lst} # 튜플의 리스트를 딕셔너리로 변환
# 튜플의 리스트를 딕셔너리로 변환
print(new_dict)
# 출력: {'apple': 2, 'banana': 3, 'orange': 4}
5. 작업에 가장 효율적인 데이터 구조 사용
- 효율적인 데이터 구조는 다양한 작업에 대한 시간 복잡도를 최적화하여 Python 코드 성능을 향상.
- 최적의 데이터 구조 선택은 코드 속도에 큰 영향을 미칠 수 있음.
- 예: 목록을 사용한 대기열 데이터 구조 구현은 더 큰 목록에서 느릴 수 있음.
- 대신, collections.deque 개체를 사용하여 대기열을 구현하면 성능 향상 가능.
- 숫자 목록의 최대값과 최소값을 검색할 때 올바른 데이터 구조 선택은 코드 성능을 향상시키는 좋은 예시.
# 리스트를 사용하여 최대 및 최소 값을 찾는 방법
numbers = [5, 10, 2, 8, 1]
max_val = max(numbers) # 리스트에서 최대값 찾기
min_val = min(numbers) # 리스트에서 최소값 찾기
# 힙(heap)을 사용하여 최대 및 최소 값을 찾는 방법
import heapq # heapq 모듈 가져오기
numbers = [5, 10, 2, 8, 1]
max_val = heapq.nlargest(1, numbers)[0] # 힙을 사용하여 리스트에서 최대값 찾기
min_val = heapq.nsmallest(1, numbers)[0] # 힙을 사용하여 리스트에서 최소값 찾기
6. 데코레이터를 사용하여 코드 단순화
- 데코레이터는 Python에서 함수나 클래스의 동작을 수정하는 강력한 도구.
- 다른 기능을 감싸고 동작을 수정하는 본질적인 기능.
- 코드를 단순화하고 성능을 향상시키는 데 사용 가능.
예시코드) 잠재적 오류 처리를 위한 래퍼 함수와 데코레이터 사용
- 잠재적 오류를 처리하는 래퍼 함수 포함
- 데코레이터로서 래퍼 함수 활용
- "potentially_error_prone_function" 함수가 동일한 이름으로 두 번 정의
- 데코레이터 적용으로 두 번째 함수 정의가 첫 번째 함수를 덮어쓰기
- 특정 조건 충족 시 오류 발생하는 함수에 대한 오류 처리 제공
def potentially_error_prone_function(): # 잠재적으로 오류가 발생할 수 있는 함수 정의
if some_condition: # 특정 조건을 검사
raise ValueError("Oops!") # 조건이 참이면 ValueError 예외를 발생
return "Success" # 조건이 거짓이면 "Success" 문자열 반환
def error_handling_wrapper(func): # 오류 처리 래퍼 함수 정의
def wrapper(): # 래퍼 함수 내부
try:
result = func() # 인수로 전달된 func를 실행하고 결과 저장
except Exception as e: # 예외 발생 시
print("An error occurred:", e) # 오류 메시지 출력
return None # None 반환
return result # 오류가 없으면 결과 반환
return wrapper # 래퍼 함수 반환
@error_handling_wrapper # 데코레이터 구문 사용
def potentially_error_prone_function(): # 위에서 정의된 함수와 이름이 같음
if some_condition:
raise ValueError("Oops!") # ValueError 예외 발생 조건 동일
return "Success" # "Success" 문자열 반환
7. 문자열 조인을 위해 조인 방법 사용
- 루프에서 '+' 연산자를 사용하여 문자열을 연결하는 것보다 `join` 메서드를 사용하여 문자열을 결합.
- 이 방법은 연산이 더 빠르고 메모리 효율적임.
# Slow way
words = ['Hello', 'welcome', 'to', 'the', 'blog'] # 단어 리스트 정의
result = ''
for word in words: # 단어 리스트를 반복
result += word + ' ' # 각 단어를 결과 문자열에 추가하고 뒤에 공백을 붙임
# Fast way
wayresult = ' '.join(words) # ' '를 사용하여 단어 리스트를 결합하여 문자열로 만듦. 훨씬 효율적인 방법
print(wayresult) # 'Hello welcome to the blog'을 출력
## 'Hello welcome to the blog' - 출력 결과
8. 가능하면 try-except 블록을 사용하지 말것.
- Try-except 블록은 코드 속도를 저하시킬 수 있으므로 피하는 것이 좋음.
- 잘못된 코드 예: 분모가 0인 경우를 처리하기 위해 try-except 블록 사용. 특히 자주 호출되는 함수에 있을 경우 비효율적일 수 있음.
- 좋은 코드 예: if-else 문을 사용하여 나누기 수행 전에 분모가 0인지 확인. 분모가 0인 경우를 처리하는 더 빠르고 효율적인 방법임.
# Bad Code
def divide(a, b):
try:
result = a / b # 분자를 분모로 나누려고 시도합니다.
except ZeroDivisionError: # 분모가 0인 경우 ZeroDivisionError 예외가 발생합니다.
result = None # 예외가 발생하면 결과를 None으로 설정합니다.
return result # 계산된 결과를 반환합니다. 분모가 0이었다면 None을 반환합니다.
# Good Code
def divide(a, b):
if b == 0: # 분모가 0인지 확인합니다.
return None # 분모가 0이면 None을 즉시 반환합니다.
else:
return a / b # 그렇지 않은 경우, 분자를 분모로 나눈 결과를 반환합니다.
9. Python 디버거(pdb)를 사용하여 코드 최적화
- Python 디버거를 사용하면 코드를 단계별로 실행하고 성능 문제를 식별할 수 있음. 병목 현상을 찾고 코드를 최적화하는데 사용.
- 예제 코드 설명:
- `pdb.set_trace()` 문은 각 반복에서 디버거를 중단하기 위해 루프 내부에 배치.
- 각 반복에서 변수와 코드 흐름을 검사하고 코드의 성능 병목 현상이나 버그를 식별하는데 사용.
import pdb # 파이썬 디버거 모듈을 임포트합니다.
def calculate_sum(n):
total = 0 # 총합을 저장할 변수를 초기화합니다.
for i in range(n): # 0부터 n-1까지의 범위를 순회합니다.
pdb.set_trace() # 디버거의 중단점을 설정합니다. 코드 실행이 이 지점에 도달하면, 디버거가 실행을 중지하고 사용자 입력을 기다립니다.
total += i # 현재의 i 값을 total에 더합니다.
return total # 총합을 반환합니다.
calculate_sum(5) # 함수를 5라는 인자와 함께 호출합니다. 결과로 0 + 1 + 2 + 3 + 4의 합인 10을 반환
댓글