본문 바로가기
Info

파이썬 고급 문법: 클래스, 클로져, 데코레이터, 제너레이터

by 별별 리뷰어 2022. 2. 3.
반응형
Python_Advanced

Python for AI

  • Basics 보다 좀 더 Advanced 한 개념을 주로 정리했음
  • 클래스, 클로져, 제너레이터 등등..

클래스 설계

In [2]:
# 학생 정보를 담는 클래스를 설계
class Student():
    # 생성자
    def __init__(self, name, number, grade, details):
        # 객체 초기화
        self.name = name
        self.number = number
        self.grade = grade
        self.details = details
        
    def __str__(self):
        return 'str 메소드 호출 : {}'.format(self.name)
In [3]:
# 학생을 만들자
# 클래스를 기반으로 생성한 인스턴스
student1 = Student('Kim', 1, 1, {'gender':'Male', 'score':95})
student2 = Student('Park', 2, 3, {'gender':'Female', 'score':90})
student3 = Student('Baek', 3, 5, {'gender':'Male', 'score':100})
In [4]:
# 학생 1번이 어떤 속성을 갖는지 확인하자
student1.__dict__
Out[4]:
{'name': 'Kim',
 'number': 1,
 'grade': 1,
 'details': {'gender': 'Male', 'score': 95}}
In [6]:
# __str__ 메소드 사용하기
# 객체에 어떤게 들었는지 참고할 때 사용하면 좋다
student_list = []
student_list.append(student1)
student_list.append(student2)
student_list.append(student3)

for i in student_list:
    print(i)
str 메소드 호출 : Kim
str 메소드 호출 : Park
str 메소드 호출 : Baek

Closure & Decorator

  • 파이썬이 일급 함수이기 때문에 사용할 수 있음
In [10]:
# 파이썬 변수의 범위
b = 10
def func_v1(a):
    print(a)
    print(b)

func_v1(5)
5
10
In [12]:
b = 10
def func_v2(a):
    print(a)
    print(b)
    b = 5

func_v2(5)

### local variable 'b' referenced before assignment 에러가 뜬다
  • 위의 func_v1 함수는 b가 글로벌 변수이기 때문에 함수에서 print()가 된다
  • 하지만 func_v2 함수에서는 print(b)를 하는데 b가 자신의 지역범위 안에 있는 것만 확인되고 b에 값이 할당되는것은 print 다음이기 때문에 error가 발생한다.
  • 이는 지역변수가 글로벌 변수보다 우선이기 때문이다.

위의 개념을 생각해보면 closure를 이해하기 쉬워짐

  • 반환되는 내부 함수에 대해서 선언된 연결을 가지고 참조하는 방식임
  • 반환 당시, 함수의 유효범위를 벗어난 변수 혹은 메소드에 직접 접근을 하는 것
In [27]:
a = 10
print(a + 10)
print(a + 20)
print(sum(range(1,10)))
20
30
45
  • print문 안이 범위이기 때문에 a를 아무리 더해도 누적이 되지 않는다.
  • 하지만 sum을 사용하면 누적된 합이 나온다.
  • 이것처럼 내가 a를 계속 더하는 것을 (결과를) 누적시킬 수 있을까?
  • 그리고 이것을 클래스 형태로 만들 수 있을까?
In [15]:
# 평균을 만들어주는 클래스를 만들어보자
class Averager():
    def __init__(self):
        self._series = []
        
    def __call__(self, v):
        self._series.append(v)
        print('class >>> {} / {}'.format(self._series, len(self._series)))
        return sum(self._series) / len(self._series)
In [24]:
# 인스턴스 생성
avg_cls = Averager()
In [25]:
avg_cls(10)
avg_cls(20)
avg_cls(30)
class >>> [10] / 1
class >>> [10, 20] / 2
class >>> [10, 20, 30] / 3
Out[25]:
20.0
  • 점점 누적이 되고 있는 것을 볼 수 있다.
  • 클래스 인스턴스 변수 안에 계속 append()하고
  • 이것을 속성값으로 계속 갖고 있기 때문에 호출할 때마다 append하고 나눠준다.

