03. 함수

함수 (function)

  • 함수의 정의

  • 함수의 Output

  • 함수의 Input

  • 함수와 스코프

  • 재귀 함수

func.png

다음의 코드를 봅시다. 무엇을 하는 코드일까요?

values = [100, 75, 85, 90, 65, 95, 90, 60, 85, 50, 90, 80]
total = 0
cnt = 0

for value in values:
    total += value
    cnt += 1
mean = total / cnt

total_var = 0
for value in values:
    total_var += (value - mean) ** 2
sum_var = total_var / cnt

target = sum_var
count = 0 
while True : 
    count += 1 
    root = 0.5 * (target + (sum_var / target))  
    if (abs(root - target) < 0.0000000000000001): 
        break 
    target = root

std_dev = target
print(std_dev)
14.499760534421096

이해하기 쉬운가요? 그리고 만약 다른 곳에서 동일한 작업을 다시해야할 경우 어떻게 해야 할까요?

import math
values = [100, 75, 85, 90, 65, 95, 90, 60, 85, 50, 90, 80]
cnt = len(values)
mean = sum(values) / cnt
sum_var = sum(pow(value - mean, 2) for value in values) / cnt
std_dev = math.sqrt(sum_var)
print(std_dev)
14.499760534421096

한줄로도 가능할까요?

import statistics
values = [100, 75, 85, 90, 65, 95, 90, 60, 85, 50, 90, 80]
statistics.pstdev(values)
14.499760534421096

3.1 함수

특정한 기능(function)을 하는 코드의 묶음

일의 단위

3.1.1 함수를 쓰는 이유

  • 가독성

  • 재사용성

  • 유지보수

programming principle

3.1.2 함수의 선언과 호출

  • 함수의 선언은 def 키워드를 활용합니다.

  • 들여쓰기(4spaces)로 함수의 body(코드 블록)를 작성합니다.

    • Docstring은 함수 body 앞에 선택적으로 작성 가능합니다.

  • 함수는 매개변수(parameter)를 넘겨줄 수도 있습니다. (인자 : argument)

  • 함수는 동작후에 return을 통해 결과값을 전달합니다.

    • 반드시 하나의 객체를 반환합니다 (return 값이 없으면, None을 반환)

  • 함수는 호출은 함수명()으로 합니다.

    • 예) func() / func(val1, val2)


활용법

def <함수이름>(parameter1, parameter2):
    <코드 블럭>
    return value

[연습] 세제곱 함수

입력 받은 수를 세제곱하여 반환(return)하는 함수 cube()을 작성해보세요.


[입력 예시]

cube(2)

[출력 예시]

8

# 위 문제를 참고하여 아래에 cube 함수를 작성하고 실행해봅시다.

def cube(num):                # num은 매개변수 (parameter)
    return num ** 3
cube = lambda num: num ** 3   # 위와 같은 식
# 해당 코드를 실행하여 잘 동작하는지 확인합니다.

cube(2)                       # 2는 인자 (argument)
8
function descrpition
# 우리가 활용하는 print문도 파이썬에 지정된 함수입니다. 
# 아래에서 'hi'는 argument이고 출력을 하게 됩니다.

print('hi')
hi
built_in

파이썬 문서

# 내장함수 목록을 직접 확인해봅시다.

