상세 컨텐츠

본문 제목

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(4) 일급함수-3,4

경험/2021 인프런리프2기

by mizu-umi 2021. 3. 28. 14:05

본문

728x90

관련글 ▼

[인프런 리프 2기] 08. 파이썬 중급 과정 3주차(1) 시퀀스-1,2

[인프런 리프 2기] 09. 파이썬 중급 과정 3주차(2) 시퀀스-3,4

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(3) 일급함수-1,2

 


 

 

파이썬 일급함수 (3) 클로저 심화

  • 클로저 사용예제
  • 잘못된 클로저 사용
  • 클로저 정리

 

지난 번 수업에서 클로저를 클래스로 구현해보면서 클로저는 특별한 attribute를 사용해서 만드는 것이 아니란 걸 알았는데 클로저 심화와 데코레이터를 배우면서 확실하게 알았다. 함수를 활용해서 만든 함수(?)라고 이해하면 되는 거였다. 물론, 내 입장에서는 그렇게 외워야 머리 속에 잘 들어오고 이해하기 쉽기 때문에 단순화 시킨 것이지만 말이다.

 


 

클로저의 정의

외부에서 호출된 함수의 변수값, 또는 상태(reference)를 복사(snapshot)하고 저장 후에 접근(access) 가능하도록 해주는 것. 간단히 말하면 값을 저장해서 새로운 값과 함께 누적해준다는 의미.

 

# 클래스 이용
class Averager():
    def __init__(self):
        self._series = [] # [] 리스트형에 값을 넣는게 closure라고 보면 되겠네.

    def __call__(self, v): # call을 이용하면 class를 함수처럼 사용할 수 있다.
        self._series.append(v)
        print('inner >> {} / {}'.format(self._series, len(self._series)))
        return sum(self._series) / len(self._series)

앞서 클래스를 이용해서 closure를 구현해보았다. 이번에는 함수만 이용해서 closure를 구현해본다.

 

함수를 이용한 클로저

def closure_ex1():
    series = []
    def averager(v):
        series.append(v)
        print('inner >> {} / {}'.format(series, len(series)))
        return sum(series) / len(series)
    return averager

여기서 averager라는 함수 밖에 있는 series라는 변수는 free variable, 즉 자유변수라고 불린다. 자유 변수는 closure scope에서 내가 적용하려는 함수 바깥에서 선언된 변수라고 할 수 있다.

 

만약 series라는 변수가 global scope에 존재한다면 값은 누적된다. 하지만 현재 local scope에 존재하기 때문에 series(빈 리스트)는 averager라는 함수를 호출하는 순간 소멸(더이상 빈리스트가 아니며 값이 바뀌니까)된다.

 

avg_closure1 = closure_ex1

print(avg_closure1) 

avg_closure1라는 변수에 closure_ex1을 그냥 호출하면 함수가 return된다. 왜? 해당 closure 안에 averager를 return 하라고 마무리지었으니까.

 

avg_closure1 = closure_ex1()

print(avg_closure1(10))
print(avg_closure1(30))
print(avg_closure1(50))

함수를 실행할 때마다 새로운 값이 누적되도록 만든다.

 

함수 검사 Function Inspection

print(dir(avg_closure1))
print()
print(dir(avg_closure1.__code__))
print()
print(avg_closure1.__code__.co_freevars) # free variable을 출력
print(avg_closure1.__closure__[0].cell_contents) # contents를 출력

 


 

잘못된 Closure의 사용

def closure_ex2():
    # free variable
    cnt = 0
    total = 0
    
    def averager(v):
        cnt += 1
        total += v 
        return total / cnt
    return averager

분명 closure의 구조를 갖는 함수이다.

 

avg_closure2 = closure_ex2()
print(avg_closure2(10)) 

avg_closure2를 출력하면 UnboundLocalError: local variable 'cnt' referenced before assignment 오류가 발생한다.

 

왜? 우선은 averager라는 함수 scope 안에서 cnt라는 변수에 무언가 할당되어야 하는데, cnt 에 아무것도 할당되지 않은 상태에서 cnt에 1을 더하고 total에 v 값을 더하는 것이기 때문에 실행하면 오류가 발생하게 된다.

 

위와 같은 closure를 수정하는 방법은 아래와 같다.

