Intro
기존 코드의 하위 호환을 위해서, module level의 __getattr__을 쓰게 되었다.
오늘은 module level의 __getattr__에 대해서 정리해보려고 한다.
Contents
# constant.py
class Settings:
def __init__(self):
self.ENV_1 = "test1"
self.ENV_2 = "test2"
setting = None
def get_setting():
global setting
if setting is None:
setting = Settings()
return setting
def __getattr__(name: str):
return getattr(get_setting(), name)
# main.py
from constant import ENV_1
if __name__ == "__main__":
print(ENV_1)

python에서는 놀랍게도 위와 같은 코드가 정상적으로 동작한다.
왜 정상적으로 동작하는지에 대해서는 python의 import 순서 부터 이해해야한다.
python import 동작
1. module load
기본적으로는 python은 import를 만나면, 모듈 로드를 시도한다.
모듈을 로드 할때, Top-Level Code(함수나 클래스 정의 밖에 있는 모듈의 최상위 레벨 코드)를 실행 시킨다.
2. ENV_1 을 찾으려고 시도(attribute lookup)
Top-Level code에서 Settings 정의, setting = None, get_setting, __getattr__이 모듈 네임스페이스 등록되었지만, ENV_1 이라는 클래스 정의, 함수, 전역 변수 가 없는 상태이다.
python은 기본적으로 constant.__dict__(모듈 전역 네임스페이스)에서 ENV_1 키를 찾으려고 하지만 없기 때문에, AttributeError가 발생한다.
3. fallback
AttributeError가 발생하면, python은 __getattr__가 정의 되어 있는지 확인하고, 그걸 호출해서 값을 제공한다.
정리 하자면,
python은 기존 방식대로 찾아보고, 없으면 __getattr__을 호출해서 값을 제공한다는 뜻이다.
상황 설명
위의 상황은 환경 변수와 같은 싱글톤을 사용할 만한 상황에서, 기존에는 전역변수로 되어 있던 데이터를 캡슐화하여 숨긴다.
하지만, 기존 코드가 from a import REDIS_ENV 와 환경변수를 호출 하고 있었다면, 해당 import 코드들을 바꾸지 않고, 하위 호환을 할 수 있다는 것이다.
문제점
당연하지만 여러 문제점이 있다.
1. typing 문제
__getattr__의 가장 큰 문제점은 typing이 사라진다. 동적으로 호출하니까 당연한 이야기이긴 하다.
하지만, 그래서 python 개발자들 사이에서 사용에 대해서 많은 말들이 오간다.
(python은 원래 duck typing이다 vs 협업을 위해 빡세게 타입을 쓰자)
타입 체커를 이용해서, from typing import TYPE_CHECKING 방식이 있지만, 일반적으로 개발자들이 원하는 typing과는 거리가 멀다.
2. 값 고정
from constant import ENV_1는 스냅샷이여서, import를 하는 순간 값을 복사해서 ENV_1에 바인딩 하는 과정을 거친다.
즉, 다른 코드로 인해서 constant내의 setting 전역변수가 변경되더라도, ENV_1의 값은 그대로이다.
하지만, 아래와 같이 사용하면, 매 접근 시마다, constant 모듈에서 attribute lookup이 다시 일어나서, 동적으로 사용이 가능하다.
import constant
print(constant.ENV_1)
3. 굳이?
이렇게 쓰는 경우는 정말 정말 적다.
활용
# kill_switch.py
from pathlib import Path
BASE_DIR = Path(__file__).parent
STOP_FILE = BASE_DIR / "stop"
class Flags:
def __getattr__(self, name: str) -> bool:
if name == "STOP":
return STOP_FILE.exists()
raise AttributeError(name)
_flags = Flags()
def __getattr__(name: str):
return getattr(_flags, name)
# main.py
import time
import kill_switch
if __name__ == "__main__":
while True:
if kill_switch.STOP:
print("정지 신호 감지")
break
print("실행중")
time.sleep(1)
실행 위치에 stop 이라는 이름의 파일이 생성되면, 무한 루프를 탈출하도록 kill switch를 만들어 보았다.
'IT' 카테고리의 다른 글
| [개발] AI로 짠 코드, 내가 리뷰하는 건 개발자인가 AI인가 (0) | 2025.12.29 |
|---|---|
| [TimescaleDB] hyper table에 indexing을 걸면? (0) | 2025.12.20 |
| [백엔드] timestamp를 압축 시켜보자 (0) | 2025.12.18 |
| [백엔드] timestamp의 Z와 +00:00 이야기 (0) | 2025.12.17 |
| [TCP] 서버는 TCP 소켓 연결을 몇개 까지 할 수 있을 까? (0) | 2025.12.12 |