dir(__builtins__)
['ArithmeticError',
 'AssertionError',
 'AttributeError',
 'BaseException',
 'BlockingIOError',
 'BrokenPipeError',
 'BufferError',
 'BytesWarning',
 'ChildProcessError',
 'ConnectionAbortedError',
 'ConnectionError',
 'ConnectionRefusedError',
 'ConnectionResetError',
 'DeprecationWarning',
 'EOFError',
 'Ellipsis',
 'EnvironmentError',
 'Exception',
 'False',
 'FileExistsError',
 'FileNotFoundError',
 'FloatingPointError',
 'FutureWarning',
 'GeneratorExit',
 'IOError',
 'ImportError',
 'ImportWarning',
 'IndentationError',
 'IndexError',
 'InterruptedError',
 'IsADirectoryError',
 'KeyError',
 'KeyboardInterrupt',
 'LookupError',
 'MemoryError',
 'ModuleNotFoundError',
 'NameError',
 'None',
 'NotADirectoryError',
 'NotImplemented',
 'NotImplementedError',
 'OSError',
 'OverflowError',
 'PendingDeprecationWarning',
 'PermissionError',
 'ProcessLookupError',
 'RecursionError',
 'ReferenceError',
 'ResourceWarning',
 'RuntimeError',
 'RuntimeWarning',
 'StopAsyncIteration',
 'StopIteration',
 'SyntaxError',
 'SyntaxWarning',
 'SystemError',
 'SystemExit',
 'TabError',
 'TimeoutError',
 'True',
 'TypeError',
 'UnboundLocalError',
 'UnicodeDecodeError',
 'UnicodeEncodeError',
 'UnicodeError',
 'UnicodeTranslateError',
 'UnicodeWarning',
 'UserWarning',
 'ValueError',
 'Warning',
 'WindowsError',
 'ZeroDivisionError',
 '__IPYTHON__',
 '__build_class__',
 '__debug__',
 '__doc__',
 '__import__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'all',
 'any',
 'ascii',
 'bin',
 'bool',
 'breakpoint',
 'bytearray',
 'bytes',
 'callable',
 'chr',
 'classmethod',
 'compile',
 'complex',
 'copyright',
 'credits',
 'delattr',
 'dict',
 'dir',
 'display',
 'divmod',
 'enumerate',
 'eval',
 'exec',
 'filter',
 'float',
 'format',
 'frozenset',
 'get_ipython',
 'getattr',
 'globals',
 'hasattr',
 'hash',
 'help',
 'hex',
 'id',
 'input',
 'int',
 'isinstance',
 'issubclass',
 'iter',
 'len',
 'license',
 'list',
 'locals',
 'map',
 'max',
 'memoryview',
 'min',
 'next',
 'object',
 'oct',
 'open',
 'ord',
 'pow',
 'print',
 'property',
 'range',
 'repr',
 'reversed',
 'round',
 'set',
 'setattr',
 'slice',
 'sorted',
 'staticmethod',
 'str',
 'sum',
 'super',
 'tuple',
 'type',
 'vars',
 'zip']
# 편하게 써왔던 random.sample() 함수의 내부도 직접 확인해봅시다.
# https://github.com/python/cpython/blob/master/Lib/random.py#L385

[연습] 함수 만들기

아래의 코드와 동일한 my_max 함수를 만들어주세요.

정수를 두개 받아서, 큰 값을 반환합니다.

my_max(1, 5)

출력 예시)
5
# 내장함수 max()를 확인해봅시다.

max(1, 5)
5
# 위 문제를 참고하여 아래에 my_max 함수를 작성하고 실행해보세요.


def my_max(num1, num2):
    if num1 > num2:
        return num1
    else:
        return num2
    
    num1 if num1 > num2 else num2
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

my_max(1, 5)

함수의 선언과 호출 살펴보기

# 아래의 코드의 결과는 무엇일까요? 실행하기 전에 예측해봅시다!

num1 = 0
num2 = 1

def func1(a, b):
    return a + b
    
def func2(a, b):
    return a - b
    
def func3(a, b):
    return func1(a, 5) + func2(5, b)
    
result = func3(num1, num2)
print(result)
  • Python tutor를 통해 실행 순서를 직접 확인하세요.

  • 함수는 호출되면 계산을 수행하고, 값을 반환하며 종료됩니다.

3.2 함수의 Output

함수의 return

앞서 설명한 것과 마찬가지로 함수는 반환되는 값이 있으며, 이는 어떠한 종류(~~의 객체~~)라도 상관없습니다.

단, 오직 한 개의 객체만 반환됩니다.

  • 복수의 객체를 return 하는 경우 -> 복수의 객체를 하나의 tuple로 반환합니다.

  • 명시적인 return 값이 없는 경우 -> 하나의 객체 None을 반환합니다.

함수가 return 되거나 종료되면, 함수를 호출한 곳으로 돌아갑니다.

return vs print

  • return은 함수 안에서만 사용되는 키워드

  • print는 출력을 위해 사용되는 함수

  • REPL (Read-Eval-Print Loop) 환경에서는 마지막으로 작성된 코드의 리턴 값을 보여주므로 같은 동작을 하는 것으로 착각할 수 있음.

[실습] 사각형의 넓이를 구하는 함수

너비와 높이를 입력 받아 사각형의 넓이와 둘레를 반환(return)하는 함수 rectangle()을 작성해보세요.


[입력 예시]

rectangle(30, 20)

[출력 예시]

(600, 100)

# 위 문제를 참고하여 아래에 rectangle 함수를 작성하고 실행해봅시다.

def rectangle(width, height):
    area = width * height
    parameter = 2 * (width + height)
    return area, parameter
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.
print(rectangle(30, 20))
print(rectangle(50, 70))
(600, 100)
(3500, 240)

