# [Python] 함수

## 함수

---

- 입력값을 넣고 어떤 일을 수행한 후 그 결과물을 내어 놓는 것

- 똑같은 내용을 반복해서 작성하는 문제를 해결하기 위해 함수 사용

- 프로그램의 흐름을 잘 파악하고 오류를 찾기 쉬워진다

### **파이썬 함수 구조**

```javascript
def 함수_이름(매개변수):
    수행할_문장1
    수행할_문장2
    ...
    return 리턴값
```

- `def` 는 함수를 만들 때 사용하는 예약어

- `return`은 함수의 결괏값을 리턴하는 명령어 (반환값이 없을 수 있다)

**매개변수와 인수**

```javascript
def add(a, b):    # a, b는 매개변수
    return a+b

c = add(3, 4)     # 3, 4는 인수
print(c) # 7

d = add(b=7, a=5) # 매개변수를 지정하여 호출하는 경우
print(d) # 12
```

- `매개변수`와 `인수`는 혼용해서 사용

    - **매개변수**: 함수에 입력으로 전달된 값을 받는 변수

    - **인수**: 함수를 호출할 떄 전달하는 입력값을 의미

- 입력값이 없는 함수가 있을 수 있다

- 매개변수를 지정하여 호출하는 경우 매개변수의 순서와 상관 없이 사용할 수 있다

```javascript
def 함수_이름(*매개변수):
    수행할_문장
    ...
```

- 매개변수의 갯수를 모르는 경우 `*매개변수`로 설정

- 매개변수 앞에 `*`를 붙이면 입력값을 튜플로 만들어준다

```javascript
def print_kwargs(**kwargs):
    print(kwargs)

print_kwargs(a=1)               # {'a': 1}
print_kwargs(name='foo', age=3) # {'age': 3, 'name': 'foo'}
```

- 키워드 매개변수를 사용할 때는 매개변수 앞에 별 2개 `**`를 붙인다

- `a=1`이 입력되면 kwargs는 `{'a': 1}`인 딕셔너리가 된다

```javascript
# default1.py
def say_myself(name, age, man=True): 
    print("나의 이름은 %s 입니다." % name) 
    print("나이는 %d살입니다." % age) 
    if man: 
        print("남자입니다.")
    else: 
        print("여자입니다.")

```

- 매개변수에 초깃값 미리 설정

- 함수의 인자가 주어지지 않으면 초깃값이 사용된다

**리턴값**

```javascript
def add_and_mul(a, b):
    return a+b, a*b
```

- 함수의 리턴값을 항상 1개 → 여러 개일 경우 **튜플**로 반환된다

### **함수 내부 변수**

```javascript
# vartest.py
a = 1
def vartest(a):
    a = a + 1

vartest(a)
print(a) # 1
```

```javascript
# vartest.py
def vartest(a):
    a = a + 1

vartest(a)
print(a) # 오류
```

- `vartest` 함수 내에서 매개변수 `a`는 함수 안에서만 사용하는 변수 → 외부 변수 영향 X

**외부 변수를 변경하고 싶다면**

```javascript
# vartest_return.py
a = 1 
def vartest(a): 
    a = a + 1 # 
    return a

a = vartest(a) 
print(a)
```

```javascript
# vartest_global.py
a = 1 
def vartest(): 
    global a # global 
    a = a + 1

vartest() 
print(a)
```

- `return`을 사용하여 계산한 값을 반환한다

- `global` 명령어를 사용하면 외부 변수를 변경할 수 있다 (사용하지 않는 것이 좋음)

### lambda

```javascript
함수_이름 = lambda 매개변수1, 매개변수2, ... : 매개변수를_이용한_표현식
```

- `lambda`는 함수를 생성하는 예약어로 `def`와 동일한 역할

## 내장 함수

---

| **함수명** | **기능** | **예시** |
| --- | --- | --- |
| abs | 숫자의 절댓값 리턴 | abs(3)     # 3  abs(-3)    # 3  abs(-1.2)  # 1.2 |
| all | 반복 가능한 데이터 x를 입력값으로 받으며  x의 요소가 모두 참이면 True,  거짓이 하나라도 있으면 False를 리턴 | all([1, 2, 3])    # True  all([1, 2, 3, 0]) # False  all([])            # True |
| any | 반복 가능한 데이터 x를 입력으로 받아  x의 요소 중 하나라도 참이 있으면 True를 리턴,  x가 모두 거짓일 때만 False를 리턴 | any([1, 2, 3, 0]) # True any([0, ""])       # False any([])             # False |

