클래스 안에서 인스턴스의 속성이나 메서드에 접근하려면 반드시 self.를 붙여야 한다. Node 클래스 내부에 delete 메서드를 재귀 형태로 구현한다고 할 때 delete()라고 쓰면 파이썬은 스코프 범위를 늘려가며 delete 메서드를 찾게 되고, 스코프 사슬 어디에도 delete 메서드가 존재하지 않으면 NameError가 발생한다. 메서드는 클래스 객체에 저장된 함수이고, 접근은 인스턴스 → 클래스 조회를 거쳐야 하므로 self.가 그 조회의 시작점이 된다.
스코프: 이름을 찾는 범위
스코프(scope)는 이름이 유효한 코드 영역이다. 파이썬에서 어떤 이름을 만났을 때 인터프리터가 어디서 그 이름을 찾는지의 규칙이고, 점(.) 뒤에 오는 이름이 아닌 맨 이름(bare name)은 스코프의 안쪽부터 바깥으로 네 단계를 찾게 된다 — LEGB라고 부른다.
‘이름(name)’이라는 표현은 파이썬 문서의 공식 용어이다. 파이썬에서는 변수를 ‘값을 담는 상자’가 아니라 객체에 붙인 이름표로 보기 때문에 변수보다 이름이라는 말을 선호한다. 그래서
VariableError가 아닌NameError인 것도 이러한 이유다.
| 단계 | 스코프 | 어디의 이름인가 |
|---|---|---|
| Local | 지역 | 현재 함수 안에서 대입된 이름 (매개변수 포함) |
| Enclosing | 둘러싼 함수 | 함수 안에 함수가 있을 때, 바깥 함수의 지역 이름 |
| Global | 전역 | 모듈(파일) 수준에서 대입된 이름 — 최상위 함수·클래스 이름 포함 |
| Builtin | 내장 | len, print, max 등 파이썬이 기본 제공 |
해당 LEGB 영역에 클래스 본문에 대한 내용은 포함되지 않은 것을 확인할 수 있다. 메서드 안에서 맨 이름 delete를 찾으면 L → E → G → B를 뒤지는데, 클래스 본문에서 정의된 메서드 이름은 이 스코프 사슬에 포함되지 않는다(클래스 본문은 실행 후 클래스 객체의 속성 dict로 들어가게 된다). 그래서 클래스의 다른 메서드에 닿는 유일한 경로가 속성 조회, 즉 self.delete인 것이다 — 스코프 사슬이 아니라 객체의 속성 사슬을 타는 것.
파이썬에서 클래스도 객체이다.
class Node: ...를 실행하면 Node라는 이름에 클래스 객체 하나가 만들어져 붙고, 이 객체는 자기 안에 ‘속성 이름’ → ‘값’의 dict를 갖고 있다. 클래스 본문에서 정의한 메서드와 클래스 변수가 전부 그 dict의 항목이다.
class Node:
count = 0
def delete(self, key):
return
Node.__dict__ # {'count': 0, 'delete': <function Node.delete>, ...}
delete가 dict의 값으로 함수 객체가 들어 있는 것을 볼 수 있다 — 메서드가 ‘클래스에 저장된 함수’라는 말의 실체다.
인스턴스도 마찬가지로 자기 dict를 갖는다.
node = Node()
node.val = 5
node.__dict__ # {'val': 5} — 인스턴스에는 val만, delete는 없음
node.delete는 어떻게 찾아지는가 — 속성 조회의 연속으로 찾는다: node.__dict__에서 delete를 찾음 → 없음 → type(node).__dict__ 즉 Node.__dict__에서 찾음 → 있음. 부모 클래스가 있으면 그 dict까지 올라간다.
즉 obj.name이라는 점 표기는 “obj의 dict, 없으면 obj 클래스의 dict, 없으면 부모 클래스의 dict”를 차례로 뒤지는 조회이고, 상속이란 이 사슬에 dict를 하나 더 끼우는 것이다.
결론적으로 클래스 내에 선언된 변수와 메서드는 클래스 객체의 속성 dict로 들어가기 때문에 객체 사슬을 타야 해당 정보를 찾을 수 있고, 그 시작점이 자기 객체를 가리키는 self가 되는 것이다. self가 없으면 LEGB로 스코프를 확장하며 찾게 되는데, 해당 스코프 사슬에는 클래스 정보가 포함되지 않는다.
locals()는 배열 슬롯을 dict로 만들어 보여주는 것이라 locals()['y'] = 1로 지역 변수를 만드는 것이 전역과 달리 동작하지 않는다.추가로 인스턴스의 obj.__dict__와 globals()는 실제 dict라 항목을 직접 조작하면 반영된다. 단, 클래스의 __dict__는 읽기 전용 뷰(mappingproxy)라 Node.__dict__['count'] = 1은 불가하고, 클래스 속성 변경은 Node.count = 1처럼 점 표기로 한다.
| 이름 공간 | 실제 저장 방식 | dict인가 |
|---|---|---|
객체 속성 (obj.__dict__) |
dict | 예 |
클래스 속성 (Cls.__dict__) |
dict (읽기 전용 뷰로 노출) | 예 |
전역 (globals()) |
dict | 예 |
| 내장 | dict | 예 |
| 함수 지역 (매개변수 포함) | 인덱스 배열 슬롯 (컴파일 시 결정) | 개념적으로만 — 실제 접근은 dict 아님 |
참고: 메서드 내에서 전역 변수에 접근하는 것도
global을 사용해 가능하지만 스파게티 코드를 만들 수 있으므로, 지역 변수의 한 종류인 매개변수로 값을 넘겨 반환하는 형태로 처리하는 것이 바람직하다. 부수적으로 매개변수는 지역 슬롯으로 접근되므로 전역 dict 조회보다 빠르다는 이점도 있다.
부족한 내용이나 잘못된 정보가 있다면 jeong.jinyeong95@gmail.com으로 연락 바랍니다.