파이썬 코드를 작성하면서 제일 자주 작성하게 되는 코드는 어떤 코드일까? 여러 개가 있겠지만 아래 코드처럼 for-loop 안에서 리스트에 item을 append하는 로직이 후보에서 빠질 수 없을 것이다.
newlist = []
for word in oldlist:
newlist.append(word.upper())
파이썬 공식 문서(https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Loops)에서는 loop를 최적화하는 팁을 알려준다.
Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54
위 속도 비교에서 사용하는 스킬은 단 두 개이다. 점(dots) 제거하기, 지역변수화하기.
이것이 무슨 효과가 있는지 제대로 이해하려면 파이썬이 동작하는 방식을 알아야 한다.
점(dots) 제거하기
사실 이건 검색하면 쉽게 나온다. 맨 처음의 코드를 아래와 같이 바꾸면 된다.
upper = str.upper
newlist = []
append = newlist.append
for word in oldlist:
append(upper(word))
단순히 이런 코드를 짠다고 속도가 체감될 정도로 빨라진다고? 그 원리를 알아보자.
객체에 dot을 붙이는 것은 그 객체의 필드/프로퍼티/메서드에 접근하기 위함이다. 그러니까 newlist.append라는 문장은 newlist의 append를 호출하는 것이다. 사람의 말로 하면 참 쉬우나 슬프게도 파이썬 인터프리터가 저런 문장을 하나 썼다고 한 번에 메서드를 가져오지는 못한다. 여기서 속도 차이가 발생한다.
파이썬의 네임스페이스는 딕셔너리를 활용한다. 파이썬에서 메서드를 호출하면
1. newlist의 멤버에 대해 정의된 네임스페이스 딕셔너리로부터
2. append라는 이름의 멤버를 찾아오는
과정이 필요하다. 그리고 그 과정은 딕셔너리 탐색으로 진행된다.
딕셔너리 탐색이 어떻게 진행되는지는 차치하고(사실 필자도 까먹었다), 기본적으로 dot을 통한 객체 멤버에의 접근이 꽤 큰 overhead를 가지고 있다는 것, 그리고 그런 overhead가 loop 안에서 발생하는 것이 속도 차이의 핵심 원인이다.
개선된 코드에서는 loop 밖에서 어떤 변수가 메서드를 참조하게 하여 한 번에 함수를 호출할 수 있도록 한다. 결국 overhead가 loop 밖에서 단 한 번 발생하는 것이다.
지역변수화
점을 제거하는 것과 유사한 수준의 속도 향상을 가져오는 스킬이다. 그리고 점을 제거하는 것과 거의 유사한 원리로 속도가 개선되는 방식이기도 하다.
방법 자체는 간단하다. 앞서서 작성한 코드를 함수로 만들어 감싸는 것이다.
def func():
upper = str.upper
newlist = []
append = newlist.append
for word in oldlist:
append(upper(word))
return newlist
제목에서 유추할 수 있듯, 모든 것들이 func 함수의 local scope 안에 존재하게 되었다.(그러니까 기존 코드는 global scope에서 실행되었다고 생각하면 된다. 참고로 __name__ == "__main__" 조건을 통한 main scope도 global scope라고 봐야 한다.) 그것만이 변경사항이다. 왜 속도가 빨라지는 걸까?
또 파이썬의 namespace가 등장한다. 사실 이에 대해서는 예전에 포스팅한 바가 있다. (https://yongsyongs.tistory.com/15)
요점만 정리하자면, 파이썬 인터프리터에게 어떤 이름의 개체(여기서는 newlist나 append 같은 대상)로 접근(호출)하는 명령을 내린다면 파이썬은 아래와 같은 순서로 진행한다.
1. 로컬 네임스페이스에서 대상을 찾는다.
2. 없으면 글로벌 네임스페이스에서 대상을 찾는다.
3. 없으면 built-in이나 모듈의 네임스페이스에서 대상을 찾는다.
2, 3번의 경우 평범한 딕셔너리 탐색이다. 앞서서 말한 overhead가 존재한다는 것이다.
local scope에 대한 탐색이 중요하다. 파이썬은 내부적으로 local variable에 대한 접근을 최적화하여 구현하였다. 그렇기에 전역 변수보다 지역 변수에 대한 접근에 압도적으로 빠르다.
결국 전체 코드를 적절한 함수들로 쪼개어 구현한다면 변수에 대한 호출 성능이 향상되고, 전반적인 성능이 향상되는 것이다. 공식 문서에서 말하는 이 스킬들은 이러한 패러다임을 loop문으로 가져왔을 뿐이라고 보면 된다.
잡담
필자는 list-comprehension이 for loop보다 꽤 빠를 것이라 생각했는데 찾아보니 그렇지 못한 것 같다. 결국 list-comprehension은 본문에서 얘기한 팁들을 적용한 loop랑 별 차이 없는 것 같다.
그러나 어디에서 읽기로는 특수한 경우에서 list-comprehension이 map과 같은 built-in funtional functions보다 더 빠를 수 있다고 들었는데... 어디서 찾아야할지 감이 잘 안잡힌다. 이건 나중에 찾아봐야겠다.
'개발 > 파이썬' 카테고리의 다른 글
파이썬의 메서드 결정 순서(Method Resolution Order)와 프로토콜에 의한 일관성 유지 (0) | 2022.12.02 |
---|---|
모션캡쳐 파이썬 구현 (0) | 2022.07.31 |
랜덤이 포함된 로직은 단순히 기능만 테스트해서는 안된다. (0) | 2022.07.10 |
Python 네임스페이스에 대한 이해 (0) | 2022.07.09 |
는 어떤 방법으로 구해야 할까? np.ndarray와 np.matrix의 차이 (0) | 2022.07.09 |