[연습] 함수를 정의하고 값을 반환해봅시다.

리스트 두개를 받아 각각 더한 결과를 비교하여 값이 큰 리스트를 반환하는 함수를 만들어주세요.

my_list_max([10, 3], [5, 9])

예시 출력)
[5, 9]
# 위 문제를 참고하여 아래에 my_list_max 함수를 작성하고 호출하세요.

def my_list_max(list1, list2):
    if sum(list1) > sum(list2):
        return list1
    else:
        return list2
def my_list_max(list1, list2):
    return list1 if sum(list1) > sum(list2) else list2
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

print(my_list_max([10, 3], [5, 9]))
[5, 9]

3.3 함수의 입력 (Input)

3.3.1 매개변수 & 전달인자

(1) 매개변수 (parameter)

def func(x):
      return x + 2
  • x 는 매개변수(parameter)입니다.

  • 입력을 받아 함수 내부에서 활용할 변수라고 생각하면 됩니다.

  • 함수의 정의 부분에서 볼 수 있습니다.

(2) 전달인자 (argument)

func(2)
  • 2 는 (전달)인자(argument)

  • 실제로 전달되는 입력값이라고 생각하면 됩니다.

  • 함수를 호출하는 부분에서 볼 수 있습니다.

주로 혼용해서 사용하지만 엄밀하게 따지면 둘은 다르게 구분되어 사용됩니다. 개념적 구분보다 함수가 작동하는 원리를 이해하는게 더 중요합니다.

3.3.2 함수의 인자

함수는 입력값(input)으로 인자(argument)를 넘겨줄 수 있습니다.

위치 인자

기본적으로 인자는 위치에 따라 함수 내에 전달(Positional Arguments)됩니다.

[연습] 원기둥의 부피

원기둥의 반지름(r)과 높이(h)를 받아서 부피를 return하는 함수 cylinder()를 작성하세요.

원기둥 부피 = 3.14 * 반지름 * 반지름 * 높이

# 위 문제를 참고하여 아래에 cylinder 함수를 작성하고 호출하세요.

def cylinder(r, h):
    return round(3.14 * r**2 * h, 5)
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

print(cylinder(5, 2))
print(cylinder(2, 5)) # 순서를 바꾸면 다른 값이 나옵니다.
157.0
62.8
function example 02

기본 인자 값

함수를 정의할 때, 기본값 (Default Argument Values)을 지정하여 함수를 호출할 때 인자의 값을 설정하지 않도록하여, 정의된 것 보다 더 적은 개수의 인자들로 호출 될 수 있습니다.


활용법

def func(p1=v1):
    return p1
[연습] 기본 인자 값 활용

이름을 받아서 다음과 같이 인사하는 함수 greeting()을 작성하세요. 이름이 길동이면, “길동, 안녕?” 이름이 없으면 “익명, 안녕?” 으로 출력하세요.

# 위 문제를 참고하여 아래에 greeting 함수를 작성하고 실행해봅시다.

def greeting(name = '익명'):
    return f'{name}, 안녕?'
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

print(greeting())
print(greeting('철수'))
익명, 안녕?
철수, 안녕?
  • 기본 인자 값이 설정되어 있더라도 기존의 함수와 동일하게 호출 가능합니다.

  • 호출시 인자가 없으면 기본 인자 값이 활용됩니다.

function example 03

Warning

단, 기본 인자값(Default Argument Value)을 가지는 인자 다음에 기본 값이 없는 인자를 사용할 수는 없습니다.

  • 위치 매개변수 - 기본 인자 매개변수 순으로 정의해야 함.

  • 순서) 위치인자(basic) => 기본인자(default)

# 다음 코드를 실행해서 오류를 확인해봅시다.

def greeting(name='john', age):
    return f'{name}{age}살입니다.'
  File "<ipython-input-20-85fc8bb36d98>", line 2
    def greeting(name='john', age):
                ^
SyntaxError: non-default argument follows default argument
# 오류가 발생하지 않도록 아래에 직접 수정하고 실행해봅시다.

def greeting(age, name='john'):
    return f'{name}{age}살입니다.'
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

print(greeting(1))
print(greeting(2, 'json'))
john은 1살입니다.
json은 2살입니다.

키워드 인자

함수를 호출할 때 키워드 인자 (Keyword Arguments)를 활용하여 직접 변수의 이름으로 특정 인자를 전달할 수 있습니다.

# 다음 코드를 실행해서 greeting 함수를 선언합니다.

def greeting(age, name):
    return f'{name}{age}살입니다.'
