티스토리 뷰

반응형

프로그래밍 언어에서 '이름'은 어떠한 값이나 객체를 가리키는 구분자이다. 따라서 기본적으로 다른 값이라면 다른 이름으로 참조되어야 하는 것이 맞다. 그렇지 않은 경우라면 이름에 대해 충돌이 발생하고 코드는 실행되지 못하거나 예기치 않은 방식으로 동작하게 될 것이다.

하지만 이는 이상적인 경우를 상정했을 때이고, 실질적으로는 모든 이름이 유니크할 수는 없다. 심지어 제 3자가 작성한 모듈을 사용해야 하는 경우에는 외부 모듈에서 사용되는 모든 심볼 이름을 체크해서 이름 충돌을 피하는 것은 사실상 불가능하며, 공통 클래스의 서로 다른 인스턴스들도 내부에는 같은 이름의 어트리뷰트를 가지고 있기도 하다.

그래서 많은 언어들은 "이름 공간"이라는 것을 도입하여 한정된 범위에서만 이름에 대한 충돌이 발생하지 않는 선에서 프로그래머의 의무를 축소하고, 서로 분리된 공간내에서는 같은 이름이 존재하더라도 충돌을 방지할 수 있게 해주는 장치를 가진다. 흔히 이 이름공간은 다시 변수의 관점에서 변수의 스코프라고 불리며, 스코프에 따라서 전역변수지역변수로 변수를 구분하기도 한다.

파이썬의 이름공간

파이썬의 이름 공간은 기본적으로 전역 변수와 지역변수의 두 가지 구분만 존재한다. 전역 변수는 기본적으로 특정한 함수에 진입하지 않은 경우에 항상 액세스할 수 있는 변수를 말한다. 지역 변수는 함수 레벨에서 존재하며, 지역변수로 만들어진 변수는 해당 변수 내에서만 유효하며, 함수의 스코프를 벗어나는 시점에 파괴된다.

참고로 C나 자바와 같은 언어에서 변수의 스코프는 블럭단위로 존재하기 때문에 중괄호로 구분되는 블럭내에서 선언된 함수는 그 블럭 내에서의 지역 변수로 인정되며, 전역 변수를 가리게 된다.

하지만 파이썬의 이름 공간은 매우 단순하며, 그렇기 때문에 이에 익숙하지 않은 타 언어 경험자(?)들에게 굉장히 이상하게 돌아가는 것 같은 경우를 선사할 때가 있다.

오늘은 이에 대한 이야기를 좀 해보려고 한다.

이름과 바인딩

서점으로 달려가서 프로그래밍에 관한 아무 책이나 집어서 펼쳐본다면 그 속에서 "변수"라는 말과 "대입"이라는 말을 찾을 수 있을 것이다. 심지어 파이썬에 관한 책들도 이 개념에 대해서는 아무런 의심없이 사용하는 경우가 많다. 하지만 엄밀하게 말해서 파이썬에는 "대입"이라는 개념이 존재하지 않는다.

"대입"이라는 것은 변수를 실제 값을 담는 메모리 공간과, 그 공간에 붙인 이름의 덩어리로 규정한다. 따라서 "변수 a에 1이라는 값을 대입한다"는 것은 변수 a가 가리키는 어떤 메모리 공간이 있고, 이 공간에 1이라는 값을 쓴다는 것이며, 이는 1이라는 값이 해당 메모리 영역으로 복사되었음을 가리킨다.

다음의 C 코드를 보자


int a, b, c;
a = 1;
b = a;
c = b;

이 코드는 다음과 같이 해석할 수 있다.

  1. 세 변수 a, b, c 를 선언하고, 정적 메모리 영역에 공간을 각각 할당한다.
  2. a가 가리키는 메모리 구간에 1을 써 넣는다.
  3. b가 가리키는 메모리 구간에 a의 영역의 값을 써 넣는다. (즉 복사됐다)
  4. c가 가리키는 메모리 구간에 b의 영역의 값을 써 넣는다.

결국 1이라는 정수 값은 세 곳의 메모리에 각각 write 되었고, 이는 1이라는 값의 세 개의 사본이 저장되었다는 의미이다.

하지만 파이썬에서는 결코 이런 일이 일어나지 않는다.

파이썬에서 모든 것은 객체이고, 모든 이름은 어떤 객체에 연결될 수 있는 레이블일 뿐이다.