위에서 만든 Averager 클래스를 closure로 만들자

In [28]:
def closure_avg1():
    # 여기가 클로져 영역! (free variable 영역)
    # averager 함수의 유효 범위를 벗어나 있지만 접근이 가능하다
    series = []
    
    def averager(v):
        series.append(v)
        print('class >>> {} / {}'.format(series, len(series)))
        return sum(series) / len(series)
    
    return averager # 함수를 return
In [29]:
avg_closure1 = closure_avg1()
In [30]:
# 함수를 리턴 받았음
avg_closure1
Out[30]:
<function __main__.closure_avg1.<locals>.averager(v)>
In [31]:
avg_closure1(10)
avg_closure1(20)
avg_closure1(30)
class >>> [10] / 1
class >>> [10, 20] / 2
class >>> [10, 20, 30] / 3
Out[31]:
20.0
  • 전역변수의 사용을 감소할 수 있다
  • 디자인 패턴 적용이 가능함
  • 하지만 메모리 사용 측면에서 좋진 않음

데코레이터

  • 이것도 클로져처럼 외부에 있는 것을 내부에서 사용하게 해준다는 개념이긴 함
  • 하지만 클로져보다 간결하고 조합해서 사용하기에 용이하다
In [49]:
# 함수의 실행시간을 측정해보는 함수를 만들어보자
import time

def time_check(func):
    def performance_clocked(*args):
        # 시작 시간
        st = time.perf_counter() # perf_counter는 time 모듈 내장함수 (코드 시간 잼)
        result = func(*args)
        
        # 종료 시간
        et = time.perf_counter() - st
        
        # 함수명
        name = func.__name__
        
        # 매개변수
        arg_str = ','.join(repr(arg) for arg in args)
        
        # 출력
        print('Result: [%0.5fs] %s(%s) -> %r' % (et, name, arg_str, result))
        return result
    
    return performance_clocked
In [45]:
# time_check 함수에 넣은 function들 만들기
# 데코레이트용 함수 만들기
def time_func(seconds):
    time.sleep(seconds)

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

데코레이터 안 쓸 경우

In [46]:
non_decor1 = time_check(time_func)
non_decor2 = time_check(sum_func)
In [47]:
# 안에 뭐가 들었는지 확인해보자
# func 을 갖고 있는것을 알 수 있음
print(non_decor1, non_decor1.__code__.co_freevars)
<function time_check.<locals>.performance_clocked at 0x000001E1F8375D38> ('func',)
In [55]:
# 시간 재보기
print('*' * 30)
non_decor1(2)

# 더하기 함수
print('*' * 30)
non_decor2(10, 20, 30)
******************************
[2.01286s] time_func(2) -> None
******************************
[0.00000s] sum_func(10,20,30) -> 60
Out[55]:
60
  • 나는 time_func을 실행했는데, 선행으로 performance_clocked 함수 안의 부분들이 다 실행되어 나왔음
  • 이런식으로 함수를 실행할 때 원하는 것들을 같이 실행 및 꾸밀 수 있다.

데코레이터를 사용할 경우

  • @ + 사용할 함수 이름 붙이면 끝
In [56]:
@time_check
def time_func(seconds):
    time.sleep(seconds)
    
@time_check
def sum_func(*numbers):
    return sum(numbers)
In [57]:
print('*' * 30)
time_func(2)
print('*' * 30)
sum_func(10, 20, 30)
******************************
Result: [2.00484s] time_func(2) -> None
******************************
Result: [0.00000s] sum_func(10,20,30) -> 60
Out[57]:
60

