betterway24 동적인디폴트인자

김승환·2021년 7월 11일

코딩의 기술

목록 보기
15/36

None과 독스트링을 사용해 동적인 디폴트 인자를 지정하라

  • 종종 키워드 인자의 값으로 정적으로 정해지지 않는 타입의 값을 써야 할 때가 있다.
  • ex) 로그 메시지와 시간을 함께 출력하고 싶다고 하자.
  • 기본적으로 함수 호출 시간을 포함하길 원한다.
#함수가 호출될 때마다 
#디폴트 인자가 재계산 된다고 가정하면 다음과 같은 접근 방법을 사용할 수 있다.

from time import sleep
from datetime import datetime

def log(message, when=datetime.now()): #제너레이터 느낌?
    print(f'{when}: {message}')
# 함수가 정의되는 시점의 datetime으로 저장이 되어 있다.
log('안녕!')
sleep(0.1)
log('다시 안녕!')

2021-07-02 15:01:26.731556: 안녕!
2021-07-02 15:01:26.731556: 다시 안녕!

함수가 정의되는 시점에 datetime.now가 단 한번만 호출되기 때문에 타임 스탬프가 항상 같다.

#충분한 시간이 지난 이후 다시 실행을 해도 처음 실행했던 datetime이 나온다.
log('안녕!')
sleep(0.1)
log('다시 안녕!')

2021-07-02 15:01:26.731556: 안녕!
2021-07-02 15:01:26.731556: 다시 안녕!

#datetime
when=datetime.now()
print(when)

2021-07-02 15:02:14.032119

함수가 정의되는 시점에 datetime.now가 단 한번만 호출되기 때문에 타임 스탬프가 항상 같다.

원하는 동작을 달성하는 파이썬의 일반적인 관례는 디폴트 값으로 None을 지정하고 실제 동작을 독스트링에 문서화 하는 것이다.

# None값에 적절한 디폴트값을 할당해야한다.
def log(message, when=None):
    """메시지와 타임스탬프를 로그에 남긴다.
    Args:
        message: 출력할 메시지.
        when: 메시지가 발생한 시각(datetime).
            디폴트 값은 현재 시간이다.
    """
    if when is None:
        when = datetime.now() #when의 변수를 다시 할당해준다.
    print(f'{when}: {message}')

log('안녕!')
sleep(0.1)
log('다시 안녕!')

#0.1의 차이가 발생

2021-07-02 15:02:43.482770: 안녕!
2021-07-02 15:02:43.590794: 다시 안녕!

  • 디폴트 인자 값으로 None을 사용하는 것은 인자가 가변적인 경우 특히 중요하다.
  • 예를들어 JSON 데이터로 인코딩된 값을 읽으려고 하는데, 데이터 디코딩에 실패하면 디폴트로 빈 딕셔너리를 반환하고 싶다.
# default로 {}를 설정
# 앞 예시와 같이 default는 한번만 작동할 것이다.
import json

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default
#값이 디폴트이기 때문에 디폴트 값이 호출된다.
foo = decode('잘못된 데이터')
print(foo)

bar = decode('또 잘못된 데이터')
print(bar)

{}
{}

# 각 딕셔너리에 다른 키에 해당하는 값을 넣었는데 같이 변한다.
# 단순 복사가 되었다.
foo['stuff'] = 5
bar['meep'] = 1

print('Foo:', foo)
print('Bar:', bar)

Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}

  • 키와 값이 하나뿐인 서로 다른 딕셔너리가 두 개 출력될 것으로 예상한 독자도 있을 것이다.
  • 한쪽 딕셔너리를 변경하면 다른 쪽 딕셔너리도 변경 되는 것 처럼 보인다.
# 위 def 상황 예시
x= { '피자' : 10000, '사과' : 5000}
a=x
b=x
print(x)
print(a)
print(b)

{'피자': 10000, '사과': 5000}
{'피자': 10000, '사과': 5000}
{'피자': 10000, '사과': 5000}

a['치킨'] = 1000
print(a)
print(b)
print(x)

{'피자': 10000, '사과': 5000, '치킨': 1000}
{'피자': 10000, '사과': 5000, '치킨': 1000}
{'피자': 10000, '사과': 5000, '치킨': 1000}

  • 위 예시를 보면 단순? 복사의 예시로 참조된 딕셔너리가 다 변경되었다.
  • 위 문제를 해결할 방법으로 앞에서 설명했던 얇은 복사를 하면 독립적으로 작용한다.
# 얇은 복사를 시도한 예시
import copy
def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default
    
#값이 디폴트이기 때문에 디폴트 값이 호출된다.
foo = copy.copy(decode('잘못된 데이터'))
print(foo)

bar = copy.copy(decode('또 잘못된 데이터'))
print(bar)

{}
{}

# 독립적으로 저장이 된 모습.
foo['stuff'] = 5
bar['meep'] = 1

print('Foo:', foo)
print('Bar:', bar)

Foo: {'stuff': 5}
Bar: {'meep': 1}

위 문제의 해법은 이 함수에 있는 키워드 인자의 디폴트 값으로 None을 지정하고 독스트링에 동작 방식을 기술한다.

# default 값을 함수를 실행 할때마다 다시 값을 불러오게 설정
def decode(data, default=None):
    """문자열에로부터 JSON 데이터를 읽어온다
    Args:
        data: 디코딩할 JSON 데이터.
        default: 디코딩 실패시 반환할 값이다.
            디폴트 값은 빈 딕셔너리다.
    """
    try:
        return json.loads(data)
    except ValueError:
        if default is None:
            default = {}
        return default
#문제 없이 독립적으로 딕셔너리가 처리된 모습
foo = decode('잘못된 데이터')
foo['stuff'] = 5
bar = decode('또 잘못된 데이터')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)
assert foo is not bar

Foo: {'stuff': 5}
Bar: {'meep': 1}

타입 애너테이션을 사용해도 잘 작동한다.

  • 아래 예시에서 when 인자에는 datetime인 Optional 값이라는 타입 애너테이션이 붙어있다.
  • 따라서 when에 사용할 수 있는 두 값은 None과 datetime 개체뿐이다.
from typing import Optional

def log_typed(message: str,
              when: Optional[datetime]=None) -> None:
    """메시지와 타임스탬프를 로그에 남긴다.
    Args:
        message: 출력할 메시지.
        when: 메시지가 발생한 시각(datetime).
            디폴트 값은 현재 시간이다.
    """
    if when is None:
        when = datetime.now()
    print(f'{when}: {message}')
log('안녕!')
sleep(0.1)
log('다시 안녕!')

2021-07-01 19:16:54.947871: 안녕!
2021-07-01 19:16:55.048600: 다시 안녕!

typing모듈로 타입 표시하기

from typing import List

nums: List[int] = [1, 2, 3]
print(nums)

[1, 2, 3]

from typing import Dict

countries: Dict[str, str] = {"KR": "South Korea", "US": "United States", "CN": "China"}
from typing import Tuple

user: Tuple[int, str, bool] = (3, "Dale", True)
from typing import Set

chars: Set[str] = {"A", "B", "C"}
from typing import Union


def toString(num: Union[int, float]) -> str:
    return str(num)

x=toString(['가', 3.0])
print(x)
print(type(x[1])) 
print(x[0])
print(x[6])

['가', 3.0]
<class 'str'>
[
3

def repeat(message: str, times: Optional[int] = None) -> list:
    if times:
        return [message] * times
    else:
        return [message]

print(repeat("a",5))

['a', 'a', 'a', 'a', 'a']

profile
인공지능 파이팅!

0개의 댓글