본문 바로가기
코딩 연습/코딩배우기

[Python] 파이썬 기본기 UP_2 - 참조변수, 컴프리헨션(comprehension), 반복자(iterator), 컨텍스트 매니저(context manager), 제너레이터(generator)와 yield

by good4me 2021. 7. 22.

goodthings4me.tistory.com

 

 

파이썬 기본기 UP 2 - 취미 삼아 배우는 파이썬의 코딩 스킬 업을 위해 파이썬 중급(또는 고급) 내용을 나름대로 찾아서 정리하는 중이며, 이번 포스팅은 참조변수, 리스트 컴프리헨션, 이터레이터, 컨텍스트관리자, 제너레이터 등의 내용을 강의하는 자료가 있어서 스터디 후 나에게 필요한 부분만 정리해보았다. (나중에 필요 시 참고하기 위한 자료임) 

 

정리한 주요 내용은 다음과 같다.

  1. 자료형과 참조변수
  2. 리스트 컴프리헨션
  3. 반복 가능 객체(iterator)
  4. with 문과 컨텍스트 매니저(context manager)
  5. 제너레이터(generator)와 yield

 

■ 자료형과 참조변수

  • 파이썬의 모든 것은 객체이다.
  • 파이썬은 객체지향 프로그래밍 언어이다.
  • C 언어는 변수가 생성되고 변수에 값이 저장되는 구조(변수 중심)인 반면, 파이썬은 객체가 중심이 되며, 객체를 참조하는 참조 변수를 통해 객체에 접근할 수 있다.
  • 파이썬의 변수에는 동적으로 참조하는 객체가 저장된다.
  • a=100 시, 100이라는 정수형 객체가 먼저 생성되고, 객체에 대한 참조변수 a가 100 객체를 참조한다.
  • a=200 하면, 200이라는 정수형 객체가 생성되고, 참조 변수가 객체 200을 참조한다.(객체 재할당)
  • 파이썬의 모든 객체는 고유한 id를 가진다.(객체 구분)
  • '=' 연산자는 객체에 대한 참조를 만들거나 변경시킨다.
  • 객체를 여러 변수가 동시에 참조할 수 있다.
a = 100
print(id(100))  # 1859736327632 --> 100을 가지는 정수형 객체의 id
print(id(a))  # 1859736327632  --> a가 참조하는 객체의 id를 반환

b = a
print(id(b))  # 2000605828560

a = 300
print(id(a))  # 2217609689840 --> a는 새로운 객체를 참조

 

■ 리스트 컴프리헨션 (List Comprehension)

# 반복 가능 객체로 리스트 생성할 수 있는 기능
a = [1, 2, 3, 4, 5, 6, 7, 8]

lst = []
for i in a:
    lst.append(i**2)
print(lst)  # [1, 4, 9, 16, 25, 36, 49, 64]

ml = list(map(lambda x: x**2, a))
print(ml)  # [1, 4, 9, 16, 25, 36, 49, 64]

ch = [i**2 for i in a]
print(ch)  # [1, 4, 9, 16, 25, 36, 49, 64]


# list(), filter() lambda 함수 이용한 필터링
ages = [24, 39, 18, 40, 13, 54]

adult_ages = list(filter(lambda x: x >= 19, ages))
print(adult_ages)  # [24, 39, 40, 54]

ad_ages = [x for x in ages if x >= 19]
print(ad_ages)  # [24, 39, 40, 54]


# 이중 for 문을 리스트 컴프리헨션으로 구현
xy = []
for x in [1, 2, 3]:
    for y in [2, 4, 6]:
        xy.append(x * y)
print(xy)  # [2, 4, 6, 4, 8, 12, 6, 12, 18]

c_xy = [x * y for x in [1, 2, 3] for y in [2, 4, 6]]
print(c_xy)  # [2, 4, 6, 4, 8, 12, 6, 12, 18]


# 2와 3의 배수 구하기
cmulti = []
for n in range(1, 31):
    if n % 2 == 0 and n % 3 == 0:
        cmulti.append(n)
print(cmulti)  # [6, 12, 18, 24, 30]

print([n for n in range(1, 31) if n % 2 == 0 and n % 3 == 0])  # [6, 12, 18, 24, 30]
print([n for n in range(1, 31) if n % 2 == 0 if n % 3 == 0])  # [6, 12, 18, 24, 30]

 

■ 반복 가능 객체(iterable 객체)와 반복자 객체(iterator 객체)

반복 가능 자료형인 리스트, 튜플, 문자열, 딕셔너리, 집합, 파일, range 등에 내장함수 iter()를 이용해 반복자 객체(iterator)로 만들고, next(객체명) 또는 객체명.__next__()로 하나씩 꺼낸다.

