본문 바로가기

파이썬 (python)

[python 21] 사용자 정의 식별자와 참조 범위

 

 

 

이번 글에서는 사용자 정의 식별자 (user defined identifier) 와 참조 범위 (scope)에 대해 살펴보도록 하자. 이를 설명하기 전에, 잠시 모듈의 개념에 대해 살펴보기로 한다. 

 

○ 모듈 (modules)과 패키지 (packages)

 

논의를 진행하기 위해 우선 파이썬 모듈(module)에 대해 살펴보도록 하자. 하나의 모듈은 물리적으로 하나의 파일이다. 하나의 파일 내에는 여러개의 함수와 여러개의 변수가 포함될 수 있다. 아래의 abcd.py 파일에는 3개의 선언이 포함되어 있다. 먼저 2개의 함수 hap( ) 과 average( ) 가 선언되어 있고, data 라는 변수가 하나 선언되어 있다.

 

 

 

위의 파일을 %load 또는 %run 매직 명령어를 이용하여 주피터 노트북 내부로 불러들일 수가 있다. 이렇게 불러들이게 되면, 주피터 노트북에서 직접 정의한 것과 똑같은 효과가 생긴다. data 라는 부속과 hap( )이라는 부속, average( ) 라는 부속이 우리의 주피터 노트북이라는 제품에 조립되었다. abc.py 에 선언된 각각의 변수와 함수는 물리적으로 하나의 파일에 함께 저장되었다 뿐이지 각각은 상호 독립적인 개체가 된다. 

 

 

 

요즘 제품 설계 기술 중의 하나로 modular design 이란 것이 있다. 예를 들어, 휴대폰에 들어가는 카메라 같은 경우를 상상해 보자. 일단 그 자체는 부품(part)은 아니다. 여기서 부품이란 제품을 구성하는 요소 중 더 이상 분해 (dis-assemble) 할 수 없는 가장 작은 단위를 말한다. 휴대폰에 사용되는 카메라 같은 경우는 복잡한 기능을 수행하는 것으로서 하나의 부품처럼 단순하지는 않다. 여러개의 부품이 조립된 것이다. 그렇다고 해서 그 자체를 일반 시민들과 같은 최종사용자들에게 바로 판매할 수 있는 제품(final product) 도 아니다. 

 

다시 얘기해서, 여러개의 부품이 모여서 만들어졌지만 그 자체가 하나의 제품이 되지는 않는 그런 "것 (thing)" 들이 존재한다. 그런 것들을 보통 모듈(module)이라고 부른다. 모듈은 기능의 구현을 쉽게 하고, 쉽게 교체할 수 있어 고장이나 업그레이드에 수월하게 대응할 수 있는 장점이 있다. 

 

전체 프로그램을 하나의 제품이라고 보면, 하나 하나의 변수와 함수는 전체 프로그램(제품)을 구성하는 부품(part)이 된다. 파이썬 모듈은 여러 부품(변수와 함수)들을 하나로 묶을 수 있는 수단이 된다. 아래의 그림을 참고하기 바란다. 

 

[그림] 모듈은 하나의 독립적인 소프트웨어 개체가 된다

 

 

파이썬 스크립트 파일을 import 하면 그 파일은 모듈로 동작하게 된다. 예를 들어, 위의 abcd.py 파일을 아래와 같이 import 하게 되면 abcd.py는 abcd 라는 이름을 가진 하나의 독립적인 소프트웨어 모듈이 된다. dir( ) 함수를 실행시켜보면, 파일의 이름과 같은 abcd 라는 이름이 리스트에 포함된 것을 확인할 수 있다.   

 

 

 

실제 abcd 이름이 뭘 의미하는지는 abcd? 명령어를 실행시켜 확인해 보자. abcd 는 하나의 모듈임을 확인할 수 있다. 

 

 

 

dir(abcd) 명령어를 실행시켜 abcd 모듈을 구성하는 부품(변수, 함수)들을 확인해 보자. abcd.py 파일 내에 정의되어 있던 변수와 함수의 이름들이 abcd 라는 모듈 안에서 선언되고 있음을 확인할 수 있다. 이렇게 모듈 안에서 선언된 변수와 함수들은 [모듈이름.] 을 앞에 붙여 참조되거나 호출될 수 있다. In[26]은 abcd 모듈에 있는 hap( ) 함수를 호출하는 경우를 보여 주고 있다.

 

