[OIP] chapter 9 : 함수
1. 함수 기본
(1) 개념
함수란 이름이 붙여진 분리된 코드 조각으로, 코드 재사용을 위해 사용한다. 그동안 추상적으로 함수를 사용해왔는데, 앞으로는 함수가 1) 정의, 2) 호출 이라는 두 가지 주요 작업을 수행한다는 점을 인지하자.
- 함수의 정의는 함수를 생성하는 과정을 말하는데, def 키워드를 사용하여 함수의 이름, 매개변수, 함수 내부 코드 블록을 정의한다.
- 함수의 호출은 함수를 실행하는 과정으로, 함수 이름 뒤에 괄호를 붙여 호출하게 된다.
# 함수 정의
def say_yes():
print("yes")
# 함수 호출
say_yes()
## output : yes
(2) 객체로서의 함수
파이썬에서는 모든 것이 객체라는 말이 있다. '모든 것'에는 함수도 포함되기 때문에 파이썬에서 함수는 일반 데이터 객체와 동등한 지위를 갖는다. 이는 파이썬이 다른 언어에서 구현하기 힘든 기능을 제공할 수 있게 한다. 참고로 교재에서는 파이썬에서 함수를 일등 시민(first-class citizen)으로 표현하였다.
# 함수를 변수에 할당
greet = lambda name: print(f"Hello, {name}!")
greet("Alice") # 변수를 통해 함수 호출
## Hello, Alice!
# 함수를 인자로 전달
def apply_twice(func, arg):
func(arg)
func(arg)
apply_twice(greet, "Bob")
## Hello, Bob!
## Hello, Bob!
# 함수를 반환값으로 사용
def make_multiplier(n):
return lambda x: x * n
double = make_multiplier(2)
print(double(5))
## 10
(3) 인수와 매개변수
함수로 전달한 값을 인수라고 하는데, 함수 내부에서는 이를 매개변수라고 한다. 인수와 함수를 호출하면 인수의 값이 함수 내에서 해당하는 매개변수에 복사되는데 이 과정을 예시 코드를 통해 이해해보자.
def greet(name): # nameg은 매개변수
return "hello, " + name + "!"
comment = greet("Alice") # "Alice"는 인수
- "Alice"라는 문자열 인수와 함께 greet()함수를 호출
- "Alice" 문자열 값을 greet()함수의 name 매개변수에 복사
- 문자열을 반환하고 반환된 문자열을 comment 변수에 할당
파이썬은 함수의 인수처리 방식이 다른 언어에 비해 유연하고 독특한 특징이 있다.
- 위치 인수 : 가장 기본적인 인수 전달 방식으로, 함수 정의 시 선언된 매개변수 순서대로 인수를 전달
- 키워드 인수 : 매개변수 이름을 지정하여 인수를 전달하는데, 함수 정의와 다른 순서로 지정가능
- 위치 인수와 섞어서 쓸 수 도 있는데, 이 경우 위치 인수가 먼저 와야 함 - 기본 인수 : 매개변수에 기본 값을 지정할 수 있는데, 인수를 전달하지 않으면 기본값이 사용됨
- 기본 인수는 함수가 실행될 때 계산되는 것이 아니고 함수를 정의할 때 계산 된다. 따라서 가변 데이터 타입을 기본 인수로 사용하는 경우 유의해야 한다. - 가변 인수 : 함수에 전달되는 인수의 개수를 미리 알 수 없는 경우에 사용할 수 있는데, 애스터리스크(*)를 매개변수에 사용하면 여러 위치 인수 변수를 튜플로 묶는다. 관습적으로 *args 라는 이름으로 사용한다.
- 키워드 가변 인수 : 키워드 인수의 가변 버전이다. 두개의 애스터리스크를 사용하면 키워드 인수를 딕셔너리로 묶을 수 있다. 마찬가지로 **kwargs라는 이름을 관습적으로 사용한다.
- 키워드 전용 인수 : 위치 인수 뒤에 선언되는데, 함수 정의에서 *가 나오면 그 뒤에 오는 모든 매개 변수는 키워드 전용 인수가 된다.
- ex) def func(a,b,*,c,d) : c,d는 키워드 전용 인수이므로 이름=값의 형태로 제공해야 한다
(4) 독스트링
코드 작성에서 가독성은 매우 중요하다. 함수(뿐만 아니라 모듈, 클래스 등)에서는 독스트링(docstring)을 작성하여 가독성을 높일 수 있는데, 독스트링이란 함수 바디 시작 부분에 문자열을 포함시켜 함수 정의에 문서를 붙인 것을 말한다.
def greet(name):
"""
인사말을 출력하는 함수입니다.
Args:
name (str): 인사할 대상의 이름입니다.
Returns:
None
"""
print(f"Hello, {name}!")
함수의 독스트링은 help() 함수를 호출하여 출력할 수 있다. 위 예시의 경우 help(greet)이 될 것이다. 서식 없는 독스트링을 출력하고 싶다면 print(greet.__doc__)으로 작성하면 된다.
2. 함수 심화
(1) 네임스페이스와 스코프
네임스페이스(Namespace)는 이름과 객체를 매핑하는 공간을 말하는데, 다음과 같은 네임스페이스가 존재한다.
- 내장 네임스페이스(Built-in Namespace) : 파이썬 내장 함수, 예외 등이 정의된 네임스페이스
- 전역 네임스페이스(Global Namespace) : 모듈 수준에서 정의된 변수, 함수, 클래스 등이 포함된 네임스페이스
- 지역 네임스페이스(Local Namespace) : 코드 블록 내에서 정의된 변수, 함수등이 포함된 네임스페이스
메인 프로그램은 전역 네임스페이스를 정의하고, 각 함수는 자신의 네임스페이스를 정의한다. 네임스페이스라는 개념은 특정 이름이 유일하고 다른 네임스페이스에서의 같은 이름과 관계가 없음을 의미한다. 즉, 메인 프로그램에서 x라는 변수를 정의하고 함수에서 x라는 변수를 정의한 경우 이들은 서로 다른 것을 참조하게 된다.
스코프(Scope)는 이름이 유효한 범위를 말한다. 파이썬에서는 이름을 찾을때 1) 지역 스코프 2) 전역 스코프 3) 내장 스코프 순으로 찾는데, 이를 LEGB 규칙이라고 한다.
- 내장 스코프(Built-in Scope) : 내장 네임스페이스에 정의된 이름들의 스코프
- 전역 스코프(Global Scope) : 전역 네임스페이스에 정의된 이름들의 스코프
- 지역 스코프(Local Scope) : 지역 네임스페이스에 정의된 이름들의 스코프
다음 예시코드를 통해 깊은 이해를 도모해보자.
x = 10
def func1():
print(x)
def func2():
x = 20
print(x)
def func3():
print(x)
x = 20
func1() # 10
func2() # 20
func3() # UnBoundLocalError
- func1에서는 x가 전역변수로 참조되어 10이 출력된다.
- func2에서는 x가 지역변수로 선언되어 20이 출력된다.
- func3에서는 x가 지역변수로 간주되지만 초기화되기 전에 참조하려고 하여 오류가 발생한다.
- 이 부분이 처음에 와닿지 않았었는데, 파이썬은 함수 내부에서 사용될 변수를 함수가 정의될 때 분석하여 함수 내부에서 변수를 할당하는 코드가 있다면 지역 변수로 간주한다는 점을 이해하면 자연스럽게 받아들일 수 있다.
(2) 클로저
클로저(closure)는 함수와 그 함수가 정의될 때의 변수의 상태를 함께 묶어서 기억하는 기능을 의미한다. 구체적으로는 다음 세 가지 조건을 만족하는 함수를 클로저라고 한다.
- 외부함수의 내부에 정의된 내부함수일 것
- 외부함수의 지역변수를 사용하는 함수일 것
- 외부함수는 내부함수를 반환할 것
클로저의 간단한 예시를 살펴보자.
def make_counter():
count = 0 # 외부 함수의 변수
def counter():
nonlocal count # 외부 변수에 접근
count += 1
return count
return counter
my_counter = make_counter()
print(my_counter()) # 출력: 1
print(my_counter()) # 출력: 2
아직 많이 사용해보지 않은 유형의 함수라 어색한 면이 있다. 무엇보다도 클로저를 사용하는 이유가 무엇인지 궁금한데, 검색을 통해 다음과 같이 정리할 수 있었다.
- 데이터 은닉 : 클로저를 사용하여 특정 데이터를 외부에서 접근할 수 없도록 숨길 수 있음
- 상태 유지 : 함수가 호출될 때마다 상태를 유지할 수 있게 해줌
- 콜백 함수 : 클로저는 다른 함수에 전달되는 콜백 함수로 사용될 때 유용함
- 함수 생성기 : 클로저를 사용하여 여러 개의 유사한 함수를 동적으로 생성할 수 있음
(3) 제너레이터
제너레이터(generaotr)는 이터레이터의 일종으로 시퀀스를 생성하는 객체이다. range()는 대표적인 제너레이터라고 할 수 있다. 제너레이터는 모든 값을 한번에 메모리에 저장하지 않고 필요할 때마다 값을 생성하기 때문에 큰 데이터 집합을 다룰 때 메모리 사용을 최소화할 수 있다. 또한 클래스를 정의하고 __iter__()와 __next()__ 메서드를 명시적으로 구현해야 하는 이터레이터와 달리 제너레이터는 yield문을 포함하는 함수로 간단하게 구현할 수 있다.
def count_up_to(n):
count = 1
while count <= n:
yield count # 현재 count 값을 반환하고 상태를 유지
count += 1
# 제너레이터 객체 생성
counter = count_up_to(5)
# 제너레이터 사용
for number in counter:
print(number) # 출력: 1, 2, 3, 4, 5
제너레이터는 컴프리핸션으로 생성할 수 있는데, 괄호를 묶어서 사용하면 된다. 튜플 컴프리핸션이 없다는 점과 연결시켜서 기억하면 될 것이다.
squares = (x * x for x in range(10)) # 제너레이터 표현식
for square in squares:
print(square) # 출력: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81
(4) 데커레이터
코드를 바꾸지 않고 사용하고 있는 함수를 수정하고 싶은 경우 데커레이터(decorator)를 활용하면 된다. 데커레이터는 다른 함수를 인자로 받아서 새로운 함수를 반환하는 함수로, 이를 통해 함수의 동작을 변경하거나 추가 기능을 구현할 수 있다. 교재의 데커리에터 함수 예제를 살펴보자.
def document_it(func):
def new_function(*args, **kwargs):
print('Running function:', func.__name__)
print('Positional arguments:', args)
print('Keyword arguments:', kwargs)
result = func(*args, **kwargs)
print('Result:', result)
return result
return new_function
위 데커레이터 예시에서 다음 포인트를 확인할 수 있다.
- 내부에 래핑할 원래 함수를 호출하고, 동시에 여러 기능을 추가시키고 있다.
- *args, **kwargs를 통해 원래 함수가 전달받던 인수가 어떠한 형태여도 적용될 수 있도록 하였다.
- 함수 인수를 활용하고 있다.
위 데커레이터의 사용 예시를 살펴보자.
## 수동으로 데커레이터를 적용시킨 예시
def add_ints(a, b):
return a + b
cooler_add_ints = document_it(add_ints)
cooler_add_ints(3,5)
## 데커레이터의 일반적 사용 예시
@document_it
def add_ints(a, b):
return a + b
add_ints(3,5)
## output
# Running function: add_ints
# Positional arguments: (3, 5)
# Keyword arguments: {}
# Result: 8
수동으로 할당할 수 도 있으나, 두번째 예시와 같이 @를 활용하면 간편하게 데커레이터를 적용시킬 수 있다.
3. 기타 알게된 것들
- None은 False는 구분되는데 이는 생각보다 중요하다. None인지 여부는 is 연산자를 통해 확인한다.
- 파이썬은 네임스페이스의 내용을 접근하기 위해 두 가지 함수를 제공하는데, locals() 함수는 로컬 네임스페이스의 내용이 담긴 딕셔너리를 반환하고 globals()함수는 글로벌 네임스페이스의 내용이 담긴 딕셔너리를 반환한다.
- 전역변수는 모든 함수에서 접근 가능하지만, 함수 내부에서 전역변수를 새롭게 할당하려면 global 키워드를 사용해야 한다. 즉, 가변 타입의 전역 변수의 내용을 수정하는 것은 global 키워드 없이도 가능하다.
- 제너레이터에서 yield from을 사용하면 yield from 다음에 오는 이터러블의 모든 값을 순차적으로 반환할 수 있다.
- 함수는 여러 데커레이터를 가질 수 있는데, 이러한 경우 함수에서 가장 가까운 def 바로 위의 데커레이터를 먼저 실행한 뒤 그 위의 데커레이터가 실행된다.