[Effective Python] Functions

u8cnk1·2023년 1월 7일
0

Chapter 3: Functions

함수 사용의 장점

큰 프로그램을 작게 분할하여 의도를 나타낼 수 있다.
가독성 향상, 코드에 더 쉽게 접근 가능
재사용 및 refactoring 가능

Item 19: 함수가 Multiple Value를 리턴할 때 Unpack 사용하지 않기

다음과 같이 함수를 tuple에 넣고 파이썬의 unpack 구문을 이용하여 여러값을 반환하도록 할 수 있다. 두 항목 이상으로 된 tuple에서 여러 값이 함께 반환되고, 반환된 tuple의 압출을 풀어 각각의 변수를 할당한다. 이때 여러 반환 값을 catch-all unpacking을 위해 * 표현식을 사용하여 지정할 수 있다.

lengths = [63, 73, 72, 60, 67, 66, 71, 61, 72, 70]

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

그러나 변수를 할당받을 때 실수로 정렬을 잘못하게 된다면

# correct
minimum, maximum, average, median, count = get_stats(lengths) 

# average와 median의 위치가 바뀜
minimum, maximum, median, average, count = get_stats(lengths) 

나중에 발견하기 어려운 버그를 발생시키거나 예상과 다른 결과가 나올 수 있다. → 네 개 이상의 변수로 unpacking하는 경우 오류가 발생하기 쉽다.
이런 경우 class를 이용하거나 namedtuple 인스턴스를 반환하는 것이 좋다!

Item 20: None을 리턴 하기보다는 Exception으로 처리하기

특수한 의미를 나타내기 위해 None이나 0, 또는 빈 문자열을 반환하는 경우, 조건식에서 False로 평가되기 때문에 오류가 발생하기 쉽다.

예를 들어 한 숫자를 다른 숫자로 나누는 함수를 구현할 때,

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

위와 같이 0으로 나누는 경우 None을 리턴하는 것 대신에 특수 상황을 나타내는 exception으로 처리하는 것이 좋다.
호출된 코드는 항상 예외를 제대로 처리하고, 호출자는 return 값이 항상 유효하다고 가정하고 즉시 다른 블록에서 결과를 사용할 수 있다.

def careful_divide(a: float, b: float) -> float:
	"""Divides a by b.
 
 	Raises:
		ValueError: When the inputs cannot be divided.
 	"""
	try:
		return a / b
	except ZeroDivisionError as e:
		raise ValueError('Invalid inputs')

주석을 사용하여 특수한 상황에서도 함수가 0을 리턴하지 않음을 작성하는 것이 좋다.

Item 21: Variable Scope와 상호작용하는 Closure

clousre -> 정의된 범위의 변수를 참조하는 함수

(사용자 인터페이스를 렌더링할 때 중요한 메세지나 예외 이벤트를 다른 모든 것보다 먼저 표시하려는 경우 유용)

closure 함수는 정의된 범위의 변수만을 참조할 수 있다.
기본적으로는 enclousing scope에 영향을 줄 수 없지만, nonlocal문을 사용하여 closure가 enclousing 범위에서 변수를 수정함을 나타낼 수 있다.

  • nonlocal문은 특정 변수 이름을 할당할 때 scope traversal이 발생해야 함을 나타내기 위해 사용 (중첩 함수 내에서 비지역 변수 선언, 사용된 위치에서 한 단계 바깥쪽에 위치한 변수와 바인딩)
  • nonlocal문을 사용하면 closure에서 다른 scope로 데이터가 할당되는 경우가 명확해진다. -> 변수의 할당이 모듈 범위에 직접 들어가야 함을 나타내는 global문을 보완
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}

# numbers 리스트를 정렬하되 group의 번호를 우선 순위에 두고싶은 경우
def sort_priority(numbers, group):	# 외부함수
	found = False
 	def helper(x):	# 내부함수
    				# 주어진 항목이 중요한 그룹에 있는지 확인
		nonlocal found # Added (지역변수 found를 불러오기 위해 nonlocal 사용)
 		if x in group:
 			found = True
 			return (0, x)
 		return (1, x)
 	numbers.sort(key=helper)	# 리스트의 정렬 sort에 대한 인수로 helper 함수 전달
 	return found

그러나 관련 변수에 대한 nonlocal문과 할당이 멀리 떨어져 있는 경우 이해하기 어려우며, 단순한 기능 이상의 용도로 nonlocal문을 사용하지 않도록 주의해야 한다.
nonlocal의 사용이 복잡해지면 helper class를 사용하여 정리하는 것이 좋다.

#nonlocal 접근과 동일한 결과를 달성하는 class 정의

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: Variable Positional Arguments

* 함수 인자(Function Arguments) - 참고

  1. 기본값(Default value)
    • 예: def func(a, b=1):
    • b의 값을 설정하지 않으면 b는 기본값으로 1을 할당받는다
  2. 위치 인자(Positional arguments)
    • 함수 내로 전달되는 입력 인자들의 위치를 그대로 사용
      (인자로 사용되는 순서와 함수 정의에서 사용된 인자 순서를 매칭)
  3. 키워드 인자(keywords arguments)
    • 키워드 인자를 사용하면 순서를 무시하고 입력할 수 있다.
    • 위치 인자와 혼합해서 사용 가능(※ 키워드 인자를 위치인자보다 뒤에 작성)
    • 예: func의 parameter가 a,b,c일때func(1, b=2, c=3)과 같이 작성가능하다.
  4. 가변 인자(Variable arguments)
    • 위치, 키워드 인자의 개수가 많거나 몇개를 사용할 지 모르는 경우 사용
    • 함수의 선언에서 인자를 *args, **kwargs등으로 사용한다.
    • tuple 타입으로 패킹 / unpacking에 사용되는 *식과 유사하게 작동