모듈이름 뒤에 붙어 있는 닷(dot, . )은 멤버 오퍼레이터(member operator)라고 부르는 것으로 a.b 형태로 사용된다. 여기서 b 는 a 를 구성하는 하나의 요소임을 나타낸다. 즉, a 를 구성하는 여럿 중에, 그 중에 하나 b를 가리킬 때 a.b 라는 노테이션(표기법)을 사용한다.

 

멤버 오퍼레이터는 이미 우리가 일상생활에서 사용하고 있는 개념이다. (그렇게 보면, 소프트웨어에서 사용되고 있는 개념들 중에 사실 낯선 개념은 없는 것 같다. 그냥 익숙하지 않을 뿐이다.) 우리나라에 "김서방" 으로 불리는 사람이 둘이 있다. 그럼 누군가가 나에게 "김서방이 말이야, 그저께 종로에서 술을 먹는데..." 라고 얘기한다면 당연하게 "어느 김서방 말하는거야?"라고 되묻게 될 것이다. 이름을 바꿀 수는 없는 노릇이고 두 김서방을 구별해야 할 텐데, 만약 한사람은 서울에 살고 다름 한사람은 부산에 산다면 "서울사는 김서방" 이랑 "부산 사는 김서방"으로 구분하여 호칭하게 된다. 서울사는 김서방은 서울.김서방이고 부산사는 김서방은 부산.김서방인 셈이다. 

 

 

 

한가지 조심할 것은, 모듈은 한번 import 되면, 그 내용을 수정할 수 없다. 파일의 내용을 수정 편집하더라도 그 내용이 반영되지 않는다는 뜻이다. 또한, 수정한 파일을 다시 import 하더라도 원래 import 된 내용을 엎어 쓰지 않는다. 

 

패키지(packages)는 여러개의 모듈을 묶는 단위가 된다. 일단은 이 정도만 파악하고 넘어가자. 당분간의 공부에서는 소용이 없다. 

 

 

 

○ 사용자 정의 식별자 (user-defined identifier)

 

모든 문장들은 단어의 조합으로 이루어진다. 즉, 여러개의 단어가 이어져서 하나의 문장이 되는 것이다. 따라서 파이썬 문장에서 사용되는 단어들은 그 의미가 사전에 정의되어 있어야 한다. 

 

예를 들어서, 아래의 print(a) 문장은 2개의 단어, print 와 a, 로 구성되어 있다. print 는 화면에 인자의 값을 출력해 주는 함수의 이름 (identifier) 으로서 파이썬이 이미 알고 있는 단어이다. 하지만, a 란 단어는 아직 "not defined" 되어 에러가 출력된다. 문장에서 a 란 단어를 사용하고 있는데, 그 단어의 의미가 정해져 있지 않아, 전체 문장이 무엇을 의미하는지 모르겠다는 뜻이다. 

 

 

 

이제 우리가 a 란 단어 (word)를 어떤 데이터를 가리키는 변수의 이름(name 또는 identifier) 로 정의하고 나면, 위에서 에러가 났던 print(a) 문장이 성공적으로 실행되는 것을 확인할 수 있다. 아래의 그림을 참조하기 바란다. 

 

 

 

위의 코드에서 a = 10 이란 명령어를 통해 a 란 단어의 의미를 정의하고 있다. 이제 a 란 단어를, 이름을, 식별자를 어떤 데이터 10을 가리키는 단어로, 이름으로, 식별자로 사용하겠다는 선언을 나타내고 있다. 이렇게 a 란 단어의 의미가 선언된 후에는 print(a) 문장이 전혀 문제없이 실행됨을 확이할 수 있다.

 

다시 정리하면, 파이썬에서 만들어지는 문장은 단어들로 구성된다. 이 단어들에는 파이썬에서 기본적으로 제공하는 예약어, 내장함수의 이름이 포함되는데, 그 수가 많지는 않다. 이 단어들 만을 사용해서 만들 수 있는 문장의 수도 극히 제한적일 수 밖에 없다. 하지만, 사용자가 (user)가 단어를 추가할 수 있다. 그 단어는 데이터의 이름(name)이거나 함수의 이름이 된다. 

 

