지난 포스트에서 살펴봤듯이 파이썬에서는 변수, 함수, 모듈이 사용될 때 그 객체를 어디서 찾을지 결정하는 Local - Global - Built-in 계층이 존재합니다. 가장 먼저 모든 지역 변수를 담은 locals() 배열을 찾습니다. locals() 배열은 함수 호출 시 만들어지는 스택 프레임 안의 지역 변수 영역을 의미하고, 어떤 함수 안에서 자신의 지역 변수에 접근할 때는 그 변수가 스택 프레임 내의 지역 변수 영역에서 몇 번째에 있는가를 이미 알기에 색인을 사용해 빠르게 접근할 수 있습니다.
여기서 해당 객체를 찾을 수 없으면 globals() 사전에서 찾게 됩니다. globals() 에서도 찾을 수 없다면 마지막으로 __builtin__ 객체에서 찾습니다. __builtin__ 객체는 locals(), globals() 와 달리 모듈 객체로서 그 모듈 내부에서 모든 모듈, 클래스 객체가 저장된 locals() 사전을 탐색하여 특성 속성을 찾게 되는데, __builtin__ 에서 locals() 사전에 접근할 때는 색인으로 접근할 수 없고 변수 이름으로 검색해야 합니다.
Examples
math 모듈의 sin 함수를 어떻게 호출하는지에 따라 네임스페이스 탐색이 어떻게 이루어지는지 살펴보겠습니다.
import math
from math import sin
def test1(x):
res = 1
for _ in range(1000):
res += math.sin(x)
return res
def test2(x):
res = 1
for _ in range(1000):
res += sin(x)
return res
def test3(x, sin=math.sin):
res = 1
for _ in range(1000):
res += sin(x)
return res
- test1의 경우 math 라이브러리에서 명시적으로 sin 함수를 호출합니다.
- test2의 경우에는 math 모듈에서 명시적으로 sin 함수를 임포트합니다.
- test3의 경우는 sin 함수를 인자로 받도록 하고 기본값을 math 모듈의 sin 함수로 지정했습니다.
지난 포스트의 dis.dis 모듈을 이용하여 바이트코드 단으로 분석해 보겠습니다.
>>> dis.dis(test1)
7 18 LOAD_FAST 1 (res)
20 LOAD_GLOBAL 1 (math)
22 LOAD_METHOD 2 (sin)
24 LOAD_FAST 0 (x)
>>> dis.dis(test2)
13 18 LOAD_FAST 1 (res)
20 LOAD_GLOBAL 1 (sin)
22 LOAD_FAST 0 (x)
>>> dis.dis(test3)
19 18 LOAD_FAST 2 (res)
20 LOAD_FAST 1 (sin)
22 LOAD_FAST 0 (x)
- test1의 경우 math 모듈을 로드하고 그 모듈에서 sin 함수를 찾는 과정을 거칩니다.
- test2의 경우 sin 함수를 global 네임스페이스에서 직접 접근할 수 있어 test1과 달리 모듈을 찾은 이후에 모듈의 속성을 탐색하는 괒어을 피할 수 있습니다.
- test3의 경우는 sin 함수 자체를 인자로 받았기 때문에 test3 함수가 최초로 정의될 때만 math.sin 함수를 찾습니다. 즉, 함수가 정의되고 나면 sin 함수에 대한 참조는 함수 정의부에 기본 키워드 인자로 지역 변수에 저장됩니다.
실제로 이 3개의 함수를 테스트해보면 test3 > test2 > test1 순서로 빠른 것을 볼 수 있습니다.
>>> %timeit test1(123456)
173 µs ± 11.5 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit test2(123456)
127 µs ± 617 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit test3(123456)
115 µs ± 2.27 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
test3이 가장 빠르긴 하지만 실제로 파이썬다운 코드도 아니고 이렇게 사용하지도 않습니다. 또한 네임스페이스 탐색에 따른 성능 저하는 해당 함수를 엄청나게 호출하는 줄리아 집합 같은 경우가 아니면 거의 눈에 띠지 않습니다. 대신 루프의 가장 안쪽 블록에서 특정 함수를 엄청나게 호출하는 경우라면 루프 시작 전에 전역 참조를 지역 변수에 담아두면 더 좋겠죠. 함수가 호출될 때마다 루프가 시작되기 전에 global 네임스페이스를 한 번 탐색해야 하지만 루프 안에서는 더 빠르게 동작하게 됩니다.
'Computer > Python' 카테고리의 다른 글
제너레이터와 yield (3) - 지연 계산 (0) | 2022.09.11 |
---|---|
제너레이터와 yield (2) - 피보나치, 무한급수 (0) | 2022.09.11 |
프로파일링 (5) - 바이트코드: 내부작동 이해하기 (0) | 2022.09.03 |
프로파일링 (4) - line_profiler (0) | 2022.08.20 |
프로파일링 (3) - cProfile 모듈 (0) | 2022.08.20 |