1. 명시적이고 의미 있는 이름 (Explicit and Meaningful Names)
코드를 작성할 때 가장 기본적이고 중요한 관례 중 하나는 함수, 변수, 인수에 명시적이고 의미 있는 이름을 부여하는 것입니다. 파이썬 개발자들은 좋은 이름을 통해 많은 이익을 얻을 수 있습니다.
1.1. 명확하고 설명적인 함수 이름
명확하고 설명적인 함수 이름은 코드를 더 읽기 쉽고 이해하기 쉽게 만듭니다.
1.2. 잘 지어진 함수 이름의 중요성
- 자기 문서화 코드: 잘 지어진 함수 이름은 자기 문서화 코드의 역할을 하며, 광범위한 주석의 필요성을 줄이고 코드 유지 관리를 향상시킵니다.
- 디버깅과 문제 해결의 용이성: 의미 있는 이름은 디버깅과 문제 해결 과정을 덜 번거롭게 만듭니다. 명확한 함수 이름은 예상 입력, 출력 또는 동작에 대한 단서를 제공하므로 문제를 식별하고 수정하기 쉽게 만듭니다.
1.3. 함수, 인수, 변수의 이름 선택
함수뿐만 아니라 인수와 함수의 모든 변수에 대해서도 신중하게 이름을 선택해야 합니다.
어떻게 좋은 이름을 선택할 수 있을까요?
- 명확성: 이름은 해당 요소의 기능과 역할을 명확하게 반영해야 합니다.
- 일관성: 코드 전반에 걸쳐 일관된 명명 규칙을 사용하면 읽기 쉬운 코드가 됩니다.
- 의미 전달: 이름 자체가 해당 요소의 주요 기능과 의도를 설명해야 합니다.
- 이러한 지침은 코드의 가독성과 유지보수성을 크게 향상시키며, 개발 프로세스를 더 효과적이고 효율적으로 만듭니다.
좋은 이름을 선택하는 것은 때때로 쉽지 않을 수 있습니다. 다음 팁들이 유용할 것입니다:
1.1 함수 분할하기
- "and" 단어가 보일 때 함수 분할: 예를 들어,
create_post_and_set_category()
라는 함수가 있다면, 왜 이를create_post()
와set_category()
로 분할하지 않을까요? 이렇게 하면 함수 이름을 더 짧고 명확하게 만들 수 있습니다. - 작고 단일 목적의 함수 유지: 이것은 나중에 이야기할 또 다른 모범 사례와도 일치합니다.
1.2 스네이크 케이스 사용하기
- 파이썬의 일반적인 관례 따르기:
choose_name
을 사용하고chooseName
대신 사용하지 마세요.
1.3 동작을 나타내는 함수 이름 사용하기
- 동사 또는 동사구 사용: 예를 들어,
greeting(say_hello_to: str)
대신greet(name: str)
을 사용해야 합니다.
1.4 인수 이름으로 함수의 가독성 향상시키기
- 보다 명확한 인수 이름 사용:
send_notification_to_employee(emp)
대신send_notification_to(employee)
를 사용해보세요.
1.5 어휘 일관성 유지하기
같은 일반 목적을 가진 함수간 일관성 유지: 함수 하나를
calculate
라고 명명했다면, 비슷한 일반 목적을 가진 다른 함수는compute
대신 같은 용어를 사용해야 합니다.이러한 지침을 따르면 코드의 일관성과 가독성이 향상되어 유지보수와 협업이 더 쉬워질 것입니다.
2. 바퀴를 다시 발명하지 마세요 (Avoid Reinventing the Wheel)
- Python의 강력한 내장 함수 및 라이브러리 활용: Python은 많은 강력한 내장 함수와 라이브러리를 제공하며, 이러한 기능을 활용하면 시간을 절약하고 기존에 해결된 문제를 다시 해결하는 수고를 덜 수 있습니다.
- 이러한 함수 및 라이브러리 학습과 활용: 내장 함수와 라이브러리를 익히고 활용하면 시간을 절약하고 코드 재사용을 촉진할 뿐만 아니라 모범 사례를 적용하고, 표준 또는 일반적으로 사용되는 함수와 라이브러리에 익숙한 다른 개발자와의 협업도 용이해집니다.
-- 이 방법은 개발 과정에서 효율성을 높이고, 코드의 품질을 향상시키며, 개발 커뮤니티와의 일관성을 유지하는 데 중요한 역할을 합니다.
물론입니다! 아래는 블로그 포스팅 형식으로 정리된 내용입니다.
3. 작고 단일 목적 함수 (Small and Single-Purpose)
함수는 하나의 일만 수행하고, 잘 수행해야 합니다. 작고 단일 목적의 함수는 읽기 쉽고 이해하기 쉽습니다. 더불어 재사용성이 높으며 테스트 가능성도 고려해야 합니다. 수많은 작업을 수행하는 거대한 함수의 경우 단위 테스트 작성이 악몽이 될 수 있습니다.
3.1 나쁜 예
def process_data_like_a_god(data):
# 코드 블록 1: 데이터 전처리
# 코드 블록 2: 데이터 필터링
# 코드 블록 3: 통계 계산
# 코드 블록 4: 보고서 생성
pass
3.2 더 나은 예
def process_data(data):
# Step 1: Preprocess the data
preprocessed_data = preprocess_data(data)
# Step 2: Filter the data
filtered_data = filter_data(preprocessed_data)
# Step 3: Calculate statistics
stats = calculate_statistics(filtered_data)
# Step 4: Generate report
generate_report(stats)
def preprocess_data(data):
# Perform preprocessing operations
# ...
return preprocessed_data
def filter_data(data):
# Apply filtering criteria to the data
# ...
return filtered_data
def calculate_statistics(data):
# Perform statistical calculations
# ...
return stats
def generate_report(stats):
# Generate a report based on statistics
# ...
pass
- 각 작은 함수는 단일 작업을 담당합니다.
3.3 작고 단일 목적 함수를 유지하는 팁
코드 라인 수 세기: 함수가 20라인보다 길다면 너무 길다는 지침을 따라야 합니다. 이 숫자는 절대 규칙이 아닌 지침입니다.
재사용 가능한 코드 추출: 함수 내의 코드 부분이 다른 상황에서도 사용될 수 있다면 별도의 함수로 추출하세요.
과도한 중첩 및 들여쓰기 깊이 피하기: 함수에 여러 레벨의 들여쓰기가 있다면 더 작고 관리하기 쉬운 함수로 분리할 기회가 있을 수 있습니다.
큰 함수를 작은 함수로 리팩터링하면 코드의 가독성과 유지 보수성이 향상되며, 더 나은 코드 구조를 제공할 수 있습니다.
4. 명령과 쿼리 분리 (Separate Commands From Queries)
함수를 작성할 때, 정보를 검색하는 쿼리와 작업을 수행하는 명령 사이에 명확한 구분을 두는 것이 좋습니다.
4.1 명령 (Commands, Actions)
- 명령은 객체나 시스템의 상태를 변경하거나 작업을 수행하는 함수 또는 메서드입니다.
- 데이터베이스 업데이트, 파일 쓰기, 객체의 내부 상태 변경 등의 부작용을 수반할 수 있습니다.
4.2 쿼리 (Queries, Information Retrieval)
- 쿼리는 정보를 검색하거나 질의하는 함수 또는 메서드로, 객체나 시스템의 상태를 변경하지 않아야 합니다.
- 이러한 함수들은 값을 반환하거나 객체나 시스템에 대한 정보를 제공하되 부작용을 일으키지 않아야 합니다.
4.3 예제
- 학생 정보를 다루는 시스템에서 학생을 데이터베이스에 추가하는 작업(명령)과 학생 수를 가져오는 작업(쿼리)을 분리하는 예제입니다.
# 명령 (작업) 함수
def add_student(student_name: str) -> None:
# 데이터베이스나 시스템에 학생을 추가하는 작업 수행
# ...
print(f"Added student: {student_name}")
# 쿼리 (정보 검색) 함수
def get_student_count() -> int:
# 데이터베이스나 시스템에서 학생 수 검색
# ...
count = 10 # 데이터베이스에서 검색된 값으로 가정
return count
- 이러한 소프트웨어 디자인 원칙은 시스템의 상태를 변경하는 함수와 정보를 검색하는 함수 사이의 명확한 경계를 설정합니다. 이로써 코드의 명확성과 유지보수성이 향상됩니다.
5. 필요한 정보만 요청하기 (Only Request Necessary Information)
함수에서 실제로 필요한 정보만 요청하는 것이 좋은 습관입니다. 아래의 예제를 통해 자세히 살펴보겠습니다.
5.1 예제
@dataclass
class Order:
order_id: int
customer_name: str
product_id: int
quantity: int
# Case 1: request too much information
def validate_order_quantity_1(order: Order,
quantity_in_stock: int = 100) -> bool:
# Check if the order meets the validation criteria
if order.quantity <= 0:
return False
if order.quantity > quantity_in_stock:
return False
return True
# Case 2: request only necessary information
def validate_order_quantity_2(quantity: int,
quantity_in_stock: int = 100) -> bool:
# Check if the order meets the validation criteria
if quantity <= 0:
return False
if quantity > quantity_in_stock:
return False
return True
이 예제에서 validate_order_quantity_1
은 Order
클래스의 모든 정보를 요청하나, 실제로는 quantity
정보만 필요합니다. 따라서 validate_order_quantity_2
와 같이 필요한 정보만 전달하는 것이 바람직합니다.
5.2 이점
- 모듈성 증진: 필요한 정보만 요청하면 각 함수를 유지하고 업데이트하기 훨씬 쉽습니다.
- 재사용성: 필요한 정보만 요청하면 코드의 재사용성이 높아집니다. 다른 유형의 주문에도 동일한 검증 함수를 사용할 수 있습니다.
- 복잡성 감소: 함수가 최소한의 입력으로만 작동하면, 함수의 동작과 잠재적인 경계 사례를 파악하기 쉽습니다.
- 보안 향상: 요청하는 정보를 제한함으로써, 민감한 데이터에 대한 접근 또는 노출 위험을 줄일 수 있습니다.
- 테스트 용이성 향상: 제한된 입력만 요청하는 함수의 단위 테스트를 작성할 때 불필요한 종속성이나 복잡성 없이 특정 시나리오와 경계 사례를 다룰 수 있어 감사할 것입니다.
- 이러한 원칙을 따르면 코드의 유지보수성과 재사용성을 향상시킬 수 있으며, 더 안전하고 테스트하기 쉬운 코드를 작성할 수 있습니다.
6. 매개변수의 수를 최소화하기 (Keep the Number of Parameters Minimal)
함수가 가져야 할 매개변수의 최대 수에 대한 엄격한 규칙은 없습니다. 그러나 일반적인 지침으로서 매개변수의 수는 최소로 유지해야 하며 이상적으로는 5개 이하가 좋습니다. 개인적으로 나는 항상 3개 이하로 유지하려고 노력합니다.
6.1 왜 매개변수의 수를 줄여야 하는가?
너무 많은 매개변수를 가진 함수는 함수의 서명을 복잡하게 만들고 가독성을 감소시키며 오류 발생 가능성을 높일 수 있습니다. 또한 함수의 테스트와 유지보수가 어려워질 수 있습니다.
6.2 리팩토링 팁
너무 많은 매개변수가 필요한 경우, 이는 설계 문제를 나타낼 수 있으며 코드를 리팩토링해야 할 필요가 있을 수 있습니다. 아래는 리팩토링을 위한 몇 가지 팁입니다:
함수 책임 재평가: 너무 많은 작업을 담당하는 god-like 함수를 만들지 않아야 합니다. 많은 수의 매개변수가 필요한 함수는 단일 작업 이상을 수행하려고 할 수 있습니다. 이 함수는 더 작고 집중된 함수로 분할되어야 하며, 각각은 더 적은 매개변수를 필요로 합니다.
매개변수 그룹화: 관련 매개변수를 객체나 데이터 구조로 그룹화할 수 있습니다. 이 접근 방식을 사용하면 개별 매개변수의 수를 줄일 수 있으며 더 응집력 있고 직관적인 인터페이스를 제공합니다.
기본 매개변수 설정: 일부 매개변수가 선택 사항이거나 기본값을 가지는 경우 그렇게 정의할 수 있습니다. 이렇게 하면 호출자가 해당 매개변수가 자신의 사용 사례와 관련이 없는 경우 생략할 수 있습니다.
매개변수의 수를 적절하게 관리함으로써, 함수의 복잡성을 줄이고 가독성과 유지보수성을 향상시킬 수 있습니다.
7. 가변 기본 매개변수 사용시 주의하기 (Be Careful With Mutable Default Parameters)
함수에서 기본 매개변수로 가변 객체(예: 리스트, 딕셔너리)를 사용하는 것은 피해야 합니다. 불변 객체(예: None, 정수, 문자열, 튜플)만 기본 매개변수로 사용해야 하며, 필요한 경우 기본 매개변수로 가변 객체를 사용하려면 함수 내부에서 생성해야 합니다.
7.1 가변 기본 매개변수의 문제점
아래 예시는 가변 기본 매개변수와 관련된 문제점을 명확하게 보여줍니다:
def add_item(item, items=[]):
items.append(item)
print(items)
add_item("Apple") # 출력: ['Apple']
add_item("Banana") # 출력: ['Apple', 'Banana']
add_item()
함수는 기본 매개변수로 items
를 빈 리스트로 초기화합니다. 그러나 기본 매개변수가 가변이므로, 리스트가 여러 함수 호출 간에 공유됩니다. 따라서 add_item()
을 호출할 때마다 같은 리스트에 항목이 추가되어 예기치 않은 동작이 발생합니다.
7.2 문제 해결 방법
이 문제를 해결하려면 다음과 같이 코드를 리팩토링해야 합니다:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
print(items)
add_item("Apple") # 출력: ['Apple']
add_item("Banana") # 출력: ['Banana']
이 업데이트된 버전에서 add_item()
함수는 items
매개변수가 None인지 확인하고, 그렇다면 새로운 빈 리스트를 생성합니다. 이렇게 하면 각 함수 호출에 대해 리스트의 새 인스턴스가 사용되어 가변 기본 매개변수와 관련된 공유 상태와 예기치 않은 동작을 피할 수 있습니다.
가변 기본 매개변수는 여러 호출 간에 상태를 공유할 수 있으므로 주의해야 합니다. 불필요한 혼란과 버그를 피하려면 이러한 문제를 이해하고 적절히 대응해야 합니다.
8. 플래그 인수 피하기 (Avoid Flag Arguments)
플래그 인수란 함수 매개변수로 이진 상태(보통 true 또는 false)를 나타내며, 함수 내에서 특정 조건이나 동작을 활성화 또는 비활성화 할 것인지를 나타냅니다.
8.1 플래그 인수의 예시
아래 예시에서 process_data()
함수는 플래그 인수 use_cache
를 가지며, 이것은 데이터를 캐시된 결과를 사용하여 처리할 것인지 아닌지를 결정합니다. 이 함수는 플래그 인수의 값에 따라 다른 로직을 수행합니다.
def process_data(data, use_cache=False):
if use_cache:
# 캐시된 결과를 사용하여 데이터 처리 로직
print("Processing data using cache:", data)
else:
# 캐시를 사용하지 않고 데이터 처리 로직
print("Processing data without cache:", data)
8.2 플래그 인수의 문제점
플래그 인수가 유용할 수 있는 경우도 있지만, 대부분의 경우 이러한 인수의 단점이 장점을 압도합니다. 예를 들어, 플래그 인수는 함수의 복잡성을 증가시킬 수 있습니다. 조건 분기와 분기 로직이 코드를 읽고 이해하기 어렵게 만들 수 있습니다. 더구나 이러한 방식은 함수나 함수 간에 종속성을 만들 수 있어, 플래그 인수의 동작 변경이 여러 곳에서 수정을 필요로 할 수 있으며, 코드의 얽힘을 초래할 수 있습니다.
8.3 리팩토링을 통한 해결
플래그 인수를 가진 함수는 코드 리팩토링의 기회를 나타낼 수 있습니다. 하나의 접근 방법은 기능을 명확한 목적을 가진 별도의 함수로 분리하는 것입니다. 이렇게 하면 코드 가독성이 향상되고 각 함수의 의도를 더 명확하게 만들 수 있습니다.
예를 들어, 위의 예시에서 플래그 인수를 제거하고 두 개의 별도 함수로 나눌 수 있습니다:
def process_data(data):
# 캐시를 사용하지 않고 데이터 처리 로직
print("Processing data without cache:", data)
def process_data_with_cache(data):
# 캐시된 결과를 사용하여 데이터 처리 로직
print("Processing data using cache:", data)
이제 코드가 더 모듈화되어 이해하기 쉬워졌습니다. 또한 이러한 방식으로 코드를 리팩토링하면 관심사의 분리를 더 잘 이루고 단일 책임 원칙을 준수할 수 있으며, 미래에 동작을 확장하거나 수정할 때 다른 코드 부분에 영향을 미치지 않게 됩니다.
9. 의존성 주입 사용하기 (Use Dependency Injections)
객체를 동일한 장소에서 생성하고 사용하는 것은 바람직하지 않습니다. 의존성 주입은 이러한 문제를 해결하는 해법입니다.
9.1 문제 상황
아래 예시에서 EmailFormatter
객체는 format_message
함수 내에서 생성되고 사용됩니다. 만약 이메일 대신 WhatsApp으로 메시지를 포맷해야 한다면 새로운 함수를 작성해야 할 것입니다.
class EmailFormatter:
def __init__(self):
pass
def format(self, messages: list[str]):
pass
def format_message(messages: list[str]) -> None:
filtered_messages = [msg for msg in messages if len(msg) >= 5]
formatter = EmailFormatter()
formatter.format(filtered_messages)
9.2 의존성 주입을 통한 해결
객체를 동일한 장소에서 생성하고 사용하는 대신, 아래 코드와 같이 해당 객체를 함수에 주입할 수 있습니다.
from abc import ABC, abstractmethod
class IFormatter(ABC):
@abstractmethod
def format(messages: list[str]):
pass
class EmailFormatter(IFormatter):
def __init__(self):
pass
def format(self, messages: list[str]):
pass
class WhatsAppFormatter(IFormatter):
def __init__(self):
pass
def format(self, messages: list[str]):
pass
def format_message(messages: list[str], formatter: IFormatter) -> None:
filtered_messages = [msg for msg in messages if len(msg) >= 5]
formatter.format(filtered_messages)
format_message(messages=['Hello', 'Hi', 'My dear'], formatter=EmailFormatter())
9.3 의존성 주입의 장점
포맷터 의존성을 매개변수로 주입함으로써, 특정 문자열 포맷터 구현과 함수를 분리합니다. 이로써 텍스트 포맷터를 쉽게 전환하거나 다른 형식의 인스턴스를 제공할 수 있으며 format_message()
함수를 수정할 필요가 없게 됩니다. 또한 함수가 더 테스트하기 쉬워지며, 테스트 중 의존성을 모의 객체로 쉽게 대체할 수 있게 됩니다.
10. 부분 함수 사용하기 (Use partial Function)
functools
모듈의 partial
함수를 사용하면 호출 가능한 객체의 일부 인수를 고정하여 부분 함수를 생성할 수 있습니다. 이를 통해 기존의 함수를 확장하거나 특정 인수를 고정한 새로운 함수를 만들 수 있습니다.
10.1 부분 함수의 정의
partial
함수는 두 개 이상의 인수를 받으며, 첫 번째 인수는 부분 적용하려는 호출 가능한 객체(함수 또는 메소드)이고, 나머지 인수는 호출 가능한 객체의 고정된 인수를 나타냅니다.
10.2 예제
아래 예제를 통해 partial
의 마법을 더 잘 이해해봅시다.
from functools import partial
def calculate(a, b, c):
return (a * b) + c
# `calculate` 함수를 사용하여 부분 함수를 생성하고 `c` 인수를 2로 고정
calculate_fn = partial(calculate, c=4)
# 나머지 `a`와 `b` 인수와 함께 부분 함수를 호출
result = calculate_fn(3, 2)
print(result) # 출력: 10
이 코드에서 calculate
함수는 a
와 b
를 곱하고 결과에 c
의 값을 더합니다. partial
함수를 사용하여 c
인수를 4로 고정한 새로운 함수인 calculate_fn
을 생성합니다. 그 후 calculate_fn(3, 2)
를 호출하여 나머지 인수 3과 2를 전달하면, 계산식은 ((3 \times 2) + 4)가 되며, 결과는 10이 됩니다.
10.3 부분 함수의 활용
부분 함수를 사용하면 기존 함수를 재사용하면서도 특정 인수를 고정하여 새로운 함수를 쉽게 만들 수 있습니다. 이를 통해 코드의 중복을 줄이고 유연성을 높일 수 있으며, 다양한 상황에서 동일한 기능을 수행하는 함수를 빠르게 생성할 수 있습니다.
11. 람다 함수 사용하기 (Use Lambda Functions)
람다 함수는 익명 함수로 알려져 있으며, 간단하고 임시적인 함수를 필요로 하는 경우 유용합니다. 일반 함수에 비해 문법이 간결하여 한 줄로 표현이 가능합니다.
11.1 람다 함수의 구조
람다 함수의 일반적인 구문은 다음과 같습니다.
lambda arguments: expression
lambda
키워드: 람다 함수를 생성하겠다는 것을 나타냅니다.arguments
: 람다 함수의 입력 매개변수 또는 인수를 나타냅니다. 쉼표로 구분된 하나 이상의 인수를 포함할 수 있으며, 일반 함수의 인수와 유사합니다.- 콜론
:
: 인수 목록과 람다 함수의 표현식을 구분합니다. expression
: 람다 함수가 평가하고 결과로 반환할 단일 표현식 또는 연산입니다. 단순하거나 복잡할 수 있지만, 단일 표현식이어야 합니다.
11.2 예제
아래 예제는 람다 함수와 전통적인 명명된 함수의 사용을 보여줍니다.
# 람다 함수 사용
square = lambda x: x ** 2
result_lambda = square(5)
print(result_lambda) # 출력: 25
# 명명된 함수 사용
def calculate_square(x):
return x ** 2
result_function = calculate_square(5)
print(result_function) # 출력: 25
11.3 람다 함수의 장점
- 간결함: 람다 함수는 간결한 문법 덕분에 한 줄로 정의할 수 있습니다.
- 임시 사용: 특정 작업을 위해 일시적으로 사용되는 함수를 빠르게 정의할 수 있습니다.
람다 함수는 특정 상황에서 전통적인 명명된 함수보다 몇 가지 이점을 제공합니다. 간단한 연산이나 한 번만 사용되는 함수를 정의할 때 유용하게 활용될 수 있습니다.
12. 타입 힌트 사용하기 (Use Type Hints)
함수의 인수와 반환 값에 대한 타입 주석을 사용하는 것이 권장됩니다. 이러한 관행은 여러 가지 이점을 제공합니다.
12.1 타입 힌트의 장점
- 가독성 및 이해 향상: 인수와 반환 값의 타입 주석은 다른 개발자에게 함수의 예상 타입과 목적을 명확하게 전달합니다. 함수의 인터페이스를 더 명시적이고 이해하기 쉽게 만듭니다.
- 문서화 향상: 타입 주석은 자체 문서화 코드 역할을 하며, 다른 개발자가 함수의 목적과 예상 동작을 이해하기 쉽게 합니다.
- 타입 오류 감지: 타입 주석을 사용하면 개발 중 타입 관련 오류를 식별할 수 있습니다.
- 도구 지원: 많은 IDE와 코드 편집기가 타입 주석에 대한 향상된 도구 지원을 제공합니다. 이러한 기능은 자동 완성, 타입 추론, 타입 검사 등을 포함합니다.
12.2 예제
아래의 예제는 타입 힌트의 사용을 보여줍니다.
def add_without_type_hint(a, b):
return a + b
def add_with_type_hint(a: int, b: int) -> int:
return a + b
print(add_without_type_hint(3, 5)) # 출력: 8
print(add_with_type_hint(3, 5)) # 출력: 8
이 예제에서 add_with_type_hint
함수는 인수 a
와 b
에 int
타입 힌트가 붙어 있으며, 반환 타입 역시 int
로 명시되어 있습니다. 타입 힌트를 사용하면 이 함수가 정수 인수를 기대하고 정수 값을 반환한다는 것이 명확해집니다. 긴 문서 문자열(docstring)이 필요하지 않게 됩니다.
12.3 결론
- 타입 힌트는 코드의 명확성과 안정성을 높이는 훌륭한 방법입니다.
- 함수의 인수와 반환 값을 이해하는 데 도움이 되며, 문서화의 일환으로 작용합니다.
- 개발 도구와의 호환성도 향상시켜 생산성을 높일 수 있습니다.
댓글