[Effective Python] 3. Functions

Stop._.bmin·2023년 1월 28일
0

Effective-python

목록 보기
3/5
post-custom-banner

Item 19: Never Unpack More Than Three Variables When Functions Return Multiple Values

  • 만약 리스트의 min과 max를 구할 때는 메서드를 활용해서 하는 방법이나 two-item touple을 활용해서 return하는 방법이 있었다.

아래 코드는 필자가 소개한 평균 길이, 중간 길이, 전체 분포를 보여주는 코드이다.

def get_stats(numbers):
    minimum = min(numbers)
    maximum = max(numbers)
    count = len(numbers)
    average = sum(numbers) / count
    sorted_numbers = sorted(numbers)
    middle = count // 2
    if count % 2 == 0:
        lower = sorted_numbers[middle - 1]
        upper = sorted_numbers[middle]
        median = (lower + upper) / 2
    else:
        median = sorted_numbers[middle]
    return minimum, maximum, average, median, count
minimum, maximum, average, median, count = get_stats(lengths)
print(f'Min: {minimum}, Max: {maximum}')
print(f'Average: {average}, Median: {median}, Count {count}')
>>>
Min: 60, Max: 73
Average: 67.5, Median: 68.5, Count 10

이 코드에는 2가지 문제점이 존재하는데
1.
return values가 모두 numeric해서 reorder 하기 쉽다. -> 버그를 발생시키기 좋다
따라서 minimum, maximum, average, median순으로 이를 구하는 것이 좋다.
2.
함수를 call 하는 line이 다양한 방법으로 wrapped 될 필요가 있어보인다.

minimum, maximum, average, median, count = get_stats(
    lengths)
    minimum, maximum, average, median, count = \
    get_stats(lengths)
(minimum, maximum, average,
 median, count) = get_stats(lengths)
(minimum, maximum, average, median, count
    ) = get_stats(lengths)
  • 이런 과정에서 문제를 줄이기 위해서는 3개의 변수 이상을 unpacking the multiple return values를 할 때 사용하면 안된다.

Item 20: Prefer Rasing Exceptions to Returning None

  • utility functions을 쓸 때 None을 반환하기보다는 예외를 일으키자.

함수에서 None을 반환할 때의 문제점

사실 없어보인다

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None

# 이 함수는 다음과 같이 활용해볼 수 있다.
result = divide(x, y)
if result is None:
    print('Invalid inputs')

위의 코드를 보면 없어보인다
그러나

result = divide(x, y)
if not result:
    print('Invalid inputs')

위와 같은 활용식을 사용한다면 error가 발생한다.

해결 방법

1) 반환 값을 두 개로 나누어서 튜플로

def divide(a, b):
    try:
        return True, a / b	     # 튜플로 결과를 반환
    except ZeroDivisionError:
        return False, None   # 튜플로 결과를 반환

success, result = divide(x, y)
if not success:
    print('Invalid inputs')

이렇게 해주면 또다른 문제를 야기시킨다

2) None를 반환하지 않는다.

  • none을 반환하지 않는 대신 예외를 일으키도록 하는 것이다
    필자가 소개한 방법으로 아래가 있다.
  • 호출하는 쪽에서 잘못됐음을 알리려고 ValueError로 변경한 것인데
def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') from e
  • 호출하는 쪽에서 잘못된 입력에 대해 예외를 처리해야하므로
x, y = 5, 2

try:
    result = divide(x, y)
except ValueError:
    print('Invalid inputs')
else:
    print('Result is {:.2f}'.format(result))
    
>>> Result is 2.50

라고 예외를 처리하면 더 깔끔하다고 한다.


Item 21: Know How Closures Interact with Variable Scope

- 클로저 변수 스포프와 상호 작용하는 방법을 알자

특정 그룹의 숫자들이 먼저 오도록 우선순위를 매기자.

  • pass a helper function
def sort_priority(values, group):
    def helper(x):
        if x in group:			
            return (0, x)			
        return (1, x)
    values.sort(key=helper)	
    
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sort_priority(numbers, group)
print(numbers)

>>> [2, 3, 5, 7, 1, 4, 6, 8]

여기에는 3가지 이유가 있다
1.
파이썬은 클로저를 지원한다
2.
함수는 파이썬에서 일급 객체이다
3.
파이썬에는 이터러블의 대소 관계를 비교하는 특정 규칙이 있다.

따라서 함수에서 우선순위가 높은 아이템을 발견했는지 여부를 반환해서 사용자 인터페이스 코드가 그에 따라 동작하게 하면 좋다고 한다.
필자는 아래와 같이 예시로 보여주었다