* Variable Positional Arguments

def문에서 가변 개수의 위치 인수를 허용하면 함수 호출을 더 명확하게 하고 시각적 노이즈를 줄일 수 있다.
python에서 마지막 위치 매개변수의 이름을 *로 접두서를 붙여 이 작업을 수행할 수 있다.

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 함수의 첫 번째 매개변수는 필수이며, 두 번째 위치 인수는 선택적이다. 두 번째 인수 없이 log('Hi there')과 같이 호출 가능!

그러나 *연산자를 generator와 함께 사용하면 프로그램의 메모리가 부족해지고 프로그램이 중단될 수 있으며 *인수를 허용하는 함수에 새로운 위치 매개 변수를 추가하면 탐지하기 어려운 버그가 발생할 수 있어 주의해야 한다.

Item 23: Keyword Arguments

함수 인수를 위치 또는 키워드로 지정할 수 있다.

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

위와 같이 정의된 경우 remainder 함수를

remainder(20, 7)
remainder(20, divisor=7)
remainder(number=20, divisor=7)
remainder(divisor=7, number=20)

다양한 방법으로 호출할 수 있다.

※이때 키워드 인수 앞에 위치 인수를 지정해야 한다. -> remainder(number=20, 7)는 에러 발생

각 인수는 한 번만 지정할 수 있다. -> remainder(20, number=7)도 에러 발생

dictionary의 내용을 사용하여 함수를 호출하는 경우 **연산자를 사용한다.

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

remainder(**my_kwargs) # **연산자를 사용하여dictionary의 값을 함수의 해당 키워드 인수로 전달
					   # 마찬가지로 6을 리턴함

인수가 반복되지 않는 한 함수 호출에서 ** 연산자를 위치 인수 또는 키워드 인수와 혼합할 수 있다.
또한 사전에 겹치는 키가 없는 경우 ** 연산자를 여러 번 사용할 수 있다.

keyword arguments의 유연성이 주는 장점

  1. 함수 호출 시 각 인자들의 목적을 명확하게 한다. -> 예: remainder(number=20, divisor=7)
  2. 함수 정의 시 지정된 기본값을 가지도록 설정할 수 있다. -> 예: def flow_rate(weight_diff, time_diff, period=1):
  3. 기존 호출자와 하위 호환성을 유지하면서 함수의 매개 변수를 확장하는 강력한 방법을 제공한다. 즉 기존 코드를 많이 migration하지 않고도 추가 기능을 제공할 수 있으므로 버그가 발생할 가능성이 줄어든다. -> 예: 2번 예시에서 새로운 인자를 추가하여 필요한 동작을 함수에 쉽게 추가할 수 있다. def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):

Item 24: None 및 Docstrings 사용하여 Dynamic Default Arguments 지정하기

default 인수 값은 모듈 로드 시 함수 정의 중에 한 번만 평가된다.
non-static 타입(예: datetime.now() 등)의 키워드 인자를 기본값으로 사용하는 경우, 함수가 호출될 때마다 default arguments가 재평가되도록 하기 위해 None을 default로 설정하고 docstring을 통해 실제 동작을 문서화 해야한다.

Item 25: Keyword 전용 인수, Positonal 전용 인수로 명확성 강조하기

키워드 전용 인수는 호출자가 (위치가 아닌) 키워드로 특정 인수를 제공하도록 강제하므로 함수 호출의 의도를 더 명확하게한다. 키워드 전용 인수는 인수 목록의 단일 * 뒤에 정의된다.

위치 지정 전용 인수는 호출자가 키워드를 사용하여 특정 매개 변수를 제공할 수 없도록 하여 커플링을 줄이는 데 도움이 된다.
위치 전용 인수는 인수 목록에서 단일 / 이전에 정의된다.

인수 목록의 / 문자와 * 문자 사이의 매개 변수는 Python 매개 변수의 default값인 위치 또는 키워드로 제공할 수 있다.

# ZeroDivisionError 예외를 무시하고 무한대를 반환, OverflowError 예외를 무시하고 그 대신 0을 반환하는 함수
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
result = safe_division_e(22, 7)
print(result)	# 3.1428571429
result = safe_division_e(22, 7, 5)
print(result)	# 3.14286
result = safe_division_e(22, 7, ndigits=2)
print(result)	# 3.14

Item 26: functools.wraps를 사용하여 Function Decorators 정의하기

decorator => 한 함수가 런타임에 다른 함수를 수정할 수 있도록 하는 구문
래핑하는 함수를 호출할 때마다 이전과 이후에 추가 코드를 실행할 수 있다. 즉, decorator는 입력 인수, 반환 값 및 제기된 예외에 액세스하고 수정할 수 있다.

@기호를 사용하여 decorator를 함수에 적용시킬 수 있다. decorated된 함수는 함수 실행 전후에 wrapper 코드를 실행한다.

그러나 디버거 같은 인트로스펙션(introspection - 객체내부의 정보를 런타임에 동적으로 얻을 수 있는 특징)을 하는 도구에서 decorator를 사용하면 문제가 발생할 수 있다. 이를 방지하기 위해 functools.wrap 도우미를 사용한다.

0개의 댓글