Python
기본적인 파이썬 모범 사례에 대해서 다룹니다.
변수
이름
- 변수 이름은 누구나 쉽게 알아볼 수 있도록 줄여 쓰지 않습니다.
- 지역 변수는
snake_case를 사용합니다. - 전역 변수는
SCREAMING_SNAKE_CASE를 사용합니다. - 멤버 변수는 공개된 경우
snake_case, 공개되지 않은 경우_private_snake_case를 사용합니다.
예시
Good 👍
ONE_DAY = 86400
VOICE_TIPS_CACHE_KEY = "VoiceTip_{country}"
class GetVoiceTips:
def call(self):
voice_tips = cache.get_or_set(
key=VOICE_TIPS_CACHE_KEY.format(self._country),
default=VoiceTip.objects.filter(country=self._country),
timeout=ONE_DAY
)
Bad 👎
d = {'data': [{'a': 'b'}, {'b': 'c'}, {'c': 'd'}], 'texts': ['a', 'b', 'c']}
for k, v in d.iteritems():
if k == 'data':
for i in v:
# Do you know what are you iterating now?
for k2, v2 in i.iteritems():
print(k2, v2)
함수
이름
- 함수 이름은
snake_case를 사용합니다. - 멤버 함수는 공개된 경우
snake_case, 공개되지 않은 경우_private_snake_case를 사용합니다.
예시
def ms_to_datetime(ms: int):
utc = datetime.utcfromtimestamp(date_ms / 1000.0)
return pytz.utc.localize(utc)
class GetIncomingLikesRecommendation:
def call(self, user):
recommendation = self._find_incoming_likes_recommendation(user)
if recommendation is None:
recommendation = self._create_recommendation(user)
return recommendation
def _find_incoming_likes_recommendation(self, user):
yesterday = timezone.now() - timedelta(days=1)
yesterday_noon = yesterday.replace(
hour=12, minute=0, second=0, microsecond=0
)
return SwipeLikeHistoryRecommendation.objects.filter(
user=user,
created_at__gte=yesterday_noon
)
문서화
함수에는 가능한 Google 스타일 Docstrings를 작성합니다.
예시
def convert_m3u8_to_mp4a(
self,
input_file_path: str,
output_file_path: str,
copy: bool = False,
params: AudioConversionParams = WHISPER_OPTIMIZED_PARAMS,
) -> ConversionResult:
"""m3u8 파일을 mp4a로 변환합니다.
Args:
input_file_path: m3u8 파일 경로
output_file_path: 변환된 mp4a 파일 경로
copy: 원본 파일 복사 여부 (기본값: False - Whisper 최적화 변환)
params: 오디오 변환 매개변수 (기본값: Whisper 최적화 설정)
Returns:
ConversionResult: 변환 결과
내려가기 규칙
코드를 위에서 아래로 읽을 때, 추상화 수준이 점진적으로 낮아지도록 작성합니다. 쉽게 설명하면, 함수는 정의되기 전에 사용되어야 합니다.
예시
Good 👍
class BanUsers:
def call(self, reason_id: str):
self._set_nickname_as_banned()
self._create_user_control(reason_id)
def _set_nickname_as_banned(self):
self.user.nickname = f"{self.nickname}{BAN_KEYWORD}"
self.user.save()
def _create_user_control(self, reason_id: str):
UserControl.objects.create(user=self.user, reason_id=reason_id)
Bad 👎
class BanUsers:
def _set_nickname_as_banned(self):
self.user.nickname = f"{self.nickname}{BAN_KEYWORD}"
self.user.save()
def _create_user_control(self, reason_id: str):
UserControl.objects.create(user=self.user, reason_id=reason_id)
def call(self, reason_id: str):
self._set_nickname_as_banned()
self._create_user_control(reason_id)
self.save()
참조
함수를 동적으로 참조하지 않습니다. 정적 분석과 디버깅을 어렵게 만드는 원인이 됩니다.
예시
Bad 👎
for tag_type, tags in self.swipe_user_tag_map.items():
for tag in tags:
method_name = f"_is_{tag}"
if hasattr(self, method_name):
methods_map[tag_type][tag] = getattr(self, method_name)
else:
raise ValueError(
f"Missing method for tag: {tag} (expected method {method_name})"
)
return methods_map
반환값
서로 다른 타입의 값을 반환하지 않습니다.
예시
Good 👍
def ask(prompt: str):
llm_service = AzureLLMService()
json_str = llm_service.query(prompt)
return json.loads(json_str)
Bad 👎
def ask(prompt: str):
llm_service = AzureLLMService()
json_str = llm_service.query(prompt)
if self.response_format == .JSON:
return json.loads(json_str)
else:
return json_str
클래스
이름
- 클래스 이름은
PascalCase를 사용합니다. - 단, 클래스 이름에 약어가 들어가는 경우에는 이를 모두 대문자로 유지합니다.
예시
Good 👍
class ChangeRepresentativeImage:
pass
class HTTPClient:
pass
Bad 👎
class SttService:
pass
서드 파티 API
문서화
외부 서비스를 사용하는 경우, 공식 문서 링크를 달아둡니다.
예시
Good 👍
class AzureLLMService(LLMService):
"""
openai-python SDK AzureOpenAI 클래스
- https://github.com/openai/openai-python/blob/main/examples/azure_ad.py
Azure OpenAI deployment list
- https://ai.azure.com/resource/deployments
"""
Bad 👎
class S3Service:
def __init__(
self,
region_name: str = "ap-northeast-2",
aws_access_key_id: str = None,
aws_secret_access_key: str = None,
):
서비스 객체
외부 서비스를 사용할 때는 {Service name}Service 클래스를 만들어 구현 세부사항을 숨깁니다.
예시
Good 👍
class CloudflareSTTService(STTService):
def transcribe(
self,
audio_file_path: str,
language_code: Optional[str] = None,
*,
task: Optional[str] = None,
vad_filter: Optional[bool] = None,
initial_prompt: Optional[str] = None,
prefix: Optional[str] = None,
**kwargs,
) -> TranscriptionOutput:
"""
음성 파일을 텍스트로 변환
Args:
audio_file_path: 오디오 파일 경로
language_code: 오디오 언어
task: 작업 유형 (transcribe/translate)
vad_filter: Voice Activity Detection 필터 사용 여부
initial_prompt: 초기 프롬프트
prefix: 접두사
**kwargs
Returns:
TranscriptionOutput: 통일된 형태의 전사 결과
"""
# 파일 유효성 검증
self._validate_audio_file(audio_file_path)
try:
with open(audio_file_path, "rb") as audio_file:
# API 요청 데이터 구성
whisper_input = CloudflareWhisperInput(
language=language_code,
task=task,
vad_filter=vad_filter,
initial_prompt=initial_prompt,
prefix=prefix,
)
# API 호출
response = requests.post(
self.url,
files={"file": audio_file},
data=whisper_input.model_dump(),
headers={
"Authorization": f"Bearer {self.api_key}",
},
)
response.raise_for_status()
cf_response = CloudflareWhisperOutput.model_validate(response.json())
return TranscriptionOutput.from_cloudflare_whisper(cf_response)
except FileNotFoundError as e:
logger.error(f"File not found: {audio_file_path}: {str(e)}")
raise
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error during transcription: {str(e)}")
raise
except requests.exceptions.RequestException as e:
logger.error(f"Request failed for {audio_file_path}: {str(e)}")
raise
except Exception as e:
logger.error(f"Transcription failed for {audio_file_path}: {str(e)}")
raise
캐시
키 구분자
캐시 키 구분자는 :을 사용합니다.
기타
데이터 전송 객체
DTO 작성에는 Pydantic을 사용합니다. DTO는 사용하는 파일 내에 같이 작성합니다.
전체 임포트
가독성과 명시성을 위해 필요한 의존성만 가져옵니다.
예시
Good 👍
from math import sqrt, pi
Bad 👎
from math import *
순환 참조 해결
임포트 구문은 항상 파일의 시작 지점에 작성합니다. 단, 순환 참조 문제가 있는 경우에는 사용하는 코드 바로 위에 작성합니다.
유틸리티 함수
외부 의존성이 없고, 핵심 도메인 로직이 아니면서, 반복되는 로직은 common/utils 폴더에 작성합니다.