따라서 파이썬의 모든 변수에는 값이 쓰여지는 것이 아니라, 변수의 이름과, 변수가 가리키는 객체 사이에 연결이 하나 만들어질 뿐이다. 이 해석은 C의 관점에서는 기술적으로 모든 파이썬 변수는 포인터이고, 그 실제 값은 객체가 위치하는 메모리 번지라는 말과 결과적으로는 같은 것이지만, 파이썬에서는 포인터를 직접적으로 다룰 일이 없으므로 이런 식의 해석까지는 이해하고 있을 필요가 없다.

나는 여기서 한가지 비유를 들고 싶다. 바로 '상자의 비유'이다.

  1. 파이썬의 모든 것은 객체이다.
  2. 객체는 그 속에 어떤 값을 가지고 있는 상자이다. 그 값은 숫자값일 수도 있고, 문자열일수도 있으며 심지어 어떤 함수일 수도 있다.
  3. 이 상자에는 여러 개의 이름표를 붙일 수 있다. 그 각각의 이름표에는 변수 이름이 쓰여있으며, 이 이름표는 떼어내어 다른 상자에 옮겨 붙일 수도 있다.
  4. 어떤 상자에 이름표를 붙이는 행위를 바인딩이라 하며, 이는 다른 언어에서는 "대입문"이라 불리는 코드와 완벽하게 같은 모양이다.

이 관점에서 아래 코드를 보자.


a = 1
b = a
c = b

이 코드를 해석하면 다음과 같다.

  1. 메모리 내부 어딘가에 정수 타입의 1의 값을 갖는 상자(객체)가 생성되어 있고, 여기에 'a'라는 이름을 붙였다.
  2. 그리고 'a'가 가리키는 바로 그 상자에 'b'라는 이름을 또 붙였다.
  3. 'b'가 가리키는 그 상자에 다시 'c'라는 이름을 또 붙였다.

위 C 코드에서 각각의 변수는 각각의 고유한 값을 가졌다. 왜냐하면 C에서는 실제로 대입이라는 것이 메모리에 write 하는 행위 그자체이며, write 했기 때문에 그 값을 복사하는 동작을 수행한 셈이다. 하지만 파이썬에서는 모든 것이 객체이며, = 을 이용한 저 구문은 모두 "바인딩"이지 대입이 아니다. 따라서 어떤 것도 복사된 적이 없다.

그리고 여기서 중요한 것은 바인딩은 실제로 이름을 붙이는 것이다. 그리고 바인딩은 단순한 참조가 아니다.

자 그러면 다음 코드는 어떨까? b 의 값은 얼마일까?


a = 1
b = a
a = a + 1

상자의 비유가 없다면, 이렇게 해석하는 사람이 분명 있다.

  1. a는 1을 가리킨다.
  2. b는 a를 가리킨다.
  3. a는 2가 되었다.
  4. b는 a를 가리킬 것이므로 2가 되었을 것이다.

과연 그럴까? 상자의 비유라면 다음과 같이 해석한다.

  1. 1a라는 이름을 붙였다.
  2. a가 가리키는 상자에 b라는 이름을 붙였다.
  3. a라는 이름은 이제 1이 아닌 2에 붙인다. 1이 들어있는 상자에서 2가 들어있는 상자로 이름표를 옮겼다.
  4. b라는 이름은 여전히 1의 상자에 붙어있다. 따라서 b는 1이다.

어느 것이 정확한지는 쉘을 통해서 직접 확인해보기 바란다.

전역변수, 지역변수

파이썬의 '이름'은 오직 두 가지 스코프만 가지며 따라서 전역 변수이거나 지역 변수 두가지만 존재할 수 있다. 이 두가지 이름 공간은 가장 파이썬스러운 방법으로 관리되는데, 전역적 이름 공간은 사실 하나의 사전(dict)객체이며, 함수 역시 별도의 (직접적으로 액세스할 수 없는) 사전을 하나씩 가지고 있으며 여기에 지역 변수들이 모이게 된다. 변수이름들을 관리하는 사전은 직접 액세스할 수 없지만, 우리는 globals(),locals()라는 두 개의 기본 내장 함수를 이용해서 전역 스코프와 현재 함수의 지역 스코프에 있는 모든 변수의 정보를 획득할 수 있다.