# 아래와 같이 키워드 인자를 사용해서 함수를 호출할 수 있습니다.

greeting(name='철수', age=24)
'철수은 24살입니다.'
# 위치 인자와 함께 사용할 수 있습니다.

greeting(24, name='철수')
'철수은 24살입니다.'

Warning

단, 아래와 같이 키워드 인자를 활용한 다음에 위치 인자를 활용할 수는 없습니다.

# 다음 코드를 실행해서 오류를 확인해봅시다.

greeting(age=24, '철수')
  File "C:\Users\User\AppData\Local\Temp/ipykernel_14428/681848598.py", line 2
    greeting(age=24, '철수')
                         ^
SyntaxError: positional argument follows keyword argument

순서

  • 정의할 때 parameter 순서

    • basic => default value

  • 호출할 때 argument 순서

    • position => keyword

3.3.3 정해지지 않은 여러 개의 인자 처리

우리가 주로 활용하는 print() 함수는 파이썬 표준 라이브러리의 내장함수 중 하나이며, 다음과 같이 구성되어있습니다.

print

# 직접 실행해서 확인해봅시다.

print('첫번째 문장')
print('두번째 문장', end='_')
print('세번째 문장', '네번째 문장')
print('다섯번째 문장', '마지막 문장', sep='/', end='끝!')
첫번째 문장
두번째 문장_세번째 문장 네번째 문장
다섯번째 문장/마지막 문장끝!

가변(임의) 인자 리스트

앞서 설명한 print()처럼 개수가 정해지지 않은 임의의 인자를 받기 위해서는 함수를 정의할 때 가변 인자 리스트(Arbitrary Argument Lists) *args를 활용합니다.

가변 인자 리스트는 tuple 형태로 처리가 되며, 매개변수에 *로 표현합니다.


활용법

def func(a, b, *args):

*args : 임의의 개수의 위치인자를 받음을 의미

보통, 이 가변 인자 리스트는 매개변수 목록의 마지막에 옵니다.

Warning

가변 인자 리스트가 위치 인자보다 앞쪽에 올 수 없습니다.

# 가변 인자 예시
# print문은 *obejcts를 통해 임의의 숫자의 인자를 모두 처리합니다.
# 아래의 코드로 확인해봅시다.

print('hi', '안녕', 'Guten Tag', 'gonnichiwa', sep=',')
hi,안녕,Guten Tag,gonnichiwa
# args는 함수 내부에서 tuple로 처리됩니다.
# 아래의 코드로 확인해봅시다.

def my_func(*args):
    return args
    
print(my_func(1, 2))
print(type(my_func(1, 2)))
(1, 2)
<class 'tuple'>
[연습] 가변 인자 리스트를 사용해봅시다.

정수를 여러 개 받아서 가장 큰 값을 반환(return)하는 함수 my_max()를 작성하세요.

max 내장 함수 사용은 금지합니다.

my_max(10, 20, 30, 50)

예시출력)
50
max(1, 2, 3, 4)
4
# 위 문제를 참고하여 아래에 my_max 함수를 작성하세요.

def my_max(*args):
    return max(args)
# 다음 코드를 실행해 올바른 결과가 나오는지 확인하세요.

my_max(-1, -2, -3, -4)
-1

가변(임의) 키워드 인자

정해지지 않은 키워드 인자들은 함수를 정의할 때 가변 키워드 인자 (Arbitrary Keyword Arguments) **kwargs를 활용합니다.

가변 키워드 인자는 dict 형태로 처리가 되며, 매개변수에 **로 표현합니다.


활용법

def func(**kwargs):

**kwargs : 임의의 개수의 키워드 인자를 받음을 의미합니다

우리가 dictionary를 만들 때 사용할 수 있는 dict() 함수는 파이썬 표준 라이브러리의 내장함수 중 하나이며, 다음과 같이 구성되어 있습니다.

dictionary

# 딕셔너리 생성 함수 예시입니다.(가변 키워드 인자 활용)

# hi = {'한국어': '안녕', '영어': 'hi'}
hi = dict(한국어='안녕', 영어='hi')
print(hi)
{'한국어': '안녕', '영어': 'hi'}
# 주의사항
# 식별자는 숫자만으로는 이루어질 수가 없습니다.(키워드인자로 넘기면 함수 안에서 식별자로 쓰이기 때문)
# 코드를 실행해서 오류를 확인해봅시다.

dict(1=1, 2=2) # X
  File "C:\Users\User\AppData\Local\Temp/ipykernel_14428/1178151218.py", line 5
    dict(1=1, 2=2) # X
         ^
SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
# Key가 숫자인 딕셔너리를 만들고 싶다면, 아래와 같이 사용해야합니다.

dict([(1, 1), (2, 2)])
dict(((1,1), (2,2)))
{1: 1, 2: 2}
# 아래의 코드를 실행시켜 kwargs가 딕셔너리로 처리되는 것을 확인해봅시다.

def my_dict(**kwargs):
    return kwargs

print(my_dict(한국어='안녕', 영어='hi', 독일어='Guten Tag'))
{'한국어': '안녕', '영어': 'hi', '독일어': 'Guten Tag'}

Note

위치인자와 *agrs, **kwargs를 함께 사용했을 때 올바른 순서

my_info(x, y, *args, **kwargs)

Tip

*가 하나일 경우 tuple, **일 경우 dictionary로 처리된다.

[실습] URL 생성기

my_url() 함수를 만들어 완성된 URL을 반환하는 함수를 작성하세요.

my_url(sidoname='서울', key='asdf')

예시 출력)
https://api.go.kr?sidoname=서울&key=asdf&
# 입력받은 가변 키워드 인자를 활용하여 'https://api.go.kr?'를 BASE_URL로 사용하는 URL을 생성해봅시다.
# 가변 키워드 인자(kwargs)를 사용하는 my_url 함수를 직접 작성해보세요.

def my_url(**kwargs):
    url = 'https://api.go.kr?'
    for k, v in kwargs.items():
        url += f'{k}={v}&'
    return url[:-1]
# 다음 코드를 실행하여 결과를 확인해보세요.

print(my_url(sidoname='서울', key='asdf'))
https://api.go.kr?sidoname=서울&key=asdf

3.4 함수와 스코프

함수는 코드 내부에 스코프 (scope)를 생성합니다. 함수로 생성된 공간은 지역 스코프 (local scope)라고 불리며, 그 외의 공간인 전역 스코프 (global scope)와 구분됩니다.

  • 전역 스코프(global scope): 코드 어디에서든 참조할 수 있는 공간

  • 지역 스코프(local scope): 함수가 만든 스코프로 함수 내부에서만 참조할 수 있는 공간

  • 전역 변수(global variable): 전역 스코프에 정의된 변수

  • 지역 변수(local variable): 지역 스코프에 정의된 변수

# 전역 스코프와 지역 스코프를 알아봅시다.

# 전역 스코프에서는 지역 스코프의 변수를 참조할 수 없습니다.
# 그러나 반대로 지역스코프에서 전역스코프의 변수는 참조(접근)할 수 있습니다.

# func 함수 바깥에서 함수 안의 지역 변수 c를 출력하고 오류를 확인해보세요.
# 그리고 반대로 func 함수 내부에서 전역 변수 a를 출력하고 결과를 확인해보세요.

a = 10

def func():
    c = 20
    print(a)

func()
# print(c)
print(a)
10
10
# 전역 스코프와 지역 스코프에 같은 이름의 변수를 만들면 어떻게 될까요?
# 아래 코드를 실행시켜보고 결과를 확인해보세요.

a = 10 # 전역 변수(global)

def func(b):
    a = 30 # 지역 변수(local variable)
    print(a)
    
func(a)
print(a)
30
10

3.4.1 변수의 수명주기

변수의 이름은 각자의 수명주기 (lifecycle)가 있습니다.

  • 빌트인 스코프 (built-in scope): 파이썬이 실행된 이후부터 영원히 유지

  • 전역 스코프 (global scope): 모듈이 호출된 시점 이후 혹은 이름 선언된 이후부터 인터프리터가 끝날 때 까지 유지

  • 지역(함수) 스코프 (local scope): 함수가 호출될 때 생성되고, 함수가 종료될 때까지 유지 (함수 내에서 처리되지 않는 예외를 일으킬 때 삭제됨)

3.4.2 이름 검색 규칙 (resolution)

파이썬에서 사용되는 이름(식별자)들은 이름공간 (namespace)에 저장되어 있습니다.

아래와 같은 순서로 이름을 찾아나가며, LEGB Rule 이라고 부릅니다.

  • Local scope: 함수

  • Enclosed scope: 특정 함수의 상위 함수

  • Global scope: 함수 밖의 변수 혹은 import된 모듈

  • Built-in scope: 파이썬안에 내장되어 있는 함수 또는 속성

즉, 함수 내에서는 바깥 스코프의 변수에 접근 가능하나 수정은 할 수 없습니다.

