jeongjinyeong's homepage

2026-09-06

파이썬에서 self를 써야 하는 이유 — 스코프 사슬과 객체 사슬

파이썬에서 self를 써야 하는 이유가 뭘까

클래스 안에서 인스턴스의 속성이나 메서드에 접근하려면 반드시 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인 것이다 — 스코프 사슬이 아니라 객체의 속성 사슬을 타는 것.

클래스 객체의 속성 dict

파이썬에서 클래스도 객체이다.

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로 스코프를 확장하며 찾게 되는데, 해당 스코프 사슬에는 클래스 정보가 포함되지 않는다.

예외

추가로 인스턴스의 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으로 연락 바랍니다.