def sort_priority2(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
found = sort_priority2(numbers, group)
print('Found', found)

>>> False 

또한, 함수의 표현식에서 변수를 reference한다면 파이썬 인터프리터는 이를 해결하려고 아래와 같은 순서로 탐색한다
1.
The current function's scope
2.
Any enclosing scopes. (such as other containing functions)
3.
The scope of the module that contains the code (also called the global scope).
4.
The built-in scope(that contains functions like len and str).

그래서 이 중 4개중에 하나라도 없으면 NameError exception이 발생한다.

helper class

스코핑 버그라는 문제들이 발생하는데 이것은 뉴비들에게 굉장히 놀랍다.
sort 메서드의 key에 sorter클래스를 넣고 있으면 instance를 호출할 수 있기 위해서 call 메서드를 정의하여야 한다.

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
    
sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True

Item 22: Reduce Visual Noise with Variable Positional Arguments

  • 가변 인수 위치로 깔끔하게 보이게 하자

Item 23: Provide Optional Behavior with Keyword Arguments

  • 키워드 인수로 선택적인 동작을 제공하자

함수롤 호출할 때 인자를 위치로 전달할 수 있다.

# 정수를 다른 정수로 나눈 나머지를 반환하는 함수
def remainder(n, d):
    return n % d


>>> print(remainder(20, 7))

6

위에서는 단순히 함수 호출 시에 값만 전달됐으나, 인자의 이름과 값을 쌍으로 전달할 수도 있다.

remainder(20, 7)
remainder(20, d=7)
remainder(n=20, d=7)
remainder(d=7, n=20) 

필자는 키워드 인자 사용을 장려한다

  • 코드를 처음 보는 사람이 함수 호출을 명확하기 이해할 수 있다.
  • 함수를 정의할 때 기본 값을 정의할 수 있다.
  • 기존 호출 코드와 호환성을 유지하면서 파라미터를 확장할 수 있다.

Item 24: Use None and Docstrings to Specify Dynamic Defalut Arguments

  • 동적 기본 인수를 지정하려면 None과 docstring을 사용하자
    가령 우리는 비정적 타입만을 사용했으나, 시간과 같이 비정적 타입의 기본인자가 아닐 때가 있다. (필자는 아래의 코드를 예시로 제시)
import datetime
import time


# Use Python 3.6 boy
def log(message, when=datetime.datetime.now()):
    print(f"{when}: {message}")

그런데 필자가 만약 now 함수를 0.1초 정도 뒤에 호출할 경우 기록된 시간이 다름을 예상할 수 있었다고 하는데 now함수가 정의될 때의 시간이 고정되어있음을 확인할 수 있다. (A default argument value is evalutated only once!)

따라서 이를 해결하기 위해 기본값을 None 로 설정한 후 docstring으로 실제 동적을 문서화한다

def log(message, when=None):
    """Log a message with a timestamp

    :input:
        message: Message to print
	when: datetime of when the message occurred.
	      Defaults to the present time.
    """
    if when is None:
        when = datetime.datetime.now()
    print(f"{when}: {message}")



log("Greetings!")
time.sleep(0.1)
log("Greetings again")


2019-05-17 12:27:54.804982: Greetings!
2019-05-17 12:27:54.906290: Greetings again

그리고 교재를 통해서 필자가 여러가지 코드를 제시했는데 모두 확인해보면 시간이 알아서 바뀌었음을 확인할 수 있다.

정리

  • 기반 인자는 모듈 로드 시점에 함수 정의 과정에서 딱 한번만 evaluate된다. {},[] 같은 기호를 쓰는 동적 값에는 이상하게 동작하는 원인이 된다.
  • 값이 동적인 키워드 인자는 기본값으로 None를 사용하자!

Item 25: Enforce Clarity with Keyword-Only and Positional-Only Arguments

  • 키워드 전용 인수로 명료성을 강요하자
  • 키워드로 인수를 넘기는 방법은 item 23에서 확인할 수 있다.
    가령 아래의 예시가 있다 (0을 분모로 할 때 ZeroDivisonError대신 다른 것을 반환하는 예제)
def safe_division(number, divisor,
                  ignore_overflow,
                  ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
else: raise

필자는 저 위의 함수에서 몇 가지 오류를 무시하고 무한대 값을 반환하도록 설계하였는데, 이것도 또한 그렇게 좋은 방법이 아닌 것을 교재에서 설명하고 있다.

따라서 복잡한 함수를 작성할 때에는 의도를 명확히 들어내도록 하자

  • keyword-only argument로 정의하자!
def safe_division_c(n, d, *, 
                    ignore_overflow=False, ignore_zero_division=False):
    ...

위와 같이 활용하는 것이다. *뒤에 있는 인수들은 위치 인자로 보내면 도작하지 않는다고 한다.

정리

  1. 키워드 인자는 함수 호출 의도를 더 명확하게 해준다
  2. bool 플래그를 여러 개 받는 함수처럼 헷갈리기 쉬운 함수를 호출할 때 키워드 인자를 넘기게 하려면 키워드 전용 인자를 사용하자.
  3. 인수 목록에서 /와 * 문자 사이의 매개변수는 Python 매개변수의 기본값인 위치 또는 키워드로 제공될 수 있다.

Item 26: Define Function Decorators with functiontools.wraps

profile
원하는 만큼만
post-custom-banner

0개의 댓글