# 이것을 통해 첫시간에 내장함수의 식별자를 사용할 수 없었던 예제에서 오류가 생기는 이유를 확인할 수 있습니다.
# Built-in scope와 Global scope를 알아봅시다.

# 아래 코드를 실행하여 오류를 확인해보세요.

print = 'hello'
print(3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_14724/2300548552.py in <module>
      5 
      6 print = 'hello'
----> 7 print(3)

TypeError: 'str' object is not callable
# print 함수를 다시 사용할 수 있도록 print라는 이름의 변수를 삭제합니다.

del print
  1. print() 코드가 실행되면

  2. 함수에서 실행된 코드가 아니기 때문에 L, E 를 건너 뛰고,

  3. print라는 식별자를 Global scope에서 찾아서 print = ssafy를 가져오고,

  4. 이는 함수가 아니라 변수이기 때문에 not callable하다라는 오류를 내뱉게 됩니다.

  5. 우리가 원하는 print()은 Built-in scope에 있기 때문입니다.

# Global scope와 Local scope를 알아봅시다.
# 함수 밖의 변수 a는 전역 변수에 해당하고, 함수 내부의 변수 a는 지역 변수에 해당합니다.
# 함수의 실행 결과로 어떤 변수의 값이 반환되는지 확인해보세요.

a = 1                  # gloabl
def local_scope(a):
    a = 3              # local
    print(a)

local_scope(a)
3
# LEGB Rule을 자세히 알아봅시다.

# 아래 코드를 실행시켜보고 print문에서 출력되는 
# 각 변수가 어느 스코프에 해당하는 변수인지 확인해보고 왜 그렇게 되는지 고민해보세요.

a = 10                        # gloabl
b = 20                        # global
def enclosed():
    a = 30                    # enclosed
    def local():
        c = 40                # local
        print(a, b, c)
        # B   E  G  L
    local()
    a = 50                    # enclosed 함수의 local
enclosed()
30 20 40
# 스코프는 함수에서만 적용됩니다.
# 아래와 같이 for문을 확인해보세요.

a = 1

for a in [1, 2, 3]:         # for문을 통해 global a 할당
    pass

print(a)
3

nonlocal

전역을 제외하고 가장 가까운 (둘러 싸고 있는) 스코프의 변수를 연결하도록 합니다.

  • nonlocal에 나열된 이름은 같은 코드 플록에서 nonlocal 앞에 등장할 수 없음.

  • nonlonal에 나열된 이름은 매개변수, for 루프 대상, 클래스/함수 정의 등으로 정의되지 않아야 함.

global과는 달리 이미 존재하는 이름과의 연결만 가능함

# 전역 변수를 바꿀 수 있을까요?

# 기본적으로 지역 스코프에서 전역 스코프의 변수를 바꿀 수는 없습니다.
# 아래 코드에서 함수 내부의 global_num은 지역 변수로 생성됩니다.
# 코드를 실행시킨 뒤 결과를 확인해보세요.

global_num = 3
def local_scope():
    global_num = 5

local_scope()
print(global_num)
3

global

현재 코드 블록 전체에 적용되며, 나열된 식별자(이름)들이 전역 변수임을 나타냅니다.

  • global에 나열된 이름은 같은 코드 블록에서 global 앞에 등장할 수 없음.

  • global에 나열된 이름은 매개변수, for 루프 대상, 클래스/함수 정의 등으로 정의되지 않아야 함.

# 전역에 있는 변수를 바꾸고 싶다면, 아래와 같이 선언할 수 있습니다. (일반적으로 권장 X)

# global 키워드를 사용하여 지역 스코프에서 전역 변수의 값을 바꿀 수 있습니다.
# 코드를 실행시킨 뒤 결과를 확인해보세요.

global_num = 3
def local_scope():
    global global_num
    global_num = 5

local_scope()
print(global_num)
5

Warning

  • 기본적으로 함수에서 선언된 변수는 Local scope에 생성되며, 함수 종료 시 사라집니다.

  • 해당 스코프에 변수가 없는 경우 LEGB rule에 의해 이름을 검색합니다.

    • 변수에 접근은 가능하지만, 해당 변수를 재할당할 수는 없습니다.

    • 값을 할당하는 경우 해당 스코프의 이름공간에 새롭게 생성되기 때문입니다.

    • 단, 함수 내에서 필요한 상위 스코프 변수는 인자로 넘겨서 활용합니다. (*클로저 제외)

  • 상위 스코프에 있는 변수를 수정하고 싶다면 global, nonlocal 키워드를 활용 가능합니다.

    • 단, 코드가 복잡해지면서 변수의 변경을 추적하기 어렵고, 예기치 못한 오류가 발생합니다.

    • 가급적 사용하지 않는 것을 권장하며 함수로 값을 바꾸고자 한다면 항상 인자로 넘기고 리턴 값을 사용하는 것을 추천

클로저란?

어떤 함수 내부에 중첩된 형태로써 외부 스코프 변수에 접근 가능한 함수

var_x = 1
var_a = 2

def x():
    var_x = 10              # enclosed
    
    def y():
        nonlocal var_x      # enclosed
        global var_a        # global var_a = 1000
        var_x = 100
        var_a = 1000
        print(var_x, var_a) 
    y()
x()
print(var_x, var_a)         # global var_x = 1 

# 그러나 가급적 사용하지 말자.
100 1000
1 1000

3.5 재귀 함수

재귀 함수(recursive function)는 함수 내부에서 자기 자신을 호출 하는 함수를 뜻합니다.

  • 무한한 호출을 목표로 하는 것이 아니며, 알고리즘을 설계 및 구현에서 유용하게 활용됩니다.

    • 알고리즘 중 재귀 함수로 로직을 표현하기 쉬운 경우가 있습니다. (ex. 점화식)

    • 변수의 사용이 줄어들며, 코드의 가독성이 높아집니다.

  • 1개 이상의 base case(종료되는 상황)가 존재하고, 수렴하도록 작성합니다.

    • 같은 문제를 다른 Input 값을 통해서 해결하는 과정

      • 큰 문제를 해결하기 위해 작은 문제로 좁히고, 작은 문제의 해답을 이용하여 해결

    • 작은 문제는 base case에 도달하여 재귀 함수가 끝날 수 있도록 함.

팩토리얼 계산

팩토리얼은 1부터 n 까지 양의 정수를 차례대로 곱한 값이며 ! 기호로 표기합니다. 예를 들어 3!은 3 * 2 * 1이며 결과는 6 입니다.

팩토리얼(factorial)을 계산하는 함수 fact(n)를 작성하세요.

n은 1보다 큰 정수라고 가정하고, 팩토리얼을 계산한 값을 반환합니다.

\[ \displaystyle n! = \prod_{ k = 1 }^{ n }{ k } \]
\[ \displaystyle n! = 1*2*3*...*(n-1)*n \]

예시 출력)
120