즉, 반복 가능 객체(iterable 객체 - list, tuple, range 등)에 대해 iter() 함수를 적용[iter(iterable 객체)]하여 iterator(데이터를 순차적으로 꺼내 이용할 수 있는 객체)를 생성하고 next() 함수(또는 __next()__)로 값을 추출(next() 반복)한다. 이때, 값이 없으면 StopIteration 예외 발생함

l = [10, 20, 30]
#print(l.next())  # AttributeError: 'list' object has no attribute 'next'
l_iter = iter(l)
print(type(l_iter), l_iter)  
# <class 'list_iterator'> <list_iterator object at 0x0000019DBF8EDCA0>
print(next(l_iter))  # 10
print(next(l_iter))  # 20
print(l_iter.__next__())  # 30
#print(l_iter.__next__())  # StopIteration


# 리스트 객체에는 __iter__ 메서드가 있다. 이 __iter__ 메서드를 호출하면 iterator가 나온다
print('__iter__' in dir(l))  # True
print(l.__iter__)  
# <method-wrapper '__iter__' of list object at 0x0000018745879F40>
print(l.__iter__())  
# <list_iterator object at 0x0000026A40B43F70> 
l_it = l.__iter__()
print(next(l_it))  # 10
print(next(l_it))  # 20
print(next(l_it))  # 30


# 정수형 객체는 반복자 객체로 변환 불가
n = 100
#n_iter = iter(n)  # TypeError: 'int' object is not iterable


# range() 함수는 range 형 객체를 만든다. range 형 객체를 iter() 함수로 iterator로 변환해보자
print(type(range(3)), range(3))  # <class 'range'> range(0, 3)

r_iter = iter(range(3))
print(type(r_iter), r_iter)  
# <class 'range_iterator'> <range_iterator object at 0x000002CB6FB4E550>
print(next(r_iter))  # 0
print(next(r_iter))  # 1
print(next(r_iter))  # 2
#print(next(r_iter))  # StopIteration
# 주어진 인자값을 기준으로 시작하고 증가하는 반복자 개체 만들기
class OddCounter:
    ''' 주어진 n부터 m씩 증가하는 값을 반환하는 클래스 '''
    def __init__(self, n, m):
        self.n = n
        self.m = m

    def __iter__(self):
        return self

    def __next__(self):
        t = self.n
        self.n += self.m
        return t

my_count = OddCounter(5, 7)
print(next(my_count))  # 5
print(my_count.__next__())  # 12
print(my_count.__next__())  # 19
print(my_count.__next__())  # 26

good4me.co.kr

# my coding practice)
class MakeListIterator:
    def __init__(self, seq_obj):
        self.idx = 0
        self.seq_obj = seq_obj
        self.new_iter = ''
    
    def __iter__(self):
        self.new_iter = 'y'
        return self
    
    def __next__(self):
        if not self.new_iter:
            print('iterator 객체 생성을 먼저 해주세요!')
            return
        if self.idx >= len(self.seq_obj):
            raise StopIteration
        lst_n = self.seq_obj[self.idx]
        self.idx += 1
        return lst_n


lst = MakeListIterator([20, 18, 30])
print(lst.__iter__())  # <__main__.MakeListIterator object at 0x000001CD3A6489A0>
print(lst.new_iter)  # y
print(lst.__next__())  # 20
print(lst.__next__())  # 18
print(lst.__next__())  # 30
#print(lst.__next__())  # StopIteration

 

▷ 반복 가능한 객체를 위한 내장함수 - min(), max(), all(), any(), ascii(), bool(), filter(), iter() 등

# all() any() 함수
# - all() 반복 가능한 항목들이 모두 참일 때만 참을 반환함
# - any() 반복 가능한 항목들 중에서 참이 하나라도 있을 때 참을 반환함

l1 = [1, 2, 3, 4]
l2 = [2, 5, 0, 9]
l3 = [1, '0', 2, 3]
l4 = [1, [], 2, 3]
l5 = [[], 0, {}, '']

print(all(l1), all(l2), all(l3), all(l4))  # True False True False
print(any(l1), any(l2), any(l3), any(l4), any(l5))  # True True True True False

☞ 함수형 프로그래밍 모듈 
# itertools - 반복자(iterator) 생성 모듈
# functools - 다른 함수를 반환할 수 있는 모듈, reduce() 등
# operator - 표준 연산자를 함수 형태로 다룰 수 있게 하는 모듈

 

■ with 문과 컨텍스트 매니저(관리자)