dir( ) 함수를 이용하면 문장에 사용할 수 있는 단어(이름)의 종류를 파악할 수 있다. 아래의 In[1]에서 확인한 이름의 종류에 In[2]에서 정의한 a 라는 단어가 추가된 것을 In[3]에서 확인할 수 있다. 

 

 

 

함수의 경우도 마찬가지다. 아래의 코드에서 In[2]에서 정의된 hello 라는 함수의 이름이 In[3]의 단어 리스트에 새로 추가되었음을 확인할 수 있다. 

 

 

 

이렇게 정의된 단어들(이름들, 식별자들)은, 아래와 같이, 실제 파이썬 문장을 만드는데 사용될 수 있다. 

 

 

 

다시 정리해 보자. 우리가 파이썬 "문장"에 사용할 수 있는 "단어"는 데이터에 대한 "이름 (name)"이거나 함수에 대한 "이름"이다. 

 

 

○ 변수 이름의 범위 (Scope)

 

데이터의 이름인 변수는 그 이름이 참조되는 범위가 있다. 가장 기본적인 규칙은 "함수 안에서 정의된 변수는 그 함수 안에서만 참조될 수 있다" 이다. 함수 안에서 정의된 변수를 보통 지역변수 (local variables) 라고 부른다. 함수 밖에서 정의된 변수는 일종의 전역변수 (global variables) 가 된다.  전역변수의 경우 그 전역에 포함되어 있는 모든 함수에서 그 변수에 대한 접근(참조)이 가능하다. 

 

변수는 선언되면 참조될 수 있다. 아래의 In[7]은 a 라는 단어의 의미를 선언한 것이다. "이제 부터 a는 100 이라는 숫자 데이터를 가리키는 이름으로 사용하겠다" 고 선언한 것이다.  그 선언 이후에 In[8] 과 같이 a 란 단어가 문장에서 사용될 수 있다. In[8]의 a 는 "a가 가리키는 값", 줄여서 "a 값"으로 a 변수에 대한 참조(reference)가 된다. 참조란 그 변수가 가리키는 값을 읽는 행위라고 보면 된다. 아래 코드에서 선언된 변수 a 와 b는 함수 밖에서 정의된 변수이므로 전역변수로  선언된 것이다. 

 

 

 

아래의 In[10]은 add( ) 함수를 정의한 것이다. 이 함수 안에서 정의된 e 변수는 add( ) 함수 안에서 선언되었으므로 add( ) 함수의 지역변수이다. add( )함수의 매개변수로 선언되어 있는 c와 d도 add( ) 함수의 지역변수가 된다. 어싸인(assign)이 없으니 변수 선언이 아니지 않은가? 변수 선언이 맞다. 함수 호출이 이루어질 때 변수 assign이 이루어진다. 예를 들어 add(10, 5) 라고 호출하게 되면 c 변수에는 10이란 값이, d 변수에는 5라는 값이 저장된다. add(c, d) 함수를 add(10, 5)로 호출하게 되면, 그 순간에 c=10, d=5 라는 2개의 assignment가 이루어진다. 함수 안에서 선언되기 때문에, 매개변수도 역시 지역변수가 된다. 

 

 

 

다시 정리해 보면, 지역변수(local variable)은 함수 안에서 선언된 변수이고, 전역변수(global variables)은 함수 바깥에서 정의된 변수이다. 

 

지역변수와 전역변수는 참조에서 차이가 나타난다. 지역변수는 그 변수가 선언된 함수 내에서만 참조될 수 있다. 반면에, 전역변수는 그 "전역" 범위 내에서 정의된 모든 함수에서 참조할 수 있다. 즉, 같은 범위 안에 있는 모든 함수들이 상호 공유할 수 있는 변수가 된다. 여기서, 전역은 객체(object), 모듈(modules) 또는 패키지(packages)에 해당한다. 아직 객체와 패키지를 공부하지 않아서 연상이 잘 안될텐데 당분간은 객체=패키지=모듈 이라고 생각해도 된다. 

 

아래 그림을 예로 들어 살펴 보자. 1개 모듈에 전역변수 a와 2개의 함수, f( )와 g( ), 가 선언되어 있다.

 