반복문을 이용한 팩토리얼 계산

# 팩토리얼을 반복문을 이용하여 구현해봅시다.

def fact(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

# while문으로 풀기

# def fact(n):
#     result = 1
#     while n > 1:
#         result *= n
#         n -= 1
#     return result
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.
fact(5)
120

재귀를 이용한 팩토리얼 계산

1! = 1
2! = 1 * 2 = 1! * 2 
3! = 1 * 2 * 3 = 2! * 3
# 재귀를 이용하여 팩토리얼을 구현해봅시다.
# 여기에 코드를 작성하세요.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.
factorial(5)
120

반복문과 재귀함수

factorial(3)
3 * factorail(2)
3 * 2 * factorial(1)
3 * 2 * 1
3 * 2
6

두 코드 모두 원리는 같습니다!

  1. 반복문 코드

    • n이 1보다 큰 경우 반복문을 돌며, n은 1씩 감소합니다.

    • 마지막에 n이 1이면 더 이상 반복문을 돌지 않습니다.

  2. 재귀 함수 코드

    • 재귀 함수를 호출하며, n은 1씩 감소합니다.

    • 마지막에 n이 1이면 더 이상 추가 함수를 호출하지 않습니다.

  • 재귀함수는 기본적으로 같은 문제이지만 점점 범위가 줄어드는 문제를 풀게 됩니다.

  • 재귀함수를 작성시에는 반드시, base case가 존재 하여야 합니다.

  • base case는 점점 범위가 줄어들어 반복되지 않는 최종적으로 도달하는 곳을 의미합니다.

  • 재귀를 이용한 팩토리얼 계산에서의 base case는 n이 1일때, 함수가 아닌 정수 반환하는 것입니다

재귀 함수 주의 사항

  • 팩토리얼 재귀함수를 Python Tutor에서 확인해보면, 함수가 호출될 때마다 메모리 공간에 쌓이는 것을 볼 수 있습니다.

  • 이 경우, 메모리 스택이 넘치거나(Stack overflow) 프로그램 실행 속도가 늘어지는 단점이 생깁니다.

  • 파이썬에서는 이를 방지하기 위해 1,000번이 넘어가게 되면 더이상 함수를 호출하지 않고, 종료됩니다. (최대 재귀 깊이 (maximum recursion depth))

  • 호출 횟수가 최대 재귀 깊이를 넘어가게 되면 Recursion Error가 발생합니다.

import sys
print(sys.getrecursionlimit())
3000

최대 재귀 깊이

def ssafy():
    print('Hello, ssafy!')
    ssafy()
 
ssafy()

ssafy()를 호출하면 아래와 같이 문자열이 계속 출력되다가 RecursionError가 발생합니다.

파이썬에서는 최대 재귀 깊이(maximum recursion depth)가 1,000으로 정해져 있기 때문입니다.


Hello, world!
Hello, world!
...
Hello, world!
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)