'컨텍스트 매니저'란, with문에서 사용하도록 설계된 객체를 말하는데, with 구문 body의 앞부분과 뒷부분 실행 코드를 대신할 수 있다. with 구문 사용 시, open()이 간단해지고, finally 절도 필요없어짐

with expression as thing:
    body

컨텍스트 매니저(context manager),

  1. with 구문의 expression 호출(실행) --> context manager가 자동적으로 __enter__() 실행 후 반환값(결과값)을 as의 변수 thing에 지정하고,
  2. 그 후 thing를 사용하는 body문 실행 후 
  3. with문을 벗어날 시, context manager는 __exit__()를 자동 실행시킴
    (예외적인 상황이 생겨도 __exit__() 메소드는 호출이 보장됨)

즉, __enter__()는 body문에 들어가기 전에 호출되고 with 문 탈출 시 __exit__() 메소드 호출함

  • 파이썬의 file 객체에는 __enter__(), __exit__() 메소드가 구현되어있다.
  • 이 객체는 file object 자신을 반환한다. __exit__() 메소드는 당연히 파일을 close 한다.

 

try:
    f = open('foo.txt', 'r')
except FileNotFoundError as e:
    print(str(e))
else:  # try error 없을 때 
    data = f.read()
    f.close()
    

try:
    f = open('file.txt', 'w')
    try:
        f.write('finally 절은 try문 수행도중 발생하는 예외에 관계없이 항상 수행된다.')
    finally:
        f.close()
except IOError:
    print('oops!')

위 코드를 아래처럼 사용

try:
    with open('file.txt', 'w') as f:
        f.write('위 try 문을 이렇게 쓸 수 있다.')
except IOError:
    print('oops!')

 

■ 제너레이터(generator)와 yield

제너레이터 객체는 모든 값을 메모리에 올려두고 이용하는 것이 아니라 필요 시 생성하고 반환하는 일을 한다.

my_gen = (x for x in range(1, 4))  # [] 대신 () 사용 시 제너레이터 표현식임
print(type(my_gen), my_gen)  
# <class 'generator'> <generator object <genexpr> at 0x000001FC4888BD60>

for n in my_gen:
    print(n, end=' ')  # 1 2 3
  • 위 코드를 보면 반복자(iterator)와 동일한 일을 하는 것처럼 보이지만, 
  • 여기서 생성된 '1 2 3 '을 미리 메모리에 만들어 두는 것이 아니라 for 문에서 필요로 할 때 마다 my_gen으로부터
  • 받아오는 것이며, 메모리에 보관하지 않는다는 점이 반복자와 다르다.(이를 lazy evaluation이라 함)

yield 문은 return 문과 유사하지만, 제너레이터를 반환한다는 점에서 차이가 있다.

def create_xgen():
    lst = range(1, 4)
    for x in lst:
        print(f'x: {x}')
        return x

r = create_xgen()
print(r)  # x: 1 1


def create_gen():  # yield 문을 가진 제너레이터
    lst = range(1, 4)
    for y in lst:
        print(f'y: {y}')
        yield y

y = create_gen()
print(y)  
# <generator object create_gen at 0x0000021A8CC81EB0>

for n in y:  # for 문에 의해 호출될 때마다 객체 반환
    print(n)
# y: 1
# 1
# y: 2
# 2
# y: 3
# 3

# 다시 실행 시, 제너레이터는 한 번 실행하면 아무것도 반환하지 않음
for n in y:
    print(n, end=' ')  # 아무것도 출력 안됨

 

## 일반 함수와 제너레이터 사용한 루프의 처리시간 비교
# - 일반함수는 피보나치 수열을 생성하여 리스트에 넣고 for문을 통해 출력
# - 제너레이터는 피보나치 수열을 계산할 때마다 for문에 넘겨준다

import time

def fibon(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result


start_t = time.time()
for x in fibon(100000):
    pass
end_t = time.time()
print(f'수행 시간 : {end_t - start_t}')  
# 수행 시간 : 0.3949697017669678


def fibon_y(n):
    start_t = time.time()
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b

start_yt = time.time()
for x in fibon_y(100000):
    pass
end_yt = time.time()
print(f'수행 시간 : {end_yt - start_yt}')  
# 수행 시간 : 0.1046915054321289

 

■ docstring

## - docstring
def square(a):
    '''입력값 a의 제곱을 반환하는 함수 '''
    return a**2

print(square.__doc__)  # 입력값 a의 제곱을 반환하는 함수

print(help(square))
# Help on function square in module __main__:        
# square(a)
#     입력값 a의 제곱을 반환하는 함수
# None

 

[출처] 널날한 교수의 고급 파이썬

 

댓글