return 값이 많은 함수를 -> 3개 이하로 unpacking 하라.

  • 함수는 여러 값을 return 하기 위해, 값들을 tuple에 넣어서 반환한다.
  • 함수가 반환한 값을 unpacking을 통해 많이 처리하는데, starred expression을 이용하여 3개 이하로 줄여라!!
  • 만약 4개 이상의 변수를 unpacking 해야한다면,
    • 대신 작은 class를 return 하거나
    • namedtuple instance를 return 해라.

함수는 None을 return 하면 안되고, 대신 예외를 발생시켜라.

  • 0과 빈 문자열 등 False 이고, None도 False 이므로 None을 return 하는 것은 위험하다.
def careful_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None

#
x, y = 1, 0
result = careful_divide(x, y)
if result is None:
    print('잘못된 입력')

#
x, y = 0, 5
result = careful_divide(x, y)
if not result:
    print('잘못된 입력') # 이 코드가 실행되는데, 사실 이 코드가 실행되면 안된다!
  • 해결책
    • None을 절대 return하지 않는 것
    • 대신 Exception을 호출한 쪽으로 발생시켜서, 호출자가 이를 처리하게 한다.
    • 더불어,
      • return 값에 대한, type annotation 을 사용하라.
      • 함수 내 예외에 대해, 꼭 docstring에 명시하라.
def careful_divide(a: float, b: float) -> float:
    """a를 b로 나눈다.
    Raises:
        ValueError: b가 0이어서 나눗셈을 할 수 없을 때
    """
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('잘못된 입력')

try:
    result = careful_divide(x, y)
except ValueError:
    print('잘못된 입력')
else:
    print('결과는 %.1f 입니다' % result)

변수 영역과 closure의 상호작용 방식을 이해하라.

  • closure 함수
    • 자신이 정의된 영역 밖의 변수를 참조하는 함수
    • 아래 예시에서 helper이 closure 함수이다.
      • group이라는 영역 밖 변수를 참조하기 때문이다.
      • closure 때문에, helper 함수가 sort_priority 함수의 group 인자에 접근할 수 있다.
  • 파이썬 함수 = first-class citizen 객체
    • first-class citizen 객체
      • 직접 가리킬 수 있다.
      • 변수에 대입하거나, 다른 함수에 인자로 전달할 수 있다.
      • 식이나 if 문에서 함수를 비교하거나, 함수를 반환하는 것이 가능하다.
    • 아래 예시에서,
      • 이 성질로 인해 sort 메서드는 helper 함수를 key 인자로 받을 수 있다.
def sort_priority(numbers, group):
    found = False
    def helper(x):
        if x in group:
            found = True # 문제를 쉽게 해결할 수 있을 것 같다
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
found = sort_priority(numbers, group)
print('발견:', found)
print(numbers)
  • 식 안에서 변수를 참조할 때,

    • python interpreter은 이 참조를 해결하기 위해 다음 순서로 영역을 뒤진다.
      • 현재 함수의 영역
      • 현재 함수를 둘러싼 영역(현재 함수를 둘러싸고 있는 함수 등)
      • 현재 코드가 들어 있는 모듈의 영역(global scope)
      • 내장 영역(bulit-in scope): len, str 등의 함수가 들어가 있는 영역
  • 식 안에서 변수에 값을 대입할 때,

    • 변수가 현재 영역에 이미 정의돼 있다면, 그 변수의 값만 새로운 값으로 바뀐다.
    • 하지만 변수가 현재 영역에 정의돼 있지 않으면, 변수 대입을 변수 정의로 취급한다.
    • 위 예시에서,
      • helper 함수 안 found 대입문은, helper 안에서 새로운 변수를 정의하는 것으로 취급되지,
      • sort_priority 안에서 기존 변수에 값을 대입하는 것으로 취급되지는 않는다.
    • 이는 함수에서 사용한 지역 변수가, 그 함수를 포함하고 있는 모듈 영역을 더럽히지 못하게 막는 것이다.
  • 위 코드를 해결하려면 nonlocal 을 사용할 수 있다. (가급적 하지마라)
  • nonlocal 문은 대입할 데이터가, closure 밖에 있어서 다른 영역에 속한다는 사실을 분명히 알려 준다.
  • 하지만, 간단한 함수 외에는 어떤 경우라도 nonlocal을 쓰지마라!!!
    • 이해가 어려워진다.
