파이썬 Default Paramter의 함정

박일우·2024년 4월 20일
0

python

목록 보기
4/4
post-thumbnail

함수를 정의할 때 특정 Parameter의 기본값을 정의하여 사용하면 굉장히 편리하다.(몇몇 언어는 기본 매개변수 정의를 제공하지 않지만...)
파이썬에서 Default Parameter을 언제,어떻게 사용하였고 사용중에 어떤 문제가 발생했는지 공유를 해보려 한다.

Default Paramter는 언제, 어떻게 사용할까?

위에서도 말했듯이 기본 매개변수는 올바르게 사용한다면 코드의 가독성과 유지보수성을 높이며, 함수의 유연성을 증가시킬 수 있다. 어떤 경우에 사용하면 좋을지 알아보자.

선택적 매개변수 제공

만드려는 함수가 다양한 매개변수를 필요로할 때 선택적 매개변수를 제공하여 필수적이지 않은 매개변수를 생략할 수 있으며, 이로 인해 함수 호출이 간소화되고, 특정 상황에 맞춰 함수의 동작을 조정할 수 있다.

from requests

def make_request(url, method='GET', data=None, headers=None)
    if not headers:
        headers = {'User-Agent': 'ilwoo-park'}

    if method == 'GET':
		return requests.get(url, headers=headers)
    elif method == 'POST':
    	if not data:
        	raise ValueError("Data is required in POST requests)
		return requests.post(url, data=data, headers=headers)
    else:
        raise ValueError("Unsupported HTTP method")

# GET 요청 예시
response = make_request('https://iw-park.com/get')
print(response.text)

# POST 요청 예시(특정 데이터 및 headers 추가)
headers = {'Content-Type': 'Application/json'}
data = {'id': 'ilwoopark'}
response = make_request('https://iw-park.com/post', method='POST', data=data, headers=headers)
print(response.status_code)

위의 make_request 함수는 request를 요청하고 response를 리턴하는 함수이다.
url, method, data, headers 총 4개의 매개변수를 필요로 하지만 method, data, headers 에 기본 매개변수를 사용하여 필요치 않은경우 사용자는 함수 호출이 간소화 된다.(GET 요청 예시)
또한, 매개변수에 따라 함수의 동작을 조정할 수 있다.(POST 요청에서 data가 없는경우, 다른 METHOD를 입력한 경우)

기능 확장성 유지

함수를 정의하고 나서 시간이 지남에 따라 확장이 필요로 할 때, 기존 코드와의 호환성을 유지하면서 새로운 기능을 추가하는것이 매우 중요하다.
기본 매개변수를 사용하면 기존 인터페이스를 그대로 유지하면서 새로운 매개변수를 추가할 수 있고, 이렇게 하면 함수의 기능을 확장하면서 기존 사용자에게는 영향을 주지 않는다.

def read_file_and_print(file_path, search=None):
    with open(file_path, 'r') as file:
        if search is None:
            print(file.read())
            return
        results = [line.strip() for line in file if search in line]
        [print(result) for result in results]


# 최초의 기능 - 파일을 읽어서 출력하기
read_and_print('example.txt')

# 확장된 기능 - 파일을 읽고 특정 텍스트가 포함된 라인을 출력하기
read_and_print('example.txt', search='Ilwoo')

read_file_and_print 함수는 초기에 '파일을 읽고 출력' 만을 담당하였다.
시간이 지남에 따라 기능이 확장되어 기존 기능에 '특정 텍스트가 포함된 라인을 출력' 이라는 기능이 확장 되면서 함수의 매개변수(search)가 추가 되었다.
만약 기본 매개변수를 사용하지 않는다면 인터페이스가 변경이 되었으므로 초기 사용자들은 추가된 매개변수를 입력하지 않아 함수 호출에 실패 할 것이다.
기본 매개변수를 사용하여 기존 사용자의 인터페이스는 유지하면서, search 매개변수를 입력하므로써 새로운 검색 기능을 사용할 수 있게 되었다.

오버로딩의 대체

파이썬에서는 오버로딩이 존재하지 않는다. 같은 이름으로 함수를 여러번 정의하면 가장 마지막에 정의된 함수로 동작한다.
기본 매개변수를 사용하여 오버로딩을 대체하여 구현할 수 있다.

def draw(shape, position, color, width=None, radius=None):
    if shape == 'line':
        if width is None:
            raise ValueError("Width required for line")
        print(f"Drawing line at {position} with color {color} and width {width}")
    elif shape == 'circle':
        if radius is None:
            raise ValueError("Radius required for circle")
        print(f"Drawing circle at {position} with color {color} and radius {radius}")
    else:
        print(f"Drawing {shape} at {position} with color {color}")

# 선 그리기
draw('line', (10, 10), color='blue', width=5)

# 원 그리기
draw('circle', (20, 20), color='red', radius=10)

# 다른 도형 그리기
draw('rectangle', (30, 30), color='green')

draw 함수는 그래픽 객체를 그리는 함수이다.
오버로딩을 지원하는 언어에서는 draw 함수를 매개변수별로 구분하여 정의를 하겠지만, 파이썬에서는 하나의 함수로 다양한 매개변수를 처리하여 오버로딩을 대체할 수 있다.
line을 그리는 경우 shape, position, color, width 매개변수를 입력하고,
circle을 그리는 경우 shape, position, color, radius 매개변수를 입력하며
다른 도형을 그리는 경우에는 shape, position, color 매개변수를 입력하여 호출하면 된다.

Default Paramter 사용시 주의사항

위에서 기본 매개변수의 사용을 알아봤다.
그럼 이번엔 사용시 주의 해야할 경우에 대해 알아보겠다.

Default Parameter의 값은 함수가 정의될 때 단 한번만 평가된다.

from datetime import datetime

def order_product(product_id, user_id, ordered_time=datetime.now()):
	order_dao = Order(product_id, user_id, ordered_time)
    return db.insert(order_dao)

# 일반 주문(실시간)
order_product('product_1', 'user_1')
# 예약 주문(내일 현재시간에 주문)
order_product('product_2', 'user_2', datetime.now() + timedelta(days=1))

order_product 함수는 제품ID, 유저ID를 입력받아 데이터베이스 주문 기록을 적재하는 함수이다.
주문은 일반 주문(실시간)과 예약 주문이 있다.
일반 주문의 경우 ordered_time 매개변수를 기본값으로 datetime.now() 을 사용하여 함수를 호출할 때 마다 실시간으로 데이터베이스에 적재하고, 예약 주문의 경우 입력받은 ordered_time 인자값으로 데이터베이스 적재한다.

이 함수를 이용하여 30일동안 100만건의 일반 주문을 처리했고, 그 동안 함수를 실행하는 프로세스(넓은 의미로는 애플리케이션)도 안정적으로 다운이 되지 않았다고 가정해보자.
30일 후 주문 데이터를 본다면 아마 ordered_time값이 다 똑같은 대참사가 일어났을 것이다.(정확한 값은 파이썬의 인터프리터가 해당 함수를 포함하는 스크립트나 모듈이 로드되는 시점의 datetime.now()의 값)

이러한 현상이 발생한 이유는 Default Parameter의 평가(생성)가 함수가 정의되는 시기에 단 한번 이루어지기 때문이다.
인터프리터가 해당 함수를 처리(정의)하는 순간 기본 매개변수가 평가되고 그 값이 heap메모리에 저장이 되며, 그 이후부터 기본 매개변수의 값은 더이상 생성되지 않고 같은 값(객체)를 사용한다.

Default Parameter의 값에 가변객체를 사용하는건 위험하다.

def join_user(id, password, authority=[])
	user_dao = User(id, password, authority)
    return db.insert(user_dao)
    
 # 일반 회원
 join_user('id_1', 'pw_1')
 # 특별 회원
 join_user('id_2', 'pw_2', ['A', 'B', 'C'])
 # 일반 회원
 join_user('id_3', 'pw_3')

join_user 함수는 ID와 비밀번호, 권한이 담긴 리스트를 입력받아서 유저 정보를 데이터베이스에 적재하는 함수이다.
일반 회원의 경우 권한이 없기 때문에 authority의 기본 값인 빈 리스트([])를 사용하고,
가입시 돈을 많이 지불한 특별 회원의 경우 authority의 다양한 권한['A', 'B', 'C']를 담은 리스트를 사용한다.

앞서도 말했듯이 함수가 정의되는 순간 매개변수 authority에 기본값인 빈 리스트가 생성이 되고, 그 이후로는 생성되지 않는다.

실제로 해당 함수를 실행시키고 1만명의 일반 회원이 가입한다면 원하는대로 잘 작동할 것이다.
사이트가 잘 작동하여 인기가 끌었고, 엄청난 돈을 투자할 특별 회원에게 막대한 권한을 주어 가입을 시켰다.
그 후 다시 일반 회원들의 가입을 받게되면 특별 회원이 가입된 이후 가입한 일반 회원들에게도 특별 회원과 같은 권한이 들어가는 대참사가 발생한다.

왜 이런일이 발생했을까?
분명히 authority의 기본값은 빈 리스트로 생성되고, 더 이상 다른 값으로 생성되지 않는다.
하지만 여기서 하나 놓친것이 있다. 바로 리스트는 가변객체인 것이다.
(가변객체에 대한 정보는 파이썬 Class변수에 관한 고찰을 참고)

def mutable_f(a, L=[]):
    L.append(a)
    return L

print(mutable_f(1)) # [1]
print(mutable_f(2)) # [1, 2]
print(mutable_f(3)) # [1, 2, 3]

위 함수의 결과를 보면 L의 기본값이 빈 리스트였지만, 가변객체이기 때문에 기본값에 계속 요소가 쌓이는걸 알 수 있다.

def immutable_f(a, I=0):
    I += a
    return I

print(immutable_f(1)) # 1
print(immutable_f(2)) # 2
print(immutable_f(3)) # 3

I의 기본값은 정수형인 0이고, 정수는 불변객체이기 때문에 기본값이 변하지 않는것을 알 수 있다.

결론

모든 프로그래밍 기술들이 그러하듯, 파이썬의 매개변수 기본값은 올바르게만 활용한다면 정말 유용한 기능이다.
하지만, 위의 주의사항처럼 잘못 사용할 경우 정말 큰 대참사가 일어날 수 있기때문에 꼭 숙지하고 사용하길 바란다.

profile
열정!열정!열정!

1개의 댓글

comment-user-thumbnail
2024년 4월 25일

오.. 생각만해도 끔찍한 대참사네요. 덕분에 참사를 면했습니다. 감사합니다!

답글 달기