...

<ipython-input-11-2bbb40950c86> in hello()
      1 def hello():
      2     print('Hello, world!')
----> 3     hello()
      4 
      5 hello()

RecursionError: maximum recursion depth exceeded while calling a Python object
# 직접 오류를 확인하세요.

# def ssafy():
#     print('Hello, ssafy!', end=" ")
#     ssafy()

# ssafy()

[실습] 피보나치 수열

첫째 및 둘째 항이 1이며 그 뒤의 모든 항은 바로 앞 두 항의 합인 수열입니다.

(0), 1, 1, 2, 3, 5, 8

피보나치 수열은 다음과 같은 점화식이 있습니다.

피보나치 값을 리턴하는 두가지 방식의 코드를 모두 작성해주세요.

\[ \displaystyle F_0 = F_1 = 1 \]
\[ F_n=F_{n-1}+F_{n-2}\qquad(n\in\{2,3,4,\dots\}) \]
  1. fib(n) : 재귀함수

  2. fib_loop(n) : 반복문 활용한 함수


예시 입력)
fib(10)

예시 호출)
55
# 재귀를 이용한 코드 fib() 를 작성하세요.

def fib(n):
    if n < 2:
        return n
    else:
        return fib(n-1) + fib(n-2)
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.

fib(10)
55
# 반복문(for문)을 이용한 코드 fib_loop() 를 작성하세요.

def fib_loop(n):
    result = []
    for i in range(n):
        if i in (0, 1):
            result.append(1)
        else:
            result.append(result[i-2] + result[i-1])
    return result[n-1]
def fib_loop(n):
    if n < 2:
        return n
    
    result = [0, 1]
    
    for i in range(2, n+1):
        next_num = result[i-1] + result[i-2]
        result.append(next_num)
    return result[-1]
# 해당 코드를 통해 올바른 결과가 나오는지 확인하세요.
fib_loop(10)
55
# 반복문(while 문)을 이용한 코드 fib_loop2()을 작성하세요.

def fib_loop2(n):
    i = 0
    result = []
    while i < n + 1:
        if i in (0, 1):
            result.append(1)
        else:
            result.append(result[i-2] + result[i-1])
        i += 1
    return result[n-1]

fib_loop2(10)
55

반복문과 재귀 함수의 차이

  • 알고리즘 자체가 재귀적인 표현이 자연스러운 경우 재귀함수를 사용합니다.

  • 재귀 호출은 변수 사용 을 줄여줄 수 있습니다.

# 재귀 호출은 입력 값이 커질 수록 연산 속도가 오래걸립니다.
# fib() 함수에 10 이상의 값을 넣어보고 실행한 뒤 연산 시간을 확인해보세요.

import time

start = time.time()
fib(30)
end = time.time()

print(end - start)
0.13866543769836426
# 반복문은 재귀로 구현된 함수보다 연산 속도가 빠른 편입니다.
# fib_loop() 함수에 10 이상의 값을 넣어보고 실행한 뒤 연산 시간을 확인해보세요.
# 그리고 100배 더 큰 1000 이상의 값도 넣어보고 실행한 뒤 연산 시간을 확인해보세요.

import time

start = time.time()
fib_loop(10000)
end = time.time()

print(end - start)
0.004734039306640625
# 시간을 더 정확하게 측정하는 방법

from timeit import default_timer as timer
from datetime import timedelta

start = timer()
fib(30)
end = timer()

print(timedelta(seconds = end - start))
0:00:00.139081
from timeit import default_timer as timer
from datetime import timedelta

start = timer()
fib_loop(10000)
end = timer()

print(timedelta(seconds = end - start))
0:00:00.004688

convention

  1. 리스트 / 배열을 담는 변수명은 복수형으로

  2. 함수 이름은 동사형으로 짓는다. 이름에서 return의 type을 추측할 수 있으면 더 좋다. (ex. get_fibonacci(), is_adult(), get_lotto_numbers())