python compile

복준수·2025년 1월 1일
0

Python

목록 보기
3/5
post-thumbnail

1. 변수 참조와 할당

스택과 힙

  • 객체참조할 시에는 스택namespace에 저장된 주소를 참조(레퍼런싱)한다.직접 힙을 참조하지 않는다.
  • 값을 할당할 시에는 해당 scope, namespace 스택에 주소를 push한다.
a= 10
b= [4,5,6]
def func1():
    a = 20
    b = [1,2,3]
    print(a) # 20
    print(b) # [1,2,3]
print(a) # 10
print(b) # [4,5,6]
func1()
  • 값 할당
    • a10이라는 값을 할당하여 전역스택에 push한다.
    • b리스트가 저장된 주소를 전역스택에 push한다.
    • 함수라는 객체가 저장된 주소를 전역스택에 push한다.
    • 변수에 변수를 할당할 때에는 스택에 저장된 값이 할당된다. (리스트의 경우 주소가 할당된다.)
  • 함수 호출
    • print() : 전역스택에 저장된 ab 가 가장 가까우므로 전역스택에 값을 참조한다. (scope 개념)
    • func1() 함수 : 함수가 호출 되면 func자체의 namespace(스택)이 새로 만들어진다.
    • 함수 내부에서 할당된 값을 함수스택에 push 한다.
    • 함수는 호출이 종료되면 본인의 스택을 지운다. Garbage Collector
  • func2()의 경우 호출시 문제가 되지 않지만
  • error()의 경우 호출시 에러를 발생시킨다
    > 코드를 예상해보면 아직 함수 스택에 apush하지 않았으므로 전역변수를 참조할 것 같지만,
    실제로 error()을 호출하면 지역변수를 참조하고 함수스택에는 a가 없으므로 에러가 발생한다.
a=10
b=[4,5,6]
def func2():
    print(a)	# 10
    print(b)	# [4,5,6]
print("test function2:", sep='\t')
dis.dis(func2)

# 호출시 에러
def error():
    print(a)
    print(b)
    a = 30
    b = [1,2,3]
print("error function: ", sep='\t')
dis.dis(error)
error()
UnboundLocalError: cannot access local variable 'a' where it is not associated with a value

2. ByteCode (dis 라이브러리)

func2의 바이트 코드

  • dis 라이브러리를 이용해 디스어셈블된 바이트 코드를 확인해볼 수 있다.

https://docs.python.org/ko/3.8/library/dis.html#opcode-BUILD_TUPLE

  • 확인해보면 LOAD_GLOBAL전역space 에서 a 변수를 참조하려 한다.

test function2:
 28           0 RESUME                   0

 29           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_GLOBAL              2 (a)
             22 CALL                     1
             30 POP_TOP

 30          32 LOAD_GLOBAL              1 (NULL + print)
             42 LOAD_GLOBAL              4 (b)
             52 CALL                     1
             60 POP_TOP
             62 RETURN_CONST             0 (None)

error의 바이트 코드

확인해보면 LOAD_FAST_CHECK 이번에는 전역 space가 아닌 local space에서 해당 변수를 참조한다.

  • 하지만 함수스택에는 a를 초기화한적이 없기 때문에 오류를 발생시킨다.
  • 이는 컴파일 과정에서 함수내부의 변수를 먼저 확인 하는데,
    ab를 초기화하기 때문에 해당 변수가 함수스택 내부에 있을 것이라고 판단한다.
  • 하지만 함수스택에 push한적이 없기 때문에 함수스택을 참조할 때 에러를 발생시킨다.
error function:
 35           0 RESUME                   0

 36           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_FAST_CHECK          0 (a)
             14 CALL                     1
             22 POP_TOP

 37          24 LOAD_GLOBAL              1 (NULL + print)
             34 LOAD_FAST_CHECK          1 (b)
             36 CALL                     1
             44 POP_TOP

 38          46 LOAD_CONST               1 (30)
             48 STORE_FAST               0 (a)

 39          50 BUILD_LIST               0
             52 LOAD_CONST               2 ((1, 2, 3))
             54 LIST_EXTEND              1
             56 STORE_FAST               1 (b)
             58 RETURN_CONST             0 (None)