#
def sort_priority(numbers, group):
    found = False
    def helper(x):
        nonlocal found       # 추가함
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found
  • nonlocal 대신 도우미 함수로 상태를 감싸는 편이 낫다.
class Sorter:
    def __init__(self, group):
        self.group = group
        self.found = False

    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)
        
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True

가변적 positional argument를 사용해 시각적인 잡음을 줄여라.

  • positional argument 를 가변적으로 받을 수 있으면,
    • 함수 호출이 더 깔끔해지고 시각적 잡음도 줄어든다.
  • 이런 가변적 positional argument는
    • varargs 혹은 star args 라고 부르기도 한다.
    • 결과는 tuple 인스턴스로 받는다.
    • * 연산자는 파이썬이 sequence의 원소들을 -> 함수의 위치 인자로 넘길 것을 명령한다.
    • 넘길 가변 positional argument가 없으면, 안써줘도 된다!!
      • 이 경우, 빈 tuple로 받는다.
 def log(message, *values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print(f'{message}: {values_str}')

log('내 숫자는 ', [1, 2])
log('안녕 ')  # 안써도 되어서, 훨씬 좋다

## 중요. * 용법이 특이하다.
favorites = [7, 33, 99]
log('좋아하는 숫자는', *favorites)
  • *args를 언제써야해?
    • *args를 받는 함수가, 인자 목록에서 가변적인 부분에 들어가는 인자의 개수가 처리하기 좋을 정도로 충분히 작다는 사실을 이미 알고 있는 경우 적합
    • 여러 리터럴이나, 변수 이름을 함께 전달하는 함수 호출에 이상적이다.
    • 프로그래머의 편의와 코드 가독성을 위한 기능이다.
  • 가변적인 positional argument를 받는 문제점 1
    • 선택적인 positional argument가, 함수에 전달되기 전에 항상 tuple로 변환된다는 점
      • 함수를 호출하는 쪽에서 generator 앞에 * 연산자를 사용하면, generator 의 모든 원소를 얻기 위해 반복한다는 뜻이다.
      • 이렇게 만들어지는 tuple은 generator 가 만들어낸 모든 값을 포함하며, 이로 인해 메모리를 아주 많이 소비하거나 프로그램이 중단돼버릴 수 있다.
def my_generator():
    for i in range(10):
        yield i

def my_func(*args):
    print(args)

it = my_generator()
my_func(*it)

>>>
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
  • 가변적인 positional argument를 받는 문제점 2
    • 기존 함수에 새로운 positional argument를 추가하면, 함수 호출 위치를 전부 찾아서 다 바꿔줘야 한다.
    • 이를 막으려면, *args 를 받아들이는 함수를 확장할 때는 키워드 기반의 인자만 추가 사용해야 한다.

keyword argument로 선택적인 기능을 제공하라.

  • keyword argument의 장점

    • 코드를 처음 보는 사람들에게, 함수 호출의 의를 명확히 알려줄 수 있다.
    • default 값을 지정할 수 있다.
    • 어떤 함수를 사용하던, 기존 호출자에게는 하위 호환성을 제공하면서 함수 파라미터를 확장할 수 있는 방법을 제공한다.
  • 선택적인(가변적인) argument를 지정하는 최선의 방법은, 항상 keyword arguemnt를 사용하고 positional argument를 절대 사용하지 않는 것이다.

  • 특징

    • 필요한 positional argument가 모두 제공되는 한, keyword argument 를 넘기는 순서는 상관 없다.
    • positional argument를 keyword argument 앞에 지정해야 한다.
  • 사용법

def remainder(number, divisor):
    return number % divisor

my_kwargs = {
    'number': 20,
    'divisor': 7,
}

assert remainder(**my_kwargs) == 6
  • 아래와 같이 사용할 수도 있다.
# case 1
my_kwargs = {
    'divisor': 7,
}
assert remainder(number=20, **my_kwargs) == 6

# case 2
my_kwargs = {
    'number': 20,
}

other_kwargs = {
    'divisor': 7,
}

assert remainder(**my_kwargs, **other_kwargs) == 6
  • 아무 keyword argument를 받는 함수를 만들고 싶다면, **kwargs 파라미터를 사용한다.
    • 함수 내에서 dict로 받아들인다.
def print_parameters(**kwargs):
    for key, value in kwargs.items():
        print(f'{key} = {value}')

print_parameters(alpha=1.5, beta=9, 감마=4)

None과 Docstring 을 사용해, 동적인 default 인자를 지정하라.

  • 아래와 같이, 동적 시간을 default 로 하고 싶지만, 잘 작동하지 않는다. (함수 선언될 때 처음 한번만 시간을 측정할 뿐)
def log(message, when=datetime.now()):
    print(f'{when}: {message}')
  • 동적으로 default argument를 달성하는 방법은, 디폴트 값으로 None 을 지정하고, 실제 동작을 docstring에 문서화하는 것이다
def log(message, when=None):
    """메시지와 타임스탬프를 로그에 남긴다.
    Args:
        message: 출력할 메시지.
        when: 메시지가 발생한 시각(datetime).
            디폴트 값은 현재 시간이다.
    """
    if when is None:
        when = datetime.now()
    print(f'{when}: {message}')

log('안녕!')
sleep(0.1)
log('다시 안녕!')
  • 이 접근 방법은 아래와 같이 type annotation을 사용해도 잘 작동한다.
    • when에 사용할 수 있는 두 값은 None과 datetime 객체 뿐이다.
from typing import Optional
from datetime import datetime

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}')