a 라는 변수이름이 보이는데 전역에서는, a=10 으로 선언되었기 때문에, 10이란 값을 가리키는 이름으로 사용된다. g( ) 함수에서 a 변수를 참조하면 전역변수 a 가 참조된다. 실제로 g( ) 함수에서 print(a) 해 보면 전역변수 a 값이 참조됨을 확인할 수 있다. 

 

 

 

그런데, f( ) 함수의 경우에는 전역변수 a 이름과 동일한 이름의  지역변수 a 를 선언하고 있다. 실제 f( ) 함수 내에서 a 값을 출력해 보면 a 라는 이름은 f( )함수의 지역변수를 가리키고 있음을 확인할 수 있다.

 

 

 

결과적으로, 어떤 함수 내에서 어떤 변수를 참조하는데, 그 변수가 그 함수 내에 지역변수로 선언되어 있는 경우에는 당연히 그 변수를 참조하게 된다. 만약에 동일한 이름의 변수가 함수 내에 정의되어 있지 않은 경우에는 전역변수를 참조하게 된다. 그런데, 전역변수에도 동일한 이름의 변수가 없으면 "not defined" 에러가 발생하게 된다. 첫번째 규칙으로 기억해 두자.

 

두번째 규칙은, 함수 안에 정의된 변수를 함수 바깥에서 참조할 수 없다는 것이다. 예를 들어, 아래의 코드에서 In[13] 처럼 함수 바깥에서 b 변수를 참조하려고 할 때, 전역변수로 b 변수가 선언되어 있으면 그 변수를 참조하되 만약 전역변수로 b 변수가 선언되어 있지 않다면 역시 "not defined" 오류가 발생하게 된다. 

 

 

같은 맥락으로, 위의 그림의 예에서 보면 f( ) 함수와 g( ) 함수 모두 b 라는 이름의 변수를 가지고 있지만, 서로 다른 변수이다. 

 

 

마지막으로 한번더 강조하고 싶은 것은 함수의 매개변수(parameter)도 그 함수의 지역변수라는 것이다. 이것도 하나의 규칙으로 기억해 두면 좋겠다. 그에 관한 내용은 위에서 이미 살펴본 바 있다.

 

 

○ 함수 이름의 범위 (Scope)

함수의 이름도 변수의 이름과 마찬가지로 호출할 수 있는 범위가 있다. 변수의 범위와 비슷한 개념이다. 단지 동일한 모듈 안에 있는 함수를 호출할 때에는 함수 이름 만으로 호출이 되지만, 만약 다른 모듈에 포함된 함수를 호출하는 경우에는 위에서 살펴본 바와 같이 모듈이름을 함수 이름 앞에 밝혀줘야 한다. 

 

 

○ 이름 공간 (Name Space)

 

이와 같이 어떤 이름이 참조되는 범위가 한정되어 있다. 이렇게 한정된 범위를 이름공간(name space)라고 부른다. 하나의 이름공간에는 동일한 이름을 중복해서 정의할 수 없다. 하지만, 서로 다른 이름공간에서는 동일한 이름을 서로 다른 의미로, 서로 다른 어떤 것(thing)을 가리키는 이름으로 사용할 수 있다.

 

만약에 이름공간이 없다면 어떤 일이 벌어질까? 이것은 마치 우리나라에 이미 "홍길동"이란 이름의 사람이 살고 있으면 그 사람 외에는 아무도 "홍길동"이란 이름을 사용할 수 가 없다는 규칙과도 같다. 만약에 내가 2세의 이름을 짓고자 한다면 우리나라 사람들의 이름을 모두 확인한 후에 중복되지 않는 이름을 하나 만들어야 하는 것과 같다. 변수 이름 하나 만드는 것과 함수 이름 하나 만드는 것이 얼마나 어려운 일이 될 것인가?

 

이러한 문제를, 동일한 이름 공간 내에서는 중복해서 정의하지 않는 것으로 규칙을 정하고, 다양한 이름 공간을 만들 수 있도록 (예를 들면, 모듈의 이름이 이름공간이 된다) 함으로써 해결하고 있다. 우리가 서울사는 김서방, 부산사는 김서방으로 구별할 수 있으면 김서방이란 이름을 자유롭게 사용할 수 있다는 뜻이다. 최소한 서울에서는 김서방이란 이름이 중복되지 않도록 하면 된다. "서울이 너무 넓은데 어떡해요?" 라고 묻기 없기.