global keword

  • global 키워드를 이용해 지역변수가 아닌 전역변수를 참조할 수 있다.
a= 10
b= 30
c=[1,2,3,4]

def func3():
    # 함수 내부에서 전역스택을 참조할 수 있다.
    global a,b,k
    print(a)
    print(b)

    d = a
    k = 1
    # string은 스택에 저장된 변수
    b = b[::-1]

    # 스택에 어떤 값을 할당하는 것이 아니므로 global 키워드가 필요 없다.
    # c의 주소를 참조하여 힙에 리스트를 바꿀수 있다!
    c.extend([5,6,7,8])
print("test function3 with global keyword:", sep = '\t')
dis.dis(func3)
  • LOAD_FAST가 아닌 LOAD_GLOBAL로 전역스택을 참조한다.
test function3 with global keyword:
 43           0 RESUME                   0

 46           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_GLOBAL              2 (a)
             22 CALL                     1
             30 POP_TOP

 47          32 LOAD_GLOBAL              1 (NULL + print)
             42 LOAD_GLOBAL              4 (b)
             52 CALL                     1
             60 POP_TOP

 49          62 LOAD_GLOBAL              2 (a)
             72 STORE_FAST               0 (d)

 50          74 LOAD_CONST               1 (1)
             76 STORE_GLOBAL             3 (k)

 52          78 LOAD_GLOBAL              4 (b)
             88 LOAD_CONST               0 (None)
             90 LOAD_CONST               0 (None)
             92 LOAD_CONST               2 (-1)
             94 BUILD_SLICE              3
             96 BINARY_SUBSCR
            100 STORE_GLOBAL             2 (b)

 55         102 LOAD_GLOBAL              8 (c)
            112 LOAD_ATTR               11 (NULL|self + extend)
            132 BUILD_LIST               0
            134 LOAD_CONST               3 ((5, 6, 7, 8))
            136 LIST_EXTEND              1
            138 CALL                     1
            146 POP_TOP
            148 RETURN_CONST             0 (None)

3. Python compile

Python compile 과정

인터프리터형 언어라고는 하지만, 컴파일이 아예 이루어지지 않는것은 아니다. 파이썬은 c언어와 달리 직접적으로 cpu가 이해할 수 있는 기계어로 변환하지 않고, 해당 과정을 거친다.

크게 cpython 이라는 인터프리터 안에 compiler가 자체적으로 존재하고 PVM이라는 부분이 존재한다.

우선 compiler는 소스코드(.py)를 byte code(.pyc)로 변환하는 과정을 거친다. (크게 4가지 단계로 구성되어 있다)
이 과정에서 파이썬 내에서 컴파일이 발생하긴 하지만, 아직 cpu가 이해할 수 있는 기계어는 아니다.

이 바이트코드를 기계어로 바꾸기 위해서는 PVM이라는 인터프리터가 필요하다.
PVM은 python 바이트 코드를 기계에서 실행 가능한 코드로 변환하고 이 인터프리터에서 주어진 파일을 줄별로 읽고 기계어로 해석해준다. 해석중에 오류가 발생하면 변환이 중단되고 오류 메세지가 이 단계에서 해석된다.

줄별로 해석된다는게 장점이 될 수도 단점이 될 수도 있다.
c언어의 경우 한번에 모든 코드를 한번에 변환하고 실행시키기 때문에 python보다 빠르다.
c언어의 경우 컴파일 단계에서 오류를 감지하고, python의 경우 컴파일을 한 이후에 줄별로 실행되는 특징 때문에 디버깅이 쉽다는 장점이 있고, 적절한 PVM이 있다면 모든 cpu에서 작동한다.

ref

https://www.geeksforgeeks.org/internal-working-of-python/
https://indianpythonista.wordpress.com/2018/01/04/how-python-runs/

0개의 댓글