postional argument만 쓰거나, keyword argument만 써서, 함수 호출을 명확하게 만들라.

def safe_division_e(numerator, denominator, /,
                    ndigits=10, *,                 # 변경
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        fraction = numerator / denominator         # 변경
        return round(fraction, ndigits)            # 변경
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
  • / 이전에 온 arguments은, position으로만 지정해야 하는 argument 이다. (keyword 로 호출하면 에러가 난다.)
    • 장점: 함수 호출의 의도를 명확히 알 수 있다.
  • * 다음에 온 arguments는, keyword로만 지정해야 하는 argument 이다. (position으로 호출하면 에러가 난다.)
    • 장점: 함수 구현과, 호출 시점 사이의 결합을 줄일 수 있다.
  • /* 사이에 있는 파라미터는, 키워드를 사용해 전달해도 되고, 위치를 기반으로 전달해도 된다. (이런 동작은 python 함수 파라미터의 기본 동작이다.)

functools.wraps 을 사용해 함수 decorator을 정의하라.

  • decorator
    • 자신이 감싸고 있는 함수가 호출되기 전과, 호출된 후에 코드를 추가로 실행해줌
    • 자신이 감싸고 있는 함수의 input argument, return, error 에 접근할 수 있다는 뜻이다.
    • 함수의 의미를 강화하거나, 디버깅을 하거나, 함수를 등록하는 등의 일을 할 수 있다.
  • decorator 적용 예: 함수 호출될 때마다, argument와 return을 출력하고 싶다면?
    • @ 기호를 사용하는 것은,
      • 이 함수에 대해 decorator을 호출한 후,
        • wrapper = trace(fibonacci)
      • decorator가 반환한 결과를 (wrapper)
        • 원래 함수가 속해야 하는 영역에,
        • 원래 함수와 같은 이름으로 등록하는 것과 같다. (fibonacci 대신 wrapper로 등록)
from functools import wraps

def trace(func):
	@wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r}) '
              f'-> {result!r}')
        return result
    return wrapper

@trace
def fibonacci(n):
    """Return n 번째 피보나치 수"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n - 1))

fibonacci(4)

print(fibonacci)
  • wraps를 wrapper 함수에 적용하면, wraps가 decorator 내부에 들어가는 함수에서 중요한 metadata를 복사해, wrapper 함수에 적용해준다.
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN