Double Underscore란?
Python에서는 이름 앞뒤에 underscore _가 붙는 경우가 많다.
그중 __name__, __init__, __str__처럼 양쪽에 double underscore가 붙은 이름을 흔히 dunder라고 부른다.
dunder는 double underscore를 줄인 말이다.
__name__
__init__
__main__
__str__
__len__
이런 이름은 Python이 특별한 의미로 사용하는 경우가 많다.
따라서 일반 변수나 함수 이름을 만들 때 직접 __something__ 형태를 사용하는 것은 피하는 것이 좋다.
underscore naming 정리
Python에서 underscore는 위치에 따라 의미가 다르다.
| 형태 | 예시 | 의미 |
|---|---|---|
_name | _count | 내부용 변수라는 관례 |
name_ | class_ | 예약어와 이름 충돌을 피하기 위한 suffix |
__name | __value | class 내부 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과 연결되어 있다. 핵심은 다음과 같다.
__name__은 현재 module의 이름을 담는다.if __name__ == "__main__"은 직접 실행될 때만 코드를 실행하기 위한 패턴이다.__init__,__str__,__len__같은 이름은 Python이 특정 상황에서 자동으로 호출한다.- 앞에만 double underscore가 붙은
__value는 name mangling 대상이다. - 새로운
__something__이름을 직접 만들어 쓰는 것은 피하는 것이 좋다.
dunder method를 이해하면 Python 객체가 len(), print(), with, for, == 같은 문법과 어떻게 연결되는지 이해하기 쉬워진다.