← Back to Blog

[Python] Double Underscore and Dunder Methods

computer science > programming

2026-07-054 min read

#computer-science #programming #python #dunder #magic-method

Double Underscore란?

Python에서는 이름 앞뒤에 underscore _가 붙는 경우가 많다. 그중 __name__, __init__, __str__처럼 양쪽에 double underscore가 붙은 이름을 흔히 dunder라고 부른다.

dunderdouble underscore를 줄인 말이다.

__name__
__init__
__main__
__str__
__len__

이런 이름은 Python이 특별한 의미로 사용하는 경우가 많다. 따라서 일반 변수나 함수 이름을 만들 때 직접 __something__ 형태를 사용하는 것은 피하는 것이 좋다.


underscore naming 정리

Python에서 underscore는 위치에 따라 의미가 다르다.

형태예시의미
_name_count내부용 변수라는 관례
name_class_예약어와 이름 충돌을 피하기 위한 suffix
__name__valueclass 내부 name mangling
__name____init__Python이 특별히 사용하는 dunder name
__ = func()사용하지 않는 값 또는 임시 변수

이 글에서는 특히 __name__처럼 양쪽에 double underscore가 붙는 문법을 중심으로 정리한다.


name

__name__은 현재 module의 이름을 담고 있는 특별한 변수이다.

Python 파일을 직접 실행하면 해당 파일의 __name__ 값은 "__main__"이 된다.

예를 들어 main.py가 있다고 하자.

print(__name__)

이 파일을 직접 실행하면 다음처럼 출력된다.

python main.py
__main__

반대로 다른 파일에서 import하면 __name__은 module 이름이 된다.

# app.py
import main

이때 main.py__name__"main"이다.


if name == "main"

Python에서 자주 보는 문법이 있다.

if __name__ == "__main__":
    main()

이 코드는 현재 파일이 직접 실행될 때만 main()을 호출하겠다는 의미이다.

예를 들어 다음 파일을 보자.

def add(a: int, b: int) -> int:
    return a + b


def main():
    print(add(2, 3))


if __name__ == "__main__":
    main()

직접 실행하면 main()이 실행된다.

python calculator.py
5

하지만 다른 파일에서 import하면 main()은 실행되지 않는다.

from calculator import add

print(add(10, 20))

이 패턴을 사용하면 하나의 파일을 다음 두 방식으로 모두 사용할 수 있다.

직접 실행 가능한 script
다른 파일에서 import 가능한 module

init

__init__은 class instance가 만들어질 때 호출되는 초기화 method이다.

class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

객체를 생성하면 __init__이 호출된다.

user = User("Alice", 20)

print(user.name)
print(user.age)

__init__은 constructor라고 설명되는 경우가 많지만, 정확히는 이미 생성된 객체를 초기화하는 method이다. 객체 생성 자체는 __new__가 담당하고, __init__은 초기 상태를 설정한다.

일반적으로는 __new__를 직접 작성할 일은 많지 않고, 대부분 __init__만 사용한다.


__str__과 repr

__str____repr__은 객체를 문자열로 표현할 때 사용된다.

class User:
    def __init__(self, name: str):
        self.name = name

    def __str__(self):
        return f"User: {self.name}"

    def __repr__(self):
        return f"User(name={self.name!r})"

str()이나 print()__str__을 사용한다.

user = User("Alice")
print(user)
User: Alice

반면 interactive shell, list 출력, debug 표현에서는 __repr__이 사용되는 경우가 많다.

users = [User("Alice")]
print(users)
[User(name='Alice')]

보통 기준은 다음과 같다.

method목적
__str__사용자에게 보여줄 읽기 쉬운 문자열
__repr__개발자가 디버깅할 때 볼 명확한 표현

len

__len__len() 함수가 호출될 때 사용된다.

class Team:
    def __init__(self):
        self.members = []

    def add(self, name: str):
        self.members.append(name)

    def __len__(self):
        return len(self.members)

사용 예시는 다음과 같다.

team = Team()
team.add("Alice")
team.add("Bob")

print(len(team))
2

__len__을 구현하면 직접 만든 객체도 Python 기본 container처럼 사용할 수 있다.


getitem

__getitem__은 대괄호 접근을 정의한다.

class Team:
    def __init__(self):
        self.members = []

    def add(self, name: str):
        self.members.append(name)

    def __getitem__(self, index: int):
        return self.members[index]

이제 객체에 []를 사용할 수 있다.

team = Team()
team.add("Alice")
team.add("Bob")

print(team[0])
Alice

__getitem__은 list 같은 container를 만들 때 자주 사용된다.


__iter__와 next

객체를 for 문에서 순회 가능하게 만들려면 iterator protocol을 구현해야 한다.

가장 단순한 방식은 __iter__에서 내부 iterable을 반환하는 것이다.

class Team:
    def __init__(self):
        self.members = []

    def add(self, name: str):
        self.members.append(name)

    def __iter__(self):
        return iter(self.members)

사용 예시는 다음과 같다.

team = Team()
team.add("Alice")
team.add("Bob")

for member in team:
    print(member)
Alice
Bob

