#!/usr/bin/env python # coding: utf-8 # *** # *** # # 22. 약한참조, 반복자, 발생자 # *** # *** # *** # ## 1 약한 참조 # *** # ### 1-1 약한 참조의 정의 # - 약한 참조 (Weak Reference) # - 레퍼런스 카운트로 고려되지 않는 참조 # ### 1-2 약한 참조의 필요성 # ![inheritance](../images/cyclingref.png) # - 1) 레퍼런스 카운트가 증가되지 않으므로 순환 참조가 방지된다. # - 순환 참조 (Cyclic Reference) # - 서로 다른 객체들 사이에 참조 방식이 순환 형태로 연결되는 방식 # - 독립적으로 존재하지만 순환 참조되는 서로 다른 객체 그룹은 쓰레기 수집이 안된다. # - 주기적으로 순환 참조를 조사하여 쓰레기 수집하는 기능이 있지만, CPU 자원 낭비가 심하다. # - 이러한 쓰레기 수집 빈도가 낮으면 순환 참조되는 많은 객체들이 메모리를 쓸데없이 점유하게 됨 # - 2) 다양한 인스턴스들 사이에서 공유되는 객체에 대한 일종의 케시(Cache)를 만드는 데 활용된다. # ### 1-3 약한 참조 모듈 # #### 1) weakref.ref(o) # - weakref 모듈의 ref(o) 함수 # - 객체 o에 대한 약한 참조를 생성한다. # - 해당 객체가 메모리에 정상적으로 남아 있는지 조사한다. # - 객체가 메모리에 남아 있지 않으면 None을 반환한다. # # - 약한 참조로 부터 실제 객체를 참조하는 방법 # - 약한 참조 객체에 함수형태 호출 # In[1]: import sys import weakref # weakref 모듈 임포트 class C: pass c = C() # 클래스 C의 인스턴스 생성 c.a = 1 # 인스턴스 c에 테스트용 값 설정 print("refcount -", sys.getrefcount(c)) # 객체 c의 레퍼런스 카운트 조회 print() d = c # 일반적인 레퍼런스 카운트 증가 방법 print("refcount -", sys.getrefcount(c)) # 객체 c의 레퍼런스 카운트 조회 print() r = weakref.ref(c) # 약한 참조 객체 r 생성 print("refcount -", sys.getrefcount(c)) # 객체 c의 레퍼런스 카운트 조회 --> 카운트 불변 print() # In[2]: print(r) # 약한 참조(weakref) 객체 print(r()) # 약한 참조로 부터 실제 객체를 참조하는 방법: 약한 참조 객체에 함수형태로 호출 print(c) print(r().a) # 약한 참조를 이용한 실제 객체 멤버 참조 print() del c # 객체 제거 del d print(r()) # None을 리턴한다 print(r().a) # 속성도 참조할 수 없다 # - 내장 자료형 객체 (리스트, 튜플, 사전 등)에 대해서는 약한 참조를 만들 수 없다. # In[3]: d = {'one': 1, 'two': 2} wd = weakref.ref(d) # #### 2) weakref.proxy(o) # - weakref의 proxy(o)는 객체 o에 대한 약한 참조 프록시를 생성한다. # - 프록시를 이용하면 함수 형식을 사용하지 않아도 실제 객체를 바로 참조할 수 있다. # - ref(o) 함수보다 더 선호되는 함수 # In[4]: import sys import weakref class C: pass c = C() c.a = 2 print("refcount -", sys.getrefcount(c)) # 객체 c의 레퍼런스 카운트 조회 p = weakref.proxy(c) # 프록시 객체를 만든다 print("refcount -", sys.getrefcount(c)) # 객체 c의 레퍼런스 카운트 조회 --> 카운트 불변 print( ) print(p) print(c) print(p.a) # In[5]: import weakref class C: pass c = C() # 참조할 객체 생성 r = weakref.ref(c) # weakref 생성 p = weakref.proxy(c) # weakref 프록시 생성 print(weakref.getweakrefcount(c)) # weakref 개수 조회 print(weakref.getweakrefs(c)) # weakref 목록 조회 #python2.x에서의 결과 # 2 # [, ] # ### 1-4 약한 사전 # - 약한 사전 (Weak Dictionary) # - 사전의 키(key)나 값(value)으로 다른 객체들에 대한 약한 참조를 지니는 사전 # - 주로 다른 객체들에 대한 캐시(Cache)로 활용 # - 일반적인 사전과의 차이점 # - 키(key)나 값(value)으로 사용되는 객체는 약한 참조를 지닌다. # - 실제 객체가 삭제되면 자동적으로 약한 사전에 있는 (키, 값)의 쌍도 삭제된다. # - 즉, 실제 객체가 사라지면 캐시역할을 하는 약한 사전에서도 해당 아이템이 제거되므로 효율적인 객체 소멸 관리가 가능하다. # - weakref 모듈의 WeakValueDictionary 클래스 # - weakref 모듈의 WeakValueDictionary 클래스의 생성자는 약한 사전을 생성한다. # In[8]: import weakref class C: pass c = C() c.a = 4 d = weakref.WeakValueDictionary() # WeakValueDictionary 객체 생성 print(d) d[1] = c # 실제 객체에 대한 약한 참조 아이템 생성 print(list(d.items())) # 사전 내용 확인 print(d[1].a) # 실제 객체의 속성 참조 del c # 실제 객체 삭제 print(list(d.items())) # 약한 사전에 해당 객체 아이템도 제거되어 있음 #python2.x에서의 결과 # # [(1, <__main__.C instance at 0x10dccad40>)] # 4 # [] # - 일반 사전을 통하여 동일한 예제를 수행하면 마지막에 해당 객체를 삭제해도 일반 사전 내에서는 여전히 존재함 # In[9]: class C: pass c = C() c.a = 4 d = {} # 일반 사전 객체 생성 print(d) d[1] = c # 실제 객체에 대한 일반 참조 아이템 생성 print(list(d.items())) # 사전 내용 확인 print(d[1].a) # 실제 객체의 속성 참조 del c # 객체 삭제 (사전에 해당 객체의 레퍼런스가 있으므로 객체는 실제로 메모리 해제되지 않음) print(list(d.items())) # 일반 사전에 해당 객체 아이템이 여전히 남아 있음 # *** # ## 2 반복자 (Iterator) # *** # ### 2-1 반복자 (Iterlator) 객체 # - 반복자 객체 # - 내부적으로 \_\_next\_\_(self)를 지니고 있는 객체 # - 내부적으로 지닌 Sequence 자료를 차례로 반환 # - 더 이상 넘겨줄 자료가 없을 때 StopIteration 예외를 발생시킴 # - next() 내장 함수에 대응됨 # # - 임의의 객체에 대해 반복자 객체를 얻어오는 방법 # - iter(o) 내장 함수 # - 객체 o의 반복자 객체를 반환한다. # - 집합적 객체 A --> iter(A) --> 반복자 객체 B 반환 --> next(B) --> 집합적 자료형 안의 내부 원소를 하나씩 반환 # - 반복자 객체의 메모리 효율성 # - 반복자가 원 객체의 원소들을 복사하여 지니고 있지 않다. # In[1]: L = [1,2,3] # print(next(L)) <-- 에러 발생 I = iter(L) print(I) # Python2.7에서는 반복자 객체에 직접 next() 메소드 호출 # print(I.next()) # print(I.next()) # print(I.next()) # print(I.next()) # Python3에서는 next() 내장 함수 사용 print(next(I)) print(next(I)) print(next(I)) print(next(I)) # In[2]: K = {1: "aaa", 2: "bbb", 3: "ccc"} J = iter(K) print(next(J)) print(next(J)) print(next(J)) # - 리스트 객체에 반복자를 활용한 예 # In[8]: t = iter([1, 2, 3]) while 1: try: x = next(t) except StopIteration: break print(x) # - 리스트 객체에 대해 일반적인 for ~ in 반복 문 사용예 # In[9]: def f(x): print(x + 1) for x in [1,2,3]: f(x) # - for ~ in 구문에 반복자를 활용할 수 있다. # - for 문이 돌때 마다 반복자 객체의 next() 함수가 자동으로 호출되어 순차적으로 각 객체에 접근 가능하다. # - StopIteration이 발생하면 for ~ in 구문이 자동으로 멈춘다. # In[11]: def f(x): print(x + 1) t = iter([1, 2, 3]) for x in t: f(x) # In[12]: def f(x): print(x + 1) for x in iter([1, 2, 3]): f(x) # In[13]: def f(x): print(x + 1) for x in iter((1, 2, 3)): f(x) # ### 2-2 클래스에 반복자 구현하기 # In[20]: class Seq: def __getitem__(self, n): if n == 10: raise IndexError() return n s = Seq() for line in s: print(line) # - 내장 함수 iter()에 대응되는 \_\_iter\_\_(self) 및 next()에 대응되는 \_\_next\_\_(self)의 구현 # - 객체 o에 iter()를 호출하면 자동으로 \_\_iter\_\_(self) 함수 호출 # - \_\_iter\_\_(self) 함수는 \_\_next\_\_(self) 함수를 지닌 반복자 객체를 반환 # - for ~ in 호출시에는 ____next ____() 함수가 ____getitem ____() 보다 우선하여 호출됨 # In[22]: class Seq: def __init__(self, file): self.file = open(file) def __getitem__(self, n): # for ~ in 이 호출될 때에는 __next__() 함수에 의하여 가려짐. if n == 10: raise StopIteration() return n def __iter__(self): return self def __next__(self): line = self.file.readline() # 한 라인을 읽는다. if not line: raise StopIteration # 읽을 수 없으면 예외 발생 return line # 읽은 라인을 리턴한다. s = Seq('readme.txt') # s 인스턴스가 next() 메소드를 지니고 있으므로 s 인스턴스 자체가 반복자임 for line in s: # 우선 __iter__() 메소드를 호출하여 반복자를 얻고, 반복자에 대해서 for ~ in 구문에 의하여 __next__() 메소드가 호출됨 print(line) print() print(Seq('readme.txt')) # list() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. print(list(Seq('readme.txt'))) # tuple() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. print(tuple(Seq('readme.txt'))) # In[23]: class Seq: def __init__(self, fname): self.file = open(fname) #def __getitem__(self, n): # if n == 10: # raise StopIteration() # return n def __iter__(self): return self def __next__(self): line = self.file.readline() # 한 라인을 읽는다. if not line: raise StopIteration() # 읽을 수 없으면 예외 발생 return line # 읽은 라인을 리턴한다. s = Seq('readme.txt') # s 인스턴스가 next() 메소드를 지니고 있으므로 s 인스턴스 자체가 반복자임 for line in s: # 우선 __iter__() 메소드를 호출하여 반복자를 얻고, 반복자에 대해서 for ~ in 구문에 의하여 next() 메소드가 호출됨 print(line), print() print(Seq('readme.txt')) print(list(Seq('readme.txt'))) # list() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. print(tuple(Seq('readme.txt'))) # tuple() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. # In[24]: class Seq: def __init__(self, fname): self.file = open(fname) def __getitem__(self, n): if n == 10: raise StopIteration() return n # def __iter__(self): # return self # def __next__(self): # line = self.file.readline() # 한 라인을 읽는다. # if not line: # raise StopIteration() # 읽을 수 없으면 예외 발생 # return line # 읽은 라인을 리턴한다. s = Seq('readme.txt') # s 인스턴스가 next() 메소드를 지니고 있으므로 s 인스턴스 자체가 반복자임 for line in s: # 우선 __iter__() 메소드를 호출하여 반복자를 얻고, 반복자에 대해서 for ~ in 구문에 의하여 next() 메소드가 호출됨 print(line), print() print(Seq('readme.txt')) print(list(Seq('readme.txt'))) # list() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. print(tuple(Seq('readme.txt'))) # tuple() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 next()를 매번 호출하여 각 원소를 얻어온다. # ### 2-3 사전의 반복자 # - 사전에 대해 for ~ in 구문은 키에 대해 반복한다. # In[25]: d = {'one':1, 'two':2, 'three':3, 'four':4, 'five':5} for key in d: print(key, d[key]) # In[26]: d = {'one':1, 'two':2, 'three':3, 'four':4, 'five':5} for key in iter(d): print(key, d[key]) # - python2.x # - d.iterkeys() 함수 # - 사전 d가 지닌 키에 대한 반복자 객체를 반환한다. # # - python3.x # - iter(d) 또는 iter(d.keys()) 사용 # In[16]: print(type(d.keys())) print(type(iter(d.keys()))) # In[17]: next(d.keys()) # In[18]: next(iter(d.keys())) # In[19]: #python3.x for key in d.keys(): # 키에 대한 반복자, iter(d.keys()) 가 반환한 반복자에 대해 __next__(self) 함수가 순차적으로 불리워짐 print(key, end=" ") print() for key in iter(d.keys()): # 키에 대한 반복자, iter(d.keys()) 가 반환한 반복자에 대해 __next__(self) 함수가 순차적으로 불리워짐 print(key, end=" ") # In[20]: keyset = iter(d) print(next(keyset)) # 반복자 객체는 항상 next() 내장 함수에 값을 반환할 수 있음 (내부적으로 __next__(self) 호출) for key in keyset: # keyset 반복자에 대해 next() 메소드가 순차적으로 호출됨 print(key, end=" ") # In[24]: print(type(d.values())) print(type(iter(d.values()))) # In[27]: #python3.x for key in d.values(): # 키에 대한 반복자, iter(d.keys()) 가 반환한 반복자에 대해 __next__(self) 함수가 순차적으로 불리워짐 print(key, end=" ") print() for key in iter(d.values()): # 키에 대한 반복자, iter(d.keys()) 가 반환한 반복자에 대해 __next__(self) 함수가 순차적으로 불리워짐 print(key, end=" ") # In[31]: print(type(d.items())) print(type(iter(d.items()))) # In[28]: #python3.x for key, value in iter(d.items()): # 키에 대한 반복자, iter(d.keys()) 가 반환한 반복자에 대해 __next__(self) 함수가 순차적으로 불리워짐 print(key, value) # ### 2-4 파일 객체의 반복자 # - 파일 객체는 그 자체가 반복자임 # - next() 함수에 의해 각 라인이 순차적으로 읽혀짐 # In[29]: #python3.x f = open('readme.txt') print("next(f) - ", next(f)) for line in f: # f.next() 가 순차적으로 호출됨 print(line) # *** # ## 3 발생자 # *** # ### 3-1 발생자란? # - 발생자(Generator) # - (중단됨 시점부터) 재실행 가능한 함수 # # - 아래 함수 f()는 자신의 인수 및 내부 로컬 변수로서 a, b, c, d를 지니고 있다. # - 이러한 a, b, c, d 변수들은 함수가 종료되고 반환될 때 모두 사라진다. # - 발생자는 f()와 같이 함수가 (임시로) 종료될 때 내부 로컬 변수가 메모리에서 해제되는 것을 막고 다시 함수가 호출 될 때 이전에 수행이 종료되었던 지점 부터 계속 수행이 가능하도록 구현된 함수이다. # In[22]: def f(a, b): c = a * b d = a + b return c, d x, y = f(1, 2) print(x, y) # - yield 키워드 # - return 대신에 yield에 의해 값을 반환하는 함수는 발생자이다. # - yield는 return과 유사하게 임의의 값을 반환하지만 함수의 실행 상태를 보존하면서 함수를 호출한 쪽으로 복귀시켜준다. # # # - 발생자는 곧 반복자이다. # - 즉, 발생자에게 next() 호출이 가능하다. # In[32]: def f(a, b): c = a * b d = a + b yield c, d g = f(1, 2) x, y = next(g) print(x, y) x, y = next(g) print(x, y) # In[35]: def f(a, b): for _ in range(2): c = a * b d = a + b yield c, d g = f(1, 2) x, y = next(g) print(x, y) x, y = next(g) print(x, y) # In[28]: def generate_ints(N): for i in range(N): yield i # In[29]: gen = generate_ints(3) # 발생자 객체를 얻는다. generate_ints() 함수에 대한 초기 스택 프레임이 만들어지나 실행은 중단되어 있는 상태임 print(gen) # print(gen.next()) # print(gen.next()) # print(gen.next()) # print(gen.next()) print(next(gen)) # 발생자 객체는 반복자 인터페이스를 가진다. 발생자의 실행이 시작됨. yield에 의해 값 반환 후 실행이 중단됨 print(next(gen)) # 발생자 실행 재개. yield에 의해 값 반환 후 다시 중단 print(next(gen)) # 발생자 실행 재개. yield에 의해 값 반환 후 다시 중단 print(next(gen)) # 발생자 실행 재개. yield에 의해 더 이상 반환할 값이 없다면 StopIteration 예외를 던짐 # - 위와 같은 세부 동작 방식을 이용하여, 다음과 같이 for ~ in 구문에 적용할 수 있다. # In[30]: for i in generate_ints(5): print(i, end=" ") # - 발생자 함수와 일반 함수의 차이점 # - 일반 함수는 함수가 호출되면 그 함수 내부에 정의된 모든 일을 마치고 결과를 반환함 # - 발생자 함수는 함수 내에서 수행 중에 중간 결과 값을 반환할 수 있음 # # # - 발생자가 유용하게 사용되는 경우 # - 함수 처리의 중간 결과를 다른 코드에서 참조할 경우 # - 즉, 모든 결과를 한꺼번에 반환 받는 것이 아니라 함수 처리 중에 나온 중간 결과를 받아서 사용해야 할 경우 # - 시퀀스 자료형을 효율적으로 만들고자 하는 경우 # ### 3-2 발생자 구문 # - 리스트 내포(List Comprehension) # - 리스트 객체의 새로운 생성 # - 메모리를 실제로 점유하면서 생성됨 # In[31]: a = [k for k in range(100) if k % 5 == 0] print(a) type(a) # - 리스트 내포 구문에 []가 아니라 () 사용 # - 리스트 대신에 발생자 생성 # - 즉, 처음부터 모든 원소가 생성되지 않고 필요한 시점에 각 원소가 만들어짐 # - 메모리를 보다 효율적으로 사용할 수 있음 # In[34]: a = (k for k in range(100) if k % 5 == 0) print(a) type(a) # In[35]: # print(a.next()) # print(a.next()) # print(a.next()) print(next(a)) print(next(a)) print(next(a)) for i in a: print(i, end=" ") # - 아래 예는 sum 내장 함수에 발생자를 넣어줌 # - sum을 호출하는 시점에는 발생자가 아직 호출되기 직전이므로 각 원소들은 아직 존재하지 않는다. # - sum 내부에서 발생자가 지니고 있는 next() 함수를 호출하여 각 원소들을 직접 만들어 활용한다. # - 메모시 사용 효율이 높다. # In[36]: a = [1, 2, 3] print(sum(a)) # In[37]: a = (k for k in range(100) if k % 5 == 0) print(sum(a)) # ### 3-3 발생자의 활용 예 1 - 피보나치 수열 # In[38]: def fibonacci(a = 1, b = 1): while 1: yield a a, b = b, a + b for k in fibonacci(): # 발생자를 직접 for ~ in 구문에 활용 if k > 100: break print(k, end=" ") # ### 3-4 발생자의 활용 예 2 - 홀수 집합 만들기 # - 반복자를 활용한 예 # In[40]: #python3.x class Odds: def __init__(self, limit = None): # 생성자 정의 self.data = -1 # 초기 값 self.limit = limit # 한계 값 def __iter__(self): # Odds 객체의 반복자를 반환하는 특수 함수 return self def __next__(self): # 반복자의 필수 함수 self.data += 2 if self.limit and self.limit <= self.data: raise StopIteration() return self.data for k in Odds(20): print(k, end=" ") print() print(list(Odds(20))) # list() 내장 함수가 객체를 인수로 받으면 해당 객체의 반복자를 얻어와 __next__(self)를 매번 호출하여 각 원소를 얻어온다. # - 발생자를 활용한 가장 좋은 예 # In[41]: def odds(limit=None): k = 1 while not limit or limit >= k: yield k k += 2 for k in odds(20): print(k, end=" ") print() print(list(odds(20))) # list() 내장 함수가 발생자를 인수로 받으면 해당 발생자의 next()를 매번 호출하여 각 원소를 얻어온다. #

참고 문헌: 파이썬(열혈강의)(개정판 VER.2), 이강성, FreeLec, 2005년 8월 29일