From 67291d1018af87f60b3a321f6a04033a739b0ae6 Mon Sep 17 00:00:00 2001 From: Roo Ryu <34956359+deepbig@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:20:14 +0900 Subject: [PATCH] =?UTF-8?q?[11=EC=A3=BC=EC=B0=A8=203]=20Effective=20Python?= =?UTF-8?q?:=20Chapter=203.=20Functions=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create 3. Functions.md * Update 3. Functions.md --- hong/python/effective-python/3. Functions.md | 816 +++++++++++++++++++ 1 file changed, 816 insertions(+) create mode 100644 hong/python/effective-python/3. Functions.md diff --git a/hong/python/effective-python/3. Functions.md b/hong/python/effective-python/3. Functions.md new file mode 100644 index 0000000..c9e5109 --- /dev/null +++ b/hong/python/effective-python/3. Functions.md @@ -0,0 +1,816 @@ +# Chapter 3. Functions + +프로그래머들이 파이썬에서 처음으로 사용하는 정리 도구는 함수(function)다. + +다른 프로그래밍 언어와 마찬가지로 함수를 사용하면 큰 프로그램을 더 작고 간단한 조각으로 나누고, 각 조각이 어떤 일을 하는지 알려주는 이름을 붙일 수 있다. + +함수를 사용하면 가독석이 좋아지고 코드에 더 쉽게 접근할 수 있다. + +재사용과 리팩터링도 쉬워진다. + +파이썬 함수에는 프로그래머가 더 편하게 프로그래밍할 수 있도록 해주는 여러 가지 추가 기능이 들어 있다. 일부는 다른 프로그래밍 언어에 있는 기능과 비슷하지만, 파이썬에만 해당하는 기능도 많다. + +이런 추가 기능을 사용하면 함수의 목적을 더 분명하게 표현할 수 있고, 코드의 잡음을 줄여서 함수 호출의 의도를 더 명확히 드러낼 수 있으며, 찾기 어려운 미묘한 버그를 현저히 줄일 수 있다. + +## Better Way 19 함수가 여러 값을 반환하는 경우 절대로 네 값 이상을 언패킹하지 말라 + +언패킹 구문의 한 가지 효과는 언패킹을 사용하면 함수가 둘 이상의 값을 반환할 수 있다는 것이다. + +```python +def get_stats(numbers): + minimum = min(numbers) + maximum = max(numbers) + return minimum, maximum + +lengths = [63, 73, 72, 60, 67, 66, 71, 61, 72, 70] + +minimum, maximum = get_stats(lengths) # 반환 값이 두 개 +``` + +이 코드는 원소가 두 개인 튜플에 여러 값을 넣어서 함께 반환하는 식으로 작동한다. 이 함수를 호출한 코드는 반환받은 튜플을 두 변수에 대입해서 언패킹한다. + +여러 값을 한꺼번에 처리하는 별표 식을 사용해 여러 값을 반환받을 수도 았다. + +```python +def get_avg_ratio(numbers): + average = sum(numbers) / len(numbers) + scaled = [x / average for x in numbers] + scaled.sort(reverse=True) + return scaled + +longest, *middle, shortest = get_avg_ratio(lengths) +``` + +요구사항에 맞춰 언패킹하는 값이 많아진다면 아래와 같은 문제가 발생하기 쉬워진다. + +```python +# 올바른 사용 +minimum, maximum, average, median, count = get_stats(lengths) + +# 실수로 위치를 바꿔 사용했을 경우 +minimum, maximum, median, average, count = get_stats(lengths) +``` + +- 위와 같이 모든 반환 값이 수(number)인 경우, 순서를 혼동하기 쉽다. 이런 실수는 나중에 알아내기 어려운 버그를 만들어낸다. 반환 값이 많으면 실수하기도 아주 쉬워진다. +- 또한, 함수를 호출하는 부분과 반환 값을 언패킹하는 부분이 길고, 여러 가지 방법으로 줄을 바꿀 수 있어서 가독성이 나쁘다. + +이런 문제를 피하려면 함수가 여러 값을 반환하거나 언패킹할 대 값이나 변수를 네 개 이상 사용하면 안된다. + +변수가 세 개 이하인 언패킹 구문은 세 가지 값을 따로 변수에 넣는 구문일 수도 있고, 두 값을 변수에 넣고 나머지 모든 값을 한 변수에 넣는 구문일 수도 있으며, 더 짧은 언패킹 구문일 수도 있다. + +이보다 더 많은 값을 언패킹 해야 한다면 경량 클래스(lightweight class)나 namedtuple을 사용하고 함수도 이런 값을 반환하게 만드는 것이 낫다. + +** 경량 클래스(lightweight class) + +- 경량 클래스 종류:  [TracebackException](https://docs.python.org/ko/3/library/traceback.html#traceback.TracebackException), [StackSummary](https://docs.python.org/ko/3/library/traceback.html#traceback.StackSummary) 및 [FrameSummary](https://docs.python.org/ko/3/library/traceback.html#traceback.FrameSummary). (Contributed by Robert Collins in [bpo-17911](https://bugs.python.org/issue?@action=redirect&bpo=17911).) +- dataclass: ***Lightweight class*** with less boilerplate for data. +- class: Creates objects that encapsulate what it is (identity) + +** namedtuple + +- collections module에 속한 Factory Function. + - `namedtuple() Factory Function for Tuples with Named Fields + - factory function for creating tuple subclasses with named fields + - Named tuples assign meaning to each position in a tuple and allow for more readable, self-documenting code. They can be used wherever regular tuples are used, and they add the ability to access fields by name instead of position index. +- Collections에 속한 class objects + - namedtuple() + - deque + - ChainMap + - Counter + - OrderedDict + - defaultdict + - UserDict + - UserList + - UserString + +```python +# Basic example +Point = namedtuple('Point', ['x', 'y']) +p = Point(11, y=22) # instantiate with positional or keyworad arguments +p[0] + p[1] # indexable like the plain tuple (11, 22) +# Output: 33 +x, y = p # unpack like a regular tuple +x, y +# Output: (11, 22) +p.x + p.y # fields also accessible by name +# Output: 33 +p # readable __repr__ with a name=value style +# Point(x=11, y=22) +``` + +- Named tuples are especially useful for assigning field names to result tuples returned by the `csv` or `sqlite3` modules: + +```python +EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade') + +import csv +for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))): + print(emp.name, emp.title) + +import sqlite3 +conn = sqlite3.connect('/companydata') +cursor = conn.cursor() +cursor.execute('SELECT name, age, title, department, paygrade FROM employees') +for emp in map(EmployeeRecord._make, cursor.fetchall()): + print(emp.name, emp.title) +``` + +In addition to the methods inherited from tuples, named tuples supports three additional methods and two attributes. To prevent conflicts with field names, the method and attribute names start with an underscore. + +_make(iterable) + +- Class method that makes a new instance from an existing sequence or iterable. + +```python +t = [11, 22] +Point._make(t) +# Output: Point(x=11, y=22) +``` + +_asdict() + +- Return a new dict which maps field names to their corresponding values: + +```python +p = Point(x=11, y=22) +p._asdict() +# Output: OrderedDict([('x', 11), ('y', 22)]) +``` + +_replace(**kwargs) + +- Return a new instance of the named tuple replacing specified fields with new values: + +```python +p = Point(x=11, y=22) +p._replace(x=33) +# Output: Point(x=33, y=22) +``` + +_fields + +- Tuple of strings listing the field names. Useful for introspection and for creating new named tuple types from existing named tuples. + +```python +p._fields # view the field names +# Output: ('x', 'y') +Color = namedtuple('Color', 'red green blue') +Pixel = namedtuple('Pixel', Point._fields + Color._fields) +Pixel(11, 22, 128, 255, 0) +# Output: Pixel(x=11, y=22, red=128, green=255, blue=0) +``` + +_field_defaults + +- Dictionary mapping field names to default values. + +```python +Account = namedtuple('Acount', ['type', 'balance'], defaults=[0]) +Account._field_defaults +# Output: {'balance': 0} +Account('premium') +# Output: Account(type='premium', balance=0) +``` + +출처: [collections — Container datatypes — Python 3.7.17 문서](https://docs.python.org/ko/3.7/library/collections.html) + +### 기억해야 할 내용 + +- 함수가 여러 값을 반환하기 위해 값들을 튜플에 넣어서 반환하고, 호출하는 쪽에서는 파이썬 언패킹 구문을 쓸 수 있다. +- 함수가 반환한 여러 값을, 모든 값을 처리하는 별표 식을 사용해 언패킹할 수도 있다. +- 언패킹 구문에 변수가 네 개 이상 나오면 실수하기 쉬우므로 변수를 네 개 이상 사용하면 안 된다. 대신 작은 클래스를 반환하거나 `namedtuple` 인스턴스를 반환하라. + +## Better Way 20 None을 반환하기보다는 예외를 발생시켜라 + +파이썬 프로그래머들은 유틸리티 함수를 작성할 때 반환 값을 None으로 하면서 이 값에 특별한 의미를 부여하려는 경향을 나타낸다. + +경우에 따라서는 이런 결정이 타당해 보인다. 예를 들어 한 수를 다른 수로 나눈는 도우미 함수를 작성할 때, 0으로 나누는 경우 결과가 정해져 있지 않으므로 None을 반환하는 것이 자연스러워 보인다. + +```python +def careful_divide(a, b): + try: + return a / b + except ZeroDivisionError: + return None +``` + +이 함수를 사용하는 코드는 반환 값을 적절히 해석하면 된다. + +하지만 결과 값이 0인 경우, 함수가 반환한 결과를 if 문 등의 조건에서 평가할 때 0 값이 문제가 될 수 있다. + +```python +x, y = 0, 5 +result = careful_divide(x, y) +if not result: + print('잘못된 입력') # 이 코드가 의도하지 않게 실행됨. +``` + +위 예제는 None인지 검사하는 대신, 실수로 빈값을 False로 취급하는 검사를 수행하면서 결과 값을 잘못 해셕하는 문제가 발생한다. + +이처럼 False와 동등한 반환 값을 잘못 해석하는 경우는 None이 특별한 의미를 가지는 파이썬 코드에서 흔히 저지르는 실수다. + +그래서 careful_divide와 같은 함수에서 None을 반환하면 오류를 야기하기 쉽다. + +실수할 가능성을 줄이는 방법은 두 가지다. + +1. 반환 값을 2-튜플로 분리하는 것이다. +- 이 튜플의 첫 번째 부분은 연산이 성공인지 실패인지를 표시한다. +- 두 번째 부분은 계산에 성공한 경우 실제 결과값을 저장한다. +- 이 함수를 호출하는 쪽에서는 투플을 언패킹해야 한다. +- 이로 인해 나눗셈 결과를 그냥 검사하지 못하고 항상 튜플에서 상태부분을 살펴보게 된다. + +```python +def careful_divide(a, b): + try: + return True, a / b + except ZeroDivisionError: + return False, None + +success, result = careful_divide(x, y) +if not success: + print('잘못된 입력') +``` + +- 이 방법의 문제점은 호출하는 쪽에서 튜플의 첫 번째 부분(변수를 사용하지 않음을 밑줄로 표시하는 파이썬 표준 관례)을 쉽게 무시할 수 있다는 것이다. +- 이렇게 작성한 코드는 한눈에 보면 잘못됐는지 알아보기 어렵지만, None을 반환하는 경우와 마찬가지로 실수할 가능성이 높아진다. +- 이런 실수를 줄일 수 있는, 더 나은 두 번째 방법은 아래와 같다. + +2. 특별한 경우에 결코 None을 반환하지 않는 것이다. 대신 Exception을 호출한 쪽으로 발생시켜서 호출자가 이를 처리하게 한다. + +```python +def careful_divide(a, b): + try: + return a / b + except ZeroDivisionError as e: + raise ValueError('잘못된 입력') +``` + +- 호출자는 더이상 반환 값에 대한 조건문을 사용하지 않아도 된다. +- 대신에 반환 값이 항상 올바르다고 가정하고, try 문의 else 블록에서 이 값을 즉시 사용할 수 있다. + +```python +x, y = 5, 2 +try: + result = careful_divide(x, y) +except ValueError: + print('잘못된 입력') +else: + print(result) +``` + +- 이 방법을 확장해서 타입 애너테이션을 사용하는 코드에도 적용할 수 있다. +- 함수의 반환 값이 항상 float이라고 지정할 수 있고, 그에 따라 None이 결코 반환되지 않음을 알릴 수 있다. +- 하지만 파이썬의 점진적 타입 지정(gradual typing)에서는 함수의 인터페이스에 예외가 포함되는지 표현하는 방법(이런 방식을 검증 오류(checked exception)라고 한다)이 의도적으로 제외됐다. +- 대신 여러분은 호출자가 어떤 Exception을 잡아내야 할지 결정할 때 문서를 참조할 것으로 예상하고, 발생시키는 예외를 문서에 명시해야 한다. +- 지금까지 설명한 모든 내용을 반영하고, 독스트링(Docstring)과 타입 애너테이션까지 포함시키면 코드는 다음과 같다. + +```python +def careful_divide(a: float, b: float) -> float: + """a를 b로 나눈다. + + Raises: + ValueError: b가 0이어서 나눗셈을 할 수 없을 때 + """ + try: + return a / b + except ZeroDivisionError as e: + raise ValueError('잘못된 입력') +``` + +- 이제 입력 출력, 예외적인 동작이 모두 명확해졌고, 호출자가 이 함수를 잘못 처리할 가능성이 매우 낮아졌다. + +** 점진적 타입 지정(gradual typing): + +- Gradual typing allows parts of a program to be dynamically typed and other parts to be statically typed. That means, the programmer can choose which part of the program he/she want to type check. +- You can gradually introduce types into your code. + +** 검증 오류(checked exception): + +- A checked exception is a contract between a function that declares and throws an exception and another function that calls those function and has to handle the exception in its body. +- 함수의 인터페이스에 예외가 포함되는지 표현하는 방법은 Python에서 의도적으로 제외됐다. +- 제외 이유: + - Open/Closed 원칙의 위배 + - Checked Exception을 처리하지 않고 상위 계증으로 던지려면 반드시 메소드 시그니처의 throws에 명시해야한다. 예외의 처리를 강제하는 장점이 되기도 하지만, 시그니처를 수정해야하는 경우, 던진 계층부터 처리한 계층까지 거치는 모든 메소드에서 Checked 예외를 수정해야 하기 때문에 open-closed 원칙에 위배된다. + - 없이도 견고한 프로그래밍이 가능. + +### 기억해야 할 내용 + +- 특별한 의미를 표시하는 None을 반환하는 함수를 사용하면 None과 다른 값(예: 0이나 빈 문자열)이 조건문에서 False로 평가될 수 있기 때문에 실수하기 쉽다. +- 특별한 상황을 표현하기 위해 None을 반환하는 대신 예외를 발생시켜라. 문서에 예외 정보를 기록해 호출자가 예외를 제대로 처리하도록 하라. +- 함수가 특별한 경우를 포함하는 그 어떤 경우에도 절대로 None을 반환하지 않는다는 사실을 타입 애너테이션으로 명시할 수 있다. + +## Better Way 21 변수 영역과 클로저의 상호작용 방식을 이해하라 + +숫자로 이뤄진 list를 정렬하되, 정렬한 리스트의 앞쪽에는 우선순위를 부여한 몇몇 숫자를 위치시켜야 한다고 가정하자. 이 패턴은 사용자 인터페이스를 표시하면서 중요한 메시지나 예외적인 이벤트를 다른 것보다 우선해 표시하고 싶을 때 유용하다. + +이런 경우를 해결하는 일반적인 방법은 리스트의 sort 메서드에 key 인자로 두우미 함수를 전달하는 것이다. + +list는 각 원소를 정렬할 때 이 도우미 함수가 반환하는 값을 기준으로 사용한다. + +도우미 함수는 주어진 원소가 중요한 숫자 그룹에 들어 있는지 검사해서 정렬 기준값을 적절히 조정해준다. + +```python +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) # Output: [2, 3, 5, 7, 1, 4, 6, 8] +``` + +이 함수가 예상대로 작동하는 세 가지 이유가 있다. + +- 파이썬이 클로저(closure)를 지원: + - 클로저란 자신이 정의된 영역 밖의 변수를 참조하는 함수다. + - 클로저로 인해 도우미 함수가 sort_priority 함수의 group 인자에 접근할 수 있다. +- 파이썬에서 함수가 일급 시민(first-class citizen) 객체임: + - 일급 시민 객체라는 말은 이를 직접가리킬 수 있고, + - 변수에 대입하거나 다른 함수에 인자로 전달할 수 있으며, + - 식이나 if 문에서 함수를 비교하거나 함수에서 반환하는 것들이 가능다하는 것을 의미한다. + - 이 성질로 인해 sort 메서드는 클로저 함수를 key 인자로 받을 수 있다. +- 파이썬에서는 시퀀스(튜플 포함)를 비교하는 구체적인 규칙이 있음: + - 파이썬은 시퀀스를 비교할 때 0번 인텍스에 있는 값을 비교한 다음, 이 값이 같으면 다시 1번 인덱스에 있는 값을 비교한다. + - 이런 식으로 순서대로 원소를 비교해 두 값이 같으면 그다음 원소로 넘어가는 작업을 시퀀스의 모든 원소를 다 비교하거나 결과가 정해질 때까지 계속한다. + - 이로 인해 helper 클로저가 반환하는 튜플이 서로 다른 두 그룹을 정렬하는 기준 역할을 할 수 있다. + +하지만 주의해야 할 점이 있다. + +여러분이 식 안에서 변수를 참조할 때 파이썬 인터프리터는 이 참조를 해결하기 위해 다음 순서로 영역을 뒤진다. + +1. 현재 함수의 영역 +2. 현재 함수를 둘러싼 영역(현재 함수를 둘러싸고 있는 함수 등) +3. 현재 코드가 들어 있는 모듈의 영역(전역 영역(global scope)이라고도 부름) +4. 내장 영역(built-in scope) (len, str 등의 함수가 들어 있는 영역) + +식이 참조하는 이름에 해당하는 변수가 이 네 가지 영역에 없으면 NameError 예외가 발생한다. + +```python +foo = does_not_exist * 5 +# Output: +# Traceback ... +# NameError: name 'does_not_exist' is not defined +``` + +변수에 값을 대입하는 것은 다른 방식으로 작동한다. 변수가 현재 영역에 이미 정의돼 있다면 그 변수의 값만 새로운 값으로 바뀐다. + +하지만, 변수가 현재 영역에 정의돼 있지 않다면 파이썬은 변수 대입을 변수 정의로 취급한다. + +파이썬에서는 클로저 밖으로 데이터를 끌어내는 특별한 구문이 있지만, 간단한 함수가 아닌 경우에는 사용하지 말라고 경고한다. + +```python +def sort_priority2(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 문은 대입할 데이터가 클로저 밖에 있어서 다른 영역에 속한다는 사실을 분명히 알려준다. +- 이 문장은 변수 대입 시 직접 모듈 영역(전역 영역)을 사용해야 한다고 지정하는 global 문을 보완해준다. + +만약 nonlocal을 사용하는 방식이 복잡해지면 도우미 함수로 상태를 감싸는 편이 더 낫다. + +```python +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 +``` + +### 기억해야 할 내용 + +- 클로저 함수는 자신이 정의된 영역 외부에서 정의된 변수도 참조할 수 있다. +- 기본적으로 클로저 내부에 사용한 대입문은 클로저를 감싸는 영역에 영향을 끼칠 수 없다. +- 클로저가 자신을 감싸는 영역의 변수를 변경한다는 사실을 표시할 때는 nonlocal 문을 사용하라. +- 간단한 함수가 아닌 경우에는 nonlocal 문을 사용하지 말라. + +## Better Way 22 변수 위치 인자를 사용해 시각적인 잡음을 줄여라 + +위치 인자(positional argument)(또는 위치 기반 인자)를 가변적으로 받을 수 있으면 함수 호출이 더 깔끔해지고 시각적 잡음도 줄어든다. + +- 이런 위치 인자를 가변 인자(varargs)나 스타 인자(star args)라고 부르기도 한다. +- 스타 인자라는 이름은 관례적으로 가변 인자의 이름을 `*args`라고 붙이는 것에서 유래했다. + +```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('내 숫자는', 1, 2) # Output: 내 숫자는: 1, 2 +log('안녕') # Output: 안녕 +``` + +- 이처럼 파이썬에서는 마지막 위치 인자 이름 앞에 `*` 를 붙이면, 그 이후의 모든 위치 인자는 선택 사항이 된다. +- 가변 인자를 써도 함수 본문은 바뀌지 않는다. 단지 호출하는 코드만 바뀐다. + +가변적인 위치 인자를 받는 데는 두 가지 문제 점이 있다. + +1. 이런 선택적인 위치 인자가 함수에 전달되기 전에 항상 튜플로 변환된다는 것이다. +- 이는 함수를 호출하는 쪽에서 제너레이터 앞에 `*` 연산자를 사용하면 제너레이터의 모든 원소를 얻기 위해 반복한다는 뜻이다. +- 이렇게 만들어지는 튜플은 제너레이터가 만들어낸 모든 값을 포함하며, 이로 인해 메모리를 아주 많이 소비하거나 프로그램이 중단돼버릴 수 있다. + +```python +def my_generator(): + for i in range(10): + yield i + +def my_func(*args): + print(args) + +it = my_generator() +my_func(*it) # Output: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) +``` + +- 따라서, `*args` 를 받는 함수는 인자 목록에서 가변적인 부분에 들어간느 인자의 개수가 처리하기 좋을 정도로 충분히 작단느 사실을 이미 알고 있는 경우에 가장 적합하다. +- `*args` 는 여러 리터럴이나 변수 이름을 함께 전달하는 함수 호출에 이상적이다. +- `*args` 는 주로 프로그래머의 평의와 코드 가독성을 위한 기능이다. + +2. 함수에 새로운 위치 인자를 추가하면 해당 함수를 호출하는 모든 코드를 변경해야만 한다는 것이다. +- 이미 가변 인자가 존재하는 함수 인자 목록의 핲부분에 위치 인자를 추가하려고 시도하면, 기존 호출 코드를 변경하지 않는 경우 호출하는 코드가 미묘하게 깨질 수도 있다. + +```python +def log(sequence, message, *values): # sequence가 추가됨 + if not values: + print(f'{sequence} - {message}') + else: + values_str = ', '.join(str(x) for x in values) + print(f'{sequence} - {message}: {values_str}') + +log('좋아하는 숫자는', 7, 33) # Output: 좋아하는 숫자는 - 7: 33 +``` + +- 예외가 발생하지 않고 코드가 작동할 수도 있기 때문에 이런 버그는 추적하기 어렵다. +- 이런 가능성을 완전히 없애려면 `*args` 를 받아들이는 함수를 확장할 때는 키워드 기반의 인자만 사용해야 한다. +- 더 방어적으로 프로그래밍하려면 타입 애너테이션을 사용해도 된다. + +### 기억해야 할 내용 + +- def 문에서 `*args`를 사용하면 함수가 가변 위치 기반 인자를 받을 수 있다. +- `*` 연산자를 사용하면 가변 인자를 받는 함수에게 시퀀스 내의 원소들을 전달할 수 있다. +- 제너레이터에 `*` 연산자를 사용하면 프로그램이 메모리를 모두 소진하고 중단될 수 있다. +- `*args`를 받는 함수에 새로운 위치 기반 인자를 넣으면 감지하기 힘든 버그가 생길 수 있다. + +## Better Way 23 키워드 인자로 선택적인 기능을 제공하라 + +다른 대부분의 프로그래밍 언어와 마찬가지로 파이썬에서도 함수를 호출할 때 위치에 따라 인자를 넘길 수 있다. + +```python +remainder(20, 7) +remainder(20, divisor=7) +reminder(number=20, divisor=7) +reminder(divisor=7, number=20) +``` + +주의할 점 + +- 위치 기반 인자를 지정하려면 키워드 인자보다 앞에 지정해야 한다. +- 각 인자는 단 한번만 지정해야 한다. + +딕셔너리의 내용물을 사용해 reminder와 같은 함수를 호출하고 싶다면 `**` 연산자를 사용할 수 있다. + +- `**` 연산자는 파이썬이 딕셔너리에 들어있는 값을 함수에 전달하되 각 값에 대응하는 키를 키워드로 사용하도록 명령한다. + +```python +my_kwargs = { + 'number': 20, + 'divisor': 7, +} +assert remainder(**my_kwargs) == 6 +``` + +- `**` 연산자를 위치 인자나 키워드 인자와 섞어서 함수를 호출할 수 있다. 다만 중복되는 인자가 없어야 한다. +- `**` 연산자를 여러 번 사용할 수도 있다. 다만 여러 딕셔너리에 겹치는 키가 없어야 한다. + +아무 키워드 인자나 받는 함수를 만들고 싶다면, 모든 키워드 인자를 dict에 모아주는 `**kwargs` 라는 파라미터를 사용한다. + +함수 본문에서는 이 `dict`를 사용해 필요한 처리를 할 수 있다. + +```python +def print_parameters(**kwargs): + for key, value in kwargs.items(): + print(f'{key} = {value}') + +print_parameters(alpha=1.5, beta=9, 감마=4) +# Output: +# alpha = 1.5 +# beta = 9 +# 감마 = 4 +``` + +키워드 인자가 제공하는 유연성을 활용했을 때의 세 가지 장점: + +1. 키워드 인잘르 사용하면 코드를 처음 보는 사람들에게 함수 호출의 의미를 명확히 알려줄 수 있다. +2. 키워드 인자의 경우 함수 정의에서 디폴트 값을 지정할 수 있다. 따라서 필요할 때는 원하는 함수 인자를 설정할 수 있는 기능을 제공하지만, 그렇지 않은 대부분의 경우에는 디폴트 동작을 그냥 받아들여도 된다. 이로 인해 코드 중복과 잡음이 줄어든다. +3. 어떤 함수를 사용하던 기존 호출자에게는 하위 호환성(backward compatibility)를 제공하면서 함수 파라미터를 확장할 수 있는 방법을 제공한다는 것이다. 이러 인해 기존 코드를 별도로 마이그레이션하지 않아도 기능을 추가할 수 있다. 인느 새로운 버그가 생길 여지가 줄어든다는 뜻이다. + +### 기억해야 할 내용 + +- 함수 인자를 위치에 따라 지정할 수도 있고, 키워드를 사용해 지정할 수도 있다. +- 카워드를 사용하면 위치 인자만 사용할 때는 혼동할 수 있는 여러 인자의 목정을 명확히 할 수 있다. +- 키워드 인자와 디폴트 값을 함께 사용하면 기본 호출 코드를 마이그레이션하지 않고도 함수에 새로운 기능을 쉽게 추가할 수 있다. +- 선택적 키워드 인자는 항상 위치가 아니라 키워드를 사용해 전달돼야 한다. + +## Better Way 24 None과 독스트링을 사용해 동적인 디폴트 인자를 지정하라 + +종종 키워드 인자의 값으로 정적으로 정해지지 않는 타입의 값을 써야 할 때가 있다. + +하지만 디폴트 인자는 이런 식으로 작동하지 않는다. 함수가 정의되는 시점에 datetime.now가 단 한 번만 호출되기 때문에 타임스탬프가 항상 같다. + +- 디폴트 인자의 값은 모듈이 load될 때 단 한 번만 평가되는데, 보통 프로그램이 시작할 때 모듈을 로드하는 경우가 많다. +- 따라서 디폴트 값을 계산하는 코드가 들어있는 모듈이 도르된 다음 다시 datetime.now() 디폴트 인자가 평가되지는 않는다. + +이런 경우 원하는 동작을 달성하는 파이썬의 일반적인 관례는 디폴트 값으로 None을 지정하고 실제 동작을 독스트링에 문서화하는 것이다. + +코드에서 인자가 None인 경우에는 적절한 디폴트 값을 할당해야 한다. + +```python +def log(message, when=None): + """메시지와 타임스탬프를 로그에 남긴다. + + Args: + message: 출력할 메시지. + when: 메시지가 발생한 시각(datetime). + 디폴트 값은 현재 시간이다. + """ + if when is None: + when = datetime.now() + print(f'{when}: {message}') +``` + +- 디폴트 인자 값으로 None을 사용하는 것은 인자가 가변적인(mutable) 경우 특히 중요하다. + +이 접근 방법은 타입 애너테이션을 사용해도 잘 작동한다. + +```python +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}') +``` + +- when 인자에는 datetime인 Optional 값이라는 타입 에너태이션이 붙어 있다. 따라서 when에 사용할 수 있는 두 값은 None과 datetime 객체뿐이다. + +### 기억해야 할 내용 + +- 디폴트 인자 값은 그 인자가 포함된 함수 정의가 속한 모듈이 로드된느 시점에 단 한 번만 평가된다. 이로 인해 동적인 값({}, [], datetime.now() 등)의 경우 이상한 동작이 일어날 수 있다. +- 동적인 값을 가질 수 있는 키워드 인자의 티폴트 값을 표현할 때는 None을 사용하라. 그리고 함수의 독스트링에 실제 동적인 디폴트 인자가 어떻게 동작하는지 문서화해두라. +- 타입 애너테이션을 사용할 때도 None을 사용해 키워드 인자의 티폴트 값을 표현하는 방식을 적용할 수 있다. + +## Better Way 25 위치로만 인자를 지정하게 하거나 키워드로만 인자를 지정하게 해서 함수 호출을 명확하게 만들라 + +키워드를 사용해 인자를 넘기는 기능은 파이썬 함수의 강력한 기능이다. 키워드 인자의 유연성을 활용하면 여러분의 코드를 처음 읽는 사람도 더 명확하게 용례를 이해할 수 있는 함수를 작성할 수 있다. + +```python +def safe_division_b(number, divisor, ignore_overflow=False, ignore_zero_division=False): + +... + +result = safe_division_b(1.0, 10**500, ignore_overflow=True) + +result = safe_division_b(1.0, 0, ignore_zero_division=True) +``` + +- 위처럼 호출하는 쪽에서 디폴트 동작을 사용하지 않고 키워드 인자를 사용해서 무사할 예외를 정하는 플래그를 설정할 수 있다. +- 이렇게 하면 어떤 예외를 무시할지 결정하는 두 boolean 변수의 위치를 혼동하지 않을 수 있다. + +하지만 이런 식으로 키워드 인자를 사용하는 것이 선택적인 사항이므로 호출하는 쪽에서 명확성을 위해 키워드 인자를 꼭 쓰도록 강요할 수 없다는 문제가 있다. + +따라서, 이와 같이 복잡한 함수의 경우 호출자가 키워드만 사용하는 인자를 통해 의도를 명확히 밝히도록 요구하는 편이 좋다. 키워드만 사용하는 인자는 키워드를 반드시 사용해 지정해야 하며, 절대 위치를 기반으로 저정할 수 없다. + +```python +def safe_division_c(number, divisor, *, ignore_overflow=False, ignore_zero_division=False) + +... +``` + +- 인자 목록에 있는 `*` 기호는 위치 인자의 마지막과 키워드만 사용하는 인자의 시작을 구분해준다. + +하지만, 위의 `safe_division_c` 함수에도 문제가 있다. 호출하는 쪽에서 이 함수의 맨 앞에 있는 두 필수 인자(number, divisor)를 호출하면서 위치와 키워드를 혼용할 수 있다. + +```python +assert safe_division_c(number=2, divisor=5) == 0.4 +assert safe_division_c(divisor=5, number=2) == 0.4 +assert safe_division_c(2, divisor=5) == 0.4 +``` + +또한 나중에 요구 사항이 변경되거나 원하는 스타일이 바뀌어서 맨 팡의 두 인자 이름을 변경하게 될 수도 있다. + +이렇게 별것 아닌 것처럼 보이는 변경만으로도 number와 divisor 인자를 키워드로 호출하는 기존 호출 코드가 깨져버릴 수 있다. + +파이썬 3.8에는 이 문제에 대한 해법이 들어 있다. 이를 `위치로만 지정하는 인자` 라고 부른다. + +- 위치로만 지정하는 인자는 반드시 위치만 사용해 인자를 지정해야 하고 키워드 인자로는 쓸 수 없다. + +```python +def safe_division_d(numerator, denominator, /, *, + ignore_overflow=False, + ignore_zero_division=False): +``` + +- 인자 목록의 `/` 기호는 위치로만 지정하는 인자의 끝을 표시한다. +- 인자 목록에서 `/` 와 `*` 사이에 있는 모든 파라미터는 위치를 사용해 전달할 수도 있고 이름을 키워드로 사용해 전달할 수도 있다. + +### 기억해야 할 내용 + +- 키워드로만 지정해야 하는 인자를 사용하면 호출하는 쪽에서 특정 인자를 (위치를 사용하지 않고) 반드시 키워드를 사용해 호출하도록 강제할 수 있다. 이로 인해 함수 호출의 의도를 명확히 할 수 있다. 키워드로만 지정해야 하는 인자는 인자 목록에서 * 다음에 위치한다. +- 위치로만 지정해야 하는 인자를 사용하면 호출하는 쪽에서 키워드를 사용해 인자를 지정하지 못하게 만들 수 있고, 이에 따라 함수 구현과 함수 호출 지점 사이의 결합을 줄일 수 있다. 위치로만 지정해야 하는 인자는 인자 목록에서 / 앞에 위치한다. +- 인자 목록에서 / 와 * 사이에 있는 파라미터는 키워드를 사용해 전달해도 되고 위치를 기반으로 전달해도 된다. 이런 동작은 파이썬 함수 파라미터의 기본 동작이다. + +## Better Way 26 functools.wrap을 사용해 함수 데코레이터를 정의하라 + +파이썬은 함수에 적용할 수 있는 데코레이터(decorator)를 정의하는 특별한 구문을 제공한다. 데코레이터는 자신이 감싸고 있는 함수가 호출되기 전과 후에 코드를 추가로 실행해준다. + +이는 데코레이터가 자신이 감싸고 있는 함수의 입력 인자, 반환 값, 함수에서 발생한 오류에 접근할 수 있다는 뜻이다. + +함수의 의미를 강화하거나 디버깅을 하거나 함수를 등록하는 등의 일에 이런 기능을 유용하게 쓸 수 있다. + +예를 들어 함수가 호출될 때마다 인자 값과 반환 값을 출력하고 싶다고 하자. + +이런 기능은 재귀적 함수에서 함수 호출이 재귀적으로 내포되는 경우를 디버깅할 때 특히 유용하다. + +여기서는 `*args`와 `**kwargs`를 사용해 감싸진 함수의 모든 파라미터를 전달받았다. + +```python +def trace(func): + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + print(f'{func.__name__}({args!r}, {kwargs!r}) -> {result!r}') + return result + return wrapper + +@trace # 데코레이터를 함수에 적용할 때는 @ 기호를 사용한다. +def fibonacci(n): + """n번째 피보나치 수를 반환한다.""" + if n in (0, 1): + return n + return (fibonacci(n - 2) + fibonacci(n - 1)) +``` + +- `@` 기호를 사용하는 것은 이 함수에 대해 데코레이터를 호출한 후, 데코레이터가 반환한 결과를 원래 함수가 속해야 하는 영역에 원래 함수와 같은 이름으로 등록하는 것과 같다. + +```python +fibonacci = trace(fibonacci) +``` + +- 이렇게 꾸며진 함수(새로운 fibonacci)는 wrapper의 코드를 원래의 fibonacci 함수가 실행되기 전과 후에 실행한다. 따라서 wrapper는 재귀 스택의 매 단계마다 함수의 인자와 반환 값을 출력한다. + +```python +fibonacci(4) + +>>> +fibonacci((0,), {}) -> 0 +wrapper((0,), {}) -> 0 +fibonacci((1,), {}) -> 1 +wrapper((1,), {}) -> 1 +... +fibonacci((4,), {}) -> 3 +wrapper((4,), {}) -> 3 +``` + +- 이 코드는 잘 작동하지만, 의도하지 않은 부작용이 있다. 데코레이터가 반환하는 함수의 이름이 fibonacci가 아니게 된다. + +```python +print(fibonacci) + +>>> +.wrapper at ...> +``` + +- 예를 들어 꾸며진 fibonacci 함수에 help 내장 함수를 호출하면 쓸모가 없다. +- 다음과 같이 호출하면 fibonacci 함수의 맨 앞부분에 있는 독스트링이 출력되야 하지만, 실제로는 그렇지 않다. + +```python +help(fibonacci) + +>>> +Help on function wrapper in module __main__: + +warpper(*args, **kwargs) +``` + +이러한 문제를 해결하는 방법은 `functools` 내장 모듈에 정의된 `wraps` 도우미 함수를 사용하는 것이다. 이 함수은 데코레이터 작성을 돕는 데코레이터다. + +```python +from functools import wraps + +def trace(func): + @wraps(func) + def wrapper(*args, **kwargs): + ... + return wrapper + +@trace +def fibonacci(n): + ... +``` + +- `wraps`를 wrapper 함수에 적용하면 wraps가 데코레이터 내부에 들어가는 함수에서 중요한 메타데이터를 복사해 wrapper 함수에 적용해준다. +- 이제 help를 함수를 실행하면 데코레이터로 감싸진 함수에 대해서도 원하는 결과를 볼 수 있다. + +```python +help(fibonacci) + +>>> +Help on function fibonacci in module __main__: + +fibonacci(n) + n번째 피보나치 수를 반환한다. +``` + +파이썬 함수에는 이 예제에서 살펴본 것보다 더 많은 표준 애트리뷰트가 있다(`__name__`, `__module__`, `__annotations__`). + +파이썬 언어에서 함수의 인터페이스를 처리하려면 이런 애트리뷰트도 보존돼야 한다. wraps를 사용하면 이 모든 애트리뷰트를 제대로 복사해서 함수가 제대로 제대로 작동하도록 해준다. + +** Code Introspection (내성, 자기성찰): + +- Introspection is an ability to determine the type of an object at runtime. +- Everything in python is an object. Every object in Python may have attributes and methods. +- By using introspection, we can dynamically examine python objects. +- Code Introspection is used for examining the classes, methods, objects, modules, keywords and get information about them so that we can utilize it. +- Python provides some built-in functions that are used for code introspection. They are: +1. type(): This function returns the type of an object. + +```python +import math + +print(type(math)) # Output: +print(type(1) # Output: +print(type("1") # Output: +``` + +1. dir(): This function return list of methods and attributes associated with that object. +2. str(): This function converts everything into a string. +3. id(): This function returns a special id of an object. + +[Code introspection in Python - GeeksforGeeks](https://www.geeksforgeeks.org/code-introspection-in-python/) + +** reflection (반사, 심사숙고): + +- Reflection refers to the ability for code to be able to examine attributes about objects that might be passed as parameters to a function. +- For example, if we write type(obj) then Python will return an object which represents the type of obj. +- Using reflection, we can write one recursive reverse function that will work for strings, lists, and any other sequence that supports slicing and concatenation. + +```python +# Python program to illustrate reflection +def reverse(sequence): + sequence_type = type(sequence) # 여기서 타입에 맞는 동작을 수행함. + empty_sequence = sequence_type() + + if sequence == empty_sequence: + return empty_sequence + + rest = reverse(sequence[1:]) + first_sequence = sequence[0:1] + + final_result = rest + first_sequence + + return final_result + +print(reverse([10, 20, 30, 40]) # Output: [40, 30, 20, 10] +print(reverse("GeeksForGeeks")) # Output: skeeGroFskeeG +``` + +- introspection: 실행 시점에 프로그램이 어떻게 실행되는지 관찰하는 것을 뜻한다. +- reflection: 실행 시점에 프로그램을 조작하는 것을 뜻한다. + +introspection은 실행 시에 object에 대한 정보를 알아내는 것, Reflection은 여기에서 그치지 않고 object에 대한 정보를 수정하는 것을 의미합니다. 엄밀히 따지면, 두가지의 의미가 다르지만 Reflection으로 합쳐서 부르는 경우가 많다. + +[reflection in Python - GeeksforGeeks](https://www.geeksforgeeks.org/reflection-in-python/) + +### 기억해야 할 내용 + +- 파이썬 데코레이터는 실행 시점에 함수가 다른 함수를 변경할 수 있게 해주는 구문이다. +- 데코레이터를 사용하면 디버거 등 인트로스펙션을 사용하는 도구가 잘못 작동할 수 있다. +- 직접 데코레이터를 구현할 때 인트로스펙션에서 문제가 생기지 않길 바란다면 functools 내장 모듈의 wraps 데코레이터를 사용하라.