직접 iterator를 만들 때는 __next__까지 구현할 수 있다. 하지만 대부분은 내부 list, tuple, dict의 iterator를 반환하는 방식으로 충분하다.


__eq__와 비교 연산

__eq__== 연산자를 정의한다.

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False

        return self.x == other.x and self.y == other.y

사용 예시는 다음과 같다.

p1 = Point(1, 2)
p2 = Point(1, 2)

print(p1 == p2)
True

크기 비교는 다음 method로 정의할 수 있다.

method연산자
__lt__<
__le__<=
__gt__>
__ge__>=

직접 전부 구현하기 번거롭다면 functools.total_ordering이나 @dataclass(order=True)를 고려할 수 있다.


__enter__와 exit

with 문에서 사용하는 context manager는 __enter__, __exit__으로 구현된다.

class FileLogger:
    def __init__(self, path: str):
        self.path = path
        self.file = None

    def __enter__(self):
        self.file = open(self.path, "w")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.file.close()

    def write(self, message: str):
        self.file.write(message)

사용 예시는 다음과 같다.

with FileLogger("log.txt") as logger:
    logger.write("hello")

with block을 벗어나면 __exit__이 호출된다. 파일 닫기, lock 해제, DB connection 정리 같은 작업에 유용하다.


call

__call__을 구현하면 객체를 함수처럼 호출할 수 있다.

class Multiplier:
    def __init__(self, factor: int):
        self.factor = factor

    def __call__(self, value: int) -> int:
        return value * self.factor

사용 예시는 다음과 같다.

double = Multiplier(2)

print(double(10))
20

상태를 가진 함수 객체가 필요할 때 사용할 수 있다.


dict

대부분의 Python 객체는 instance attribute를 __dict__에 저장한다.

class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
user = User("Alice", 20)
print(user.__dict__)
{'name': 'Alice', 'age': 20}

__dict__를 보면 객체가 어떤 attribute를 가지고 있는지 확인할 수 있다. 다만 내부 구현에 가까운 속성이므로, 일반적인 application code에서 자주 직접 수정하는 것은 좋지 않다.


slots

__slots__는 객체가 가질 수 있는 attribute를 제한한다.

class User:
    __slots__ = ("name", "age")

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

이제 정의되지 않은 attribute를 추가할 수 없다.

user = User("Alice", 20)
user.email = "alice@example.com"  # error

__slots__를 사용하면 __dict__가 만들어지지 않아 memory를 줄일 수 있다. 하지만 유연성이 줄어들기 때문에 필요한 경우에만 사용하는 것이 좋다.


__로 시작하지만 양쪽은 아닌 경우

__name__처럼 양쪽에 double underscore가 붙는 이름과, __value처럼 앞에만 double underscore가 붙는 이름은 다르다.

앞에만 double underscore가 붙으면 name mangling이 발생한다.

class User:
    def __init__(self):
        self.__token = "secret"

Python은 내부적으로 이름을 다음처럼 바꾼다.

_User__token

그래서 외부에서 user.__token으로 바로 접근하기 어렵다.

user = User()
print(user.__token)  # error

name mangling은 완전한 private을 만드는 기능은 아니다. 상속 관계에서 이름 충돌을 줄이기 위한 장치에 가깝다.

Python에서 내부용 attribute는 보통 single underscore를 사용한다.

class User:
    def __init__(self):
        self._token = "secret"

직접 dunder 이름을 만들지 않기

__something__ 형태는 Python이 특별한 용도로 예약해 사용하는 이름이다. 따라서 직접 새로운 dunder method를 만들어 사용하는 것은 피하는 것이 좋다.

예를 들어 다음처럼 프로젝트 내부 약속으로 dunder method를 만드는 것은 권장되지 않는다.

class Task:
    def __run_task__(self):  # bad
        pass

이런 이름은 나중에 Python 또는 library가 특별한 의미로 사용할 가능성이 있다. 일반적인 내부 method라면 single underscore를 사용한다.

class Task:
    def _run_task(self):
        pass

자주 보는 dunder 목록

자주 보는 dunder name은 다음과 같다.

이름의미
__name__module 이름
__main__직접 실행된 module 이름
__init__instance 초기화
__repr__개발자용 문자열 표현
__str__사용자용 문자열 표현
__len__len() 동작
__getitem__obj[index] 동작
__iter__iteration 동작
__eq__== 동작
__enter__with 진입
__exit__with 종료
__call__객체 호출
__dict__instance attribute 저장소
__slots__허용 attribute 제한

정리

Python의 double underscore 문법은 Python data model과 연결되어 있다. 핵심은 다음과 같다.

  1. __name__은 현재 module의 이름을 담는다.
  2. if __name__ == "__main__"은 직접 실행될 때만 코드를 실행하기 위한 패턴이다.
  3. __init__, __str__, __len__ 같은 이름은 Python이 특정 상황에서 자동으로 호출한다.
  4. 앞에만 double underscore가 붙은 __value는 name mangling 대상이다.
  5. 새로운 __something__ 이름을 직접 만들어 쓰는 것은 피하는 것이 좋다.

dunder method를 이해하면 Python 객체가 len(), print(), with, for, == 같은 문법과 어떻게 연결되는지 이해하기 쉬워진다.