## 일급 함수, 고차 함수, 람다 함수

---

### 일급 함수 First-Class Function

- 프로그래밍 언어가 함수를 ‘일급 시민 (값’으로 취급하는 것

- 함수를 다른 객체와 동일하게 취급

1. 함수를 변수에 할당 가능

2. 함수를 데이터 구조에 저장 가능

3. 함수를 인자로 다른 함수에 전달 가능

4. 함수를 결과로서 반환 가능

- Python은 일급 함수를 지원 → 함수를 매우 유연하게 다룰 수 있다

```javascript
# 함수를 변수에 할당
def say_hello(name):
    return f"Hello {name}"

greet = say_hello
print(greet("Alice")) # 출력: Hello Alice

# 함수를 다른 함수의 인자로 전달
def greet_loudly(greeting_func):
    return greeting_func("Alice").upper()

print(greet_loudly(greet)) # 출력: HELLO ALICE

# 함수를 반환하는 함수
def get_greeting_func():
    def greet(name):
        return f"Hello {name}"

greet = get_greeting_func()
print(greet("Alice")) # 출력: Hello Alice
```

- 함수를 변수에 할당, 다른 함수의 인자로 전달, 함수를 반환하는 함수를 만드는 등의 동작 가능

- 일급 함수를 사용하면 코드가 더울 유연

- 고차 함수와 같은 패턴 구현 가능

### 고차 함수 Higher-Order Function

- 다른 함수를 인자로 받거나, 결과로서 함수를 반환하는 함수를 의미

- ‘일급 함수’의 특성을 활용하는 프로그래밍 패턴 → 함수형 프로그래밍에서 많이 사용

- Python에서는 일급 함수를 지원 → 고차 함수를 쉽게 사용할 수 있다

    - `map()`, `filter()` 등의 내장 함수는 모두 고차 함수

```javascript
# 함수를 인자로 받는 함수
def apply_to_three(func):
    return func(3)

def square(n):
    return n ** 2

print(apply_to_three(square)) # 출력: 9

# 함수를 반환하는 함수
def apply_addr(n):
    def add(x):
        return x + n
    return add

add_five = make_addr(5)
print(Add_five(3)) # 출력: 8

# map 함수는 함수와 반복 가능한 객체를 인자로 받아, 
# 해당 함수를 각 요소에 적용한 결과를 반환하는 고차 함수
numbers = [1, 2, 3, 4]
squares = map(square, numbers)
print(list(squares)) # 출력: [1, 4, 9, 16]
```

- 코드의 재 사용성을 높이고 추상화 수준을 높여 코드의 가독성을 향상

- 명령형 스타일로 작성된 코드를 선언적인 스타일로 리팩토링 가능

### 람다 함수

- 이름이 없는 일회용 함수를 생성하는 데 사용

- `lambda` 키워드를 사용하여 작성, 작고 간단한 함수를 필요로 하는 곳에 사용

**lambda vs def**

1. `def` 키워드를 사용하여 함수를 정의하는 대신 `lambda` 키워드를 사용

2. 람다 함수는 이름이 없습니다 (익명 함수)

3. 람다 함수는 주로 한 줄로 표현, 복잡한 로직을 포함하지 안흔ㄴ다

**람다 함수 기본 구조**

```javascript
lambda arguments: expression
```

**람다 함수 예시**

```javascript
# 숫자를 받아서 그 제곱을 반환하는 람다 함수
square = lambda x: x ** 2
print(square(5)) # 출력: 25

# 두 숫자를 더하는 람다 함수
add = lambda x, y: x * y
print(add(3, 4)) # 출력: 7
```

```javascript
numbers = [1, 2, 3, 4]
squares = map(lambda x: x ** 2, numbers)
print(list(squares)) # 출력: [1, 4, 9, 16]
```

- `map()`, `filter()`, `reduce()` 등의 고차원 람다 함수를 인자로 전달하는 경우가 많다

- 간단한 로직을 빠르고 쉽게 작성 가능

- 복잡한 로직을 처리해야하는 경우 일반 함수를 사용하는 것이 가독성이 더 좋다

## 클로저

---

- 특정 함수와 그 함수가 생성된 환경을 결합한 것

- Python에서만 factory function이라고 불린다

- 클러저는 함수가 생성된 시점의 범위에 있는 모든 변수를 기억하고 이에 접근

**Python에서 클로저**

1. 함수 내부에 함수가 정의, 이러한 내부 함수를 중첩 함수라고 부른다

2. 내부 함수는 외부 함수의 변수를 참조

3. 외부 함수는 내부 함수를 반환

**클로저 예시**

```javascript
# 클로저가 아닌 경우
def outer_functions():
    def inner_function():
        return 100+100
    return inner_function

# 클로저의 경우
def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

inner = outer_function(100)
innter(200) # inner 입장에서 100을 변경할 수 있는 방법은 없다
```

- `outer_function`은 `inner_function`을 반환

- `innter_function`은 외부 함수와 변수 `x`를 참조, `outer_function`을 호출, 
- 결과를 `closure`에 저장 →`inner_function`의 코드와 `x=10`이라는 상태 기억

**클로저 활용**

1. **데이터 은닉**: 외부에서 직접 접근할 수 없는 변수를 ‘감추는’ 방법을 제공 (데이터 은닉, 캡슐화 구현 가능)

2. **지연 바인딩**: 함수가 실행될 때 그 함수의 환경을 기억 → 함수의 행동을 호출 시점의 상황에 따라 동적으로 변경하는 것이 가능

3. **함수형 프로그래밍과 데코레이터**: 함수를 반환하는 능력 덕분에 고차 함수와 데코레이터의 구현에 필수적 (데코레이터는 원래 함수의 행동을 변경하지 않고 기능을 추가하거나 수정하는 데 사용)

## 데코레이터

---

[PEP 318 – Decorators for Functions and Methods | peps.python.org](https://peps.python.org/pep-0318/#background)

- Python 2.4 버전에서 도입된 오래된 기능

- 함수나 메소드에 추가 기능을 동적으로 부여할 때 사용 → `@` 기호와 함께 사용

- 데코레이터로 지정된 함수가 바로 아래에 정의된 함수에 적용

**데코레이터 적용 원리**

- 데코레이터는 **고차 함수 higher-order function**

    - 하나 이상의 함수를 인자로 받고 함수를 결과로 반환하는 함수

- 데코레이터는 하나의 함수를 인자로 받아 기능이 추가 또는 변경된 새로운 함수를 반환

**데코레이터 예시**

```javascript
def simple_decorator(function):
    def wrapper():
        print("Before the functions call")
        function()
        print("After the function call")

@simple_decorator
def hello():
    print("Hello, world!")

hello() # 데코레이터가 없는 상태에서는 simple_decorator(hello)()와 같다
simple_decorator(hello)() # 이전 wrapper()와 동일
```

```javascript
Before the function call
Hello, world!
After the function call
```

다른 예시

```javascript
def simple_decorator(function):
    def wrapper(1):
        print("Before the functions call")
        function()
        print("After the function call")
    return wrapper

@simple_decorator
def 합(1):
    return sum(1)

합([1, 2, 3, '4'])
```

**매개변수가 있는 함수의 데코레이터**

```javascript
def debug(function):
    def new_function():
        print(f'{function.__name__} 함수 시작')
        function()
        print(f'{function.__name__} 함수 끝')
    return new_function

@debug
def sum_1_to_n(n):
    return n * (n + 1) / 2

result = sum_1_to_n(30)

print(result)
```

- `debug` 데코레이터를 붙여서 실행하면 에러 발생

```javascript
def debug(function):
    def new_function(n):
        print(f'{function.__name__} 함수 시작')
        print(n)
        print(f'{function.__name__} 함수 끝')
    return new_function

@debug
def sum_1_to_n(n):
    return n * (n + 1) / 2

result = sum_1_to_n(30)

print(result)
```

```javascript
sum_1_to_n 함수 시작
30
sum_1_to_n 함수 끝
None
```

```javascript
def debug(function):
    def new_function(*args, **kwargs):
        print(f'{function.__name__} 함수 시작')
        function(*args, **kwargs)
        print(f'{function.__name__} 함수 끝')
    return new_function
```

- 어떤 형태의 인자를 사용하든 상관 없이 사용할 수 있는 데코레이터를 붙이려면 `new_function`을 수정해야한다

**리턴 값이 있는 함수의 데코레이터**

```javascript
def debug(function):
    def new_function(*args, **kwargs):
        print(f'{function.__name__} 함수 시작')
        function(*args, **kwargs)
        print(f'{function.__name__} 함수 끝')
    return new_function

@debug
def sum_1_to_n(n):
    return n * (n + 1) / 2

result = sum_1_to_n(30)

print(result)
```

```javascript
sum_1_to_n 함수 시작
sum_1_to_n 함수 끝
None
```

**중첩 데코레이터**

```javascript
@decorator1
@decorator2
...
@decoratorN
def function(*args, **kwargs):
    pass
```

- 여러 개의 데코레이터를 붙일 수 있다

```javascript
def decorator1(function):
    def new_function(*args, **kwargs):
        print('첫 번째 데코레이터 시작')
        result = function(*args, **kwargs)
        print('첫 번째 데코레이터 끝')
        return result
    return new_function

def decorator2(function):
    def new_function(*args, **kwargs):
        print('두 번째 데코레이터 시작')
        result = function(*args, **kwargs)
        print('두 번째 데코레이터 끝')
        return result
    return new_function
    
def decorator3(function):
    def new_function(*args, **kwargs):
        print('세 번째 데코레이터 시작')
        result = function(*args, **kwargs)
        print('세 번째 데코레이터 끝')
        return result
    return new_function

@decorator1
@decorator2
@decorator3
def sum_1_to_n(n):
    return n * (n + 1) / 2

result = sum_1_to_n(30)

print(f'result: {result}')
```

```javascript
첫 번째 데코레이터 시작
두 번째 데코레이터 시작
세 번째 데코레이터 시작
세 번째 데코레이터 끝
두 번째 데코레이터 끝
첫 번째 데코레이터 끝
result: 465.0
```

다른 예시

```javascript
def decorator1(function):
    def new_function():
        print('첫 번째 데코레이터 시작')
        function()
        print('첫 번째 데코레이터 끝')
        return result
    return new_function

def decorator2(function):
    def new_function():
        print('두 번째 데코레이터 시작')
        function()
        print('두 번째 데코레이터 끝')
        return result
    return new_function
    
def decorator3(function):
    def new_function():
        print('세 번째 데코레이터 시작')
        function()
        print('세 번째 데코레이터 끝')
        return result
    return new_function

@decorator1
@decorator2
@decorator3
def print_hello():
  print('hello world!')

print_hello()
```

```javascript
첫 번째 데코레이터 시작
두 번째 데코레이터 시작
세 번째 데코레이터 시작
hello world!
세 번째 데코레이터 끝
두 번째 데코레이터 끝
첫 번째 데코레이터 끝
465.0
```

**동적 데코레이터**

```javascript
def add(function):
    def new_function(*args, **kwargs):
        result = function(*args, **kwargs)
        return result + 100
    return new_function

@add
def plus(a, b):
    return a + b

result = plus(10, 20)
print(f'result: {result}') # result: 130
```

- `add`를 데코레이터로 사용하여 원래 함수의 결과에 100을 더하도록 변한다

- 함수의 결과에 100이 아닌 다른 숫자를 더하는 데코레이터가 필요한 경우 비슷한 로직의 데코레이터를 동적으로 생성하는 것이 가능하다

```javascript
def add(n):
    def decorator(function):
        def new_function(*args, **kwargs):
            result = function(*args, **kwargs)
            return result + n
        return new_function
    return decorator
```

- `add` 함수는 `decorator`함수를 리턴하는 고위 함수

- `decorator`함수는 `function` 함수를 인자로 받아 `new_function` 함수를 리턴하는 고위 함수

⇒ `add` 함수는 `decorator`라는 고위함수를 리턴하는 고위함수 
     → 인자 n을 받아 고위 함수를 동적으로 생성

```javascript
@add(10)
def plus(a, b):
    return a + b

result = plus(10, 20)
print(f'result: {result}') # result: 40
```

- `add()` 호 안의 수를 바꾸어 넣으면 plust 함수가 변경될 수 있다

**클래스형 데코레이터**

```javascript
class Debug:
    def __init__(self, function):
        self.function = function
        
    def __call__(self, *args, **kwargs):
        print(f'{self.function.__name__} 함수 시작')
        self.function()
        print(f'{self.function.__name__} 함수 끝')
```

- `debug` 데코레이터를 클래스 형식으로 작성한 경우

```javascript
@Debug
def f1():
    print('안녕하세요')

@Debug
def f2():
    print('hello')

f1()
f2()
```

```javascript
f1 함수 시작
안녕하세요
f1 함수 끝
f2 함수 시작
hello
f2 함수 끝
```

**데코레이터의 활용**

- 다양한 상황에서 유용하게 사용 가능

- 함수의 실행 시간 측정 → 로깅을 수행하는 경우 사용자의 접근 권한 확인

For the site tree, see the [root Markdown](https://slashpage.com/yerim-devnote.md).