함수 내에서 어떤 이름이 사용되는 과정은 다음과 같은 순서를 따른다.

  1. 해당 함수의 이름 공간내에서 참조하고자 하는 변수 이름을 키로 찾는다.
  2. 지역 변수 이름 공간 내에 정의된 키(변수)가 있으면 이는 지역변수로 인식된다.
  3. 지역 변수 이름 공간 내에 정의된 키가 없으면 다시 전역 변수 이름 공간을 찾는다.
  4. 이 때 변수 이름이 발견되면 전역 변수로 기능한다.
  5. 만약 두 개의 이름 공간내에서 찾을 수 없는 변수명을 참조하려고 했다면 Unbound Error가 발생한다. 해당 이름에 바인드된 객체가 존재하지 않는다는 뜻이다.

그리고 함수 내에서 변수를 선언/정의하는 방법에는 다음과 같은 문법이 사용된다.

  • global x : 변수 x는 명시적으로 전역 이름 공간 내에서만 찾는다.
  • x = a : 그외에 대입문의 좌변에 위치하는 변수는 모두 지역 변수가 된다.

지역변수 x가 존재하고 같은 이름의 전역 변수가 존재한다면, 함수 블록 내에서는 항상 지역 변수 x만 참조된다.

이 때 중요한 것은 "변수는 선언되기 전에 참조될 수 없다"는 것이다. 이 명제는 논리적으로 당연한 것이다. 그리고 다음은 초보자들이 흔히 할 수 있는 실수 중 하나이다.


c = 3  # 1

def f(x):
    print(c)  # 2
    c += x  # 3
    print(c)  # 4

아마 이런 식으로 기대하고 코드를 작성했을 것이다.

  1. 전역변수 c가 있고, 그 초기값은 3이다.
  2. 함수 f가 실행되면 우선 전역변수 c의 현재 값을 출력하고
  3. c += 3을 통해서 c의 값을 x만큼 증가시킨다.
  4. 변경된 c의 값을 출력한다.

하지만 이 함수의 실행 결과는 처참하게 에러일 뿐이다. 그 이유는,

  1. c += x에서 c는 함수 f의 지역변수가 된다.
  2. 따라서 # 2부분에서 print(c)하는 것은 지역 변수가 선언되기 전에 참조하는 것이므로 Unbound Local Error가 된다.

이 함수에서 에러를 없애기 위해서는 다음과 같이 수정한다.


c = 3

def f(x):
    global c
    print(c)
    c += x
    print(c)
    
f(1)
# 3
# 4
f(2)
# 4
# 6

정리해보면, 함수 내에서 지역변수/전역변수를 참조하기 위한 방법을 생각해보면 다음과 같이 정리할 수 있다.

  1. 모든 지역 변수는 참조되기 전에 반드시 선언되어야 한다.
  2. 전역변수를 변경하려는 경우에는 반드시 참조하기 전에 global 키워드를 써서 전역 변수임을 명시한다.

하지만 더 중요한 것은 가급적이면 함수 내에서는 전역 변수의 값을 변경하지 않는 것이다. 함수 내에서 전역 변수를 변경하는 것 자체가 문제가 되지도 않고, 충분히 작은 규모내에서는 그렇게 쓰는 편이 유리할 수도 있지만, 함수는 함수 내의 범위의 값만 변경하는 것이 좋다.

만약 전역 변수의 값을 함수를 통해서 변경하고 싶다면, 함수를 통해서 계산만 하고, 그 결과를 이용한 업데이트는 전역적인 레벨에서 해주는 것이 맞다. 예를 들어, a, b, c, d, e 다섯개의 변수의 값을 각각 1씩 올려주는 함수가 있다고 하자.


a, b, c, d, e = 5, 3, 7, 2, 9

def increase_abcde():
    global a, b, c, d, e
    a += 1
    b += 1
    c += 1
    d += 1
    e += 1
    
increase_abcde()  

이런식으로 작성할 수도 있겠지만, 보다 나은 디자인은 다음과 같은 것이다.


a, b, c, d, e = 5, 3, 7, 2, 9

def increased_abcde():
    return a+1, b+1, c+1, d+1, e+1

a, b, c, d, e = increased_abcde()

반응형

'파이썬 how to' 카테고리의 다른 글

리스트 정렬하는 방법  (0) 2017.05.31
파이썬의 반복자와 for 반복문  (0) 2017.05.26
파이썬 사전 사용법  (0) 2017.05.19
파이썬 튜플 기초 사용법  (0) 2017.05.15
iPython 설치하는 방법  (3) 2017.05.09
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함