Generator

  • 호출할 때마다 반환해줌
  • 지능형 리스트, 딕셔너리, 집합의 경우, 데이터 셋이 증가될 경우 메모리 사용량이 증가한다.
  • 이때, 제너레이터가 이것을 완화할 수 있다.
  • 그리고 단위 실행 가능한 coroutine 구현에 사용된다.

클래스를 만들고, 이를 제너레이터로 바꿔보자

  • 문자열을 받으면, split해서 iteration 하는 클래스를 만들어보고
  • 이를 제너레이터로 활용해서 수정하기
In [21]:
class WordSplitIter():
    def __init__(self, text):
        self._idx = 0
        self._text = text.split(' ')
    
    # 다음 순서를 기억하기 위해 idx가 필요함
    def __next__(self): # 호출때 마다 인덱스가 바뀌면서 단어가 나옴
        try:
            word = self._text[self._idx]
        except IndexError:
            raise StopIteration()
        self._idx += 1
        return word
    
    def __iter__(self): # 클래스를 반복할 것임
        print('Called __iter__')
        return self
    
    def __repr__(self): # 객체 호출 때 어떤 정보가 있는지 알아보기
        return 'WordSplitIter(%s)' % (self._text)
In [22]:
wi = WordSplitIter('who says the nights are for sleeping')

# repr 메소드를 부르면서 객체에 어떤 정보가 저장됬는지 확인
wi
Out[22]:
WordSplitIter(['who', 'says', 'the', 'nights', 'are', 'for', 'sleeping'])
In [23]:
# 이제 next를 하면서 단어를 하나씩 부름
print(next(wi))
print(next(wi))
print(next(wi))
who
says
the
  • 다음에 나올 단어를 커서로 가르키고, next가 실행되면 반환된다.
  • 끝까지 next를 실행하면 인덱스가 리스트 범위를 넘어가면서 IndexError가 생김 ### 위의 클래스를 Generator로 바꿔보자
In [28]:
class WordSplitIter():
    def __init__(self, text):
        self._text = text.split(' ')
    
    # generator가 있기 때문에 따로 idx를 만들 필요가 없음. (내부적으로 기억함)
    # 아까 만든 __next__()도 필요가 없음. 그리고 IndexError도 알아서 나옴
    
    def __iter__(self):
        # 여기가 바로 제너레이터!!
        for word in self._text:
            yield word # 여기서 알아서 다 처리함
        return
    
    def __repr__(self): # 객체 호출 때 어떤 정보가 있는지 알아보기
        return 'WordSplitIter(%s)' % (self._text)
In [29]:
wi = WordSplitIter('who says the nights are for sleeping')
# repr 메소드를 부르면서 객체에 어떤 정보가 저장됬는지 확인
wi
Out[29]:
WordSplitIter(['who', 'says', 'the', 'nights', 'are', 'for', 'sleeping'])
In [32]:
# iter 함수를 호출해서 iteratable하게 바꿔줘야 함
wg = iter(wi)

# 이제 next를 하면서 단어를 하나씩 부름
print(next(wg))
print(next(wg))
print(next(wg))
who
says
the
  • yield 예약어 때문에 쉽게 구현이 가능해졌다.
In [79]:
def generator_ex1():
    print('start')
    yield 'AAA'
    print('continue')
    yield 'BBB'
    print('end')
    
temp = iter(generator_ex1())
In [71]:
# 순서대로 나옴
print(next(temp))
print(next(temp))
# print(next(temp))
start
AAA
continue
BBB
In [74]:
# 반복문에서 주로 사용함
for i in generator_ex1():
    print(i)
start
AAA
continue
BBB
end

괄호를 사용하면 지능형 제너레이터를 만든다

In [80]:
temp2 = [x * 3 for x in generator_ex1()]
temp3 = (x * 3 for x in generator_ex1())
start
continue
end
In [82]:
print(temp2)
print(temp3)
['AAAAAAAAA', 'BBBBBBBBB']
<generator object <genexpr> at 0x000001E1F8389448>
반응형

댓글