def closure_ex3():
    # free variable
    cnt = 0
    total = 0
    
    def averager(v):
        nonlocal cnt, total
        cnt += 1
        total += v
        return total / cnt
    return averager

nonlocal을 이용함으로써 averager 바깥 영역에 있는 cnt, total을 참조한다.

 

변수가 local 바깥, 즉 global scope에 있을 경우 함수 안에 global을 사용하는 것과 유사하다.

 

다만, global variable (전역변수) 를 사용하는 것은 실수가 발생할 가능성이 높아지므로 별로 좋지 않다. nonlocal이 local scope안에서 global같은 역할을 하는데  함수는 호출하지 않는 이상 실행되지 않으니 closure을 이용해서 변수를 할당하고 값을 누적하는 게 낫다.

 


 

 

파이썬 일급함수 (4) 데코레이터

  • 클로저 -> 데코레이터 관계
  • 데코레이터 실습(1)
  • 데코레이터 실습(2)

 

데코레이터 Decorator

클로저의 한 형태를 가지고 있으며 쉽게 말해서 장식해주는 역할을 한다고 보면 된다.

 

장점

  1. 중복을 제거하고 코드를 간결하게 해주며 공통 함수를 작성할 수 있다
  2. 로깅, 프레임워크, 유효성 체크 등 한번에 수백가지의 수행을 해야할 때 공통 기능으로써 사용할 수 있다.
  3. 조합해서 사용하기 용이하다.

 

단점

  1. 너무 많이 쓰면 가독성이 떨어진다. 
  2. 특정 기능에 한정된 함수는 단일 함수로 작성하는게 낫다. 
  3. 디버깅이 불편하다.

 

예를 들어 @classmethod, @staticmethod 같이 골뱅이(@)로 시작하는 친구들이 바로 decorator다. 나중에 다 만들어놓고 사용하기는 편하지만 만드는 게 복잡하고 힘들다.

 

요약: @로 작성된 예약어 밑에 작성된 함수가 곧 decorator

 


 

데코레이터 실습

사람들이 무언가를 클릭할때마다 통계를 누적해주는 프로그래밍은 주로 static으로 짠다. 데코레이터로 통계 기능을 만들어주면 편하게 알 수 있다.

 

import time

def perf_clock(func):
    
    def perf_clocked(*args):
        # 함수 시작 시간
        st = time.perf_counter() 

        # 함수 실행 
        result = func(*args) 
    
        # 함수 종료 시간
        et = time.perf_counter() - st

        # 실행 함수명
        name = func.__name__ 

        # 함수 매개변수
        arg_str = ','.join(repr(arg) for arg in args)

        # 결과 출력
        print('[%0.5fs] %s(%s) -> %r' % (et, name, arg_str, result))

        return result
    return perf_clocked

위와 같은 decorator를 작성해준 다음 아무 함수를 짜본다.

 

def time_func(seconds):
    time.sleep(seconds)

def sum_func(*numbers):
    return sum(numbers)

 

데코레이터를 사용하지 않았을때

non_deco1 = perf_clock(time_func)
non_deco2 = perf_clock(sum_func)

none_deco1이라는 변수에 perf_clock이라는 함수를 할당 및 time_func를 호출한다.

-> 복잡하게 짜야한다.

 

print('-'*40, 'Called None Decorator')
print()
non_deco1(1.5)
print('-'*40, 'Called None Decorator')
print()
non_deco2(100,200,300,400,500)

위의 결과를 출력해보면 아래와 같다.

---------------------------------------- Called None Decorator

[1.50970s] time_func(1.5) -> None
---------------------------------------- Called None Decorator

[0.00000s] sum_func(100,200,300,400,500) -> 1500

 

데코레이터를 사용할 때

@perf_clock 
def time_func1(seconds):
    time.sleep(seconds)

def sum_func1(*nums):
    return sum(nums)

데코레이터의 outer function을 @으로 선언해주면 데코레이터를 사용하는 것이 된다.

 

time_func1(1.5)
sum_func1(100,200,300,400,500)

훨씬 간략하게 원하는 값을 도출할 수 있다.

 

[0.00001s] sum_func(100,200,300,400,500) -> 1500

[1.50141s] time_func1(1.5) -> None

 

 


< 강의 출처 - 인프런 우리를 위한 프로그래밍 파이썬 중급 (Inflearn Original) >

728x90
반응형

관련글 더보기

댓글 영역