함수 완벽 정복!

About_work·2023년 1월 18일
0

python 기초

목록 보기
11/65

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개의 댓글