14wk-2: 클래스공부 4단계 – 파이썬의 비밀 (3)

Author

최규빈

Published

June 7, 2023

강의영상

youtube: https://youtube.com/playlist?list=PLQqh36zP38-yZWViSW4CktNpOM7Km4BAe

imports

import numpy as np
import pandas as pd

클래스공부 4단계: 파이썬의 비밀 (3)

비밀12: __iter__ (\(\star\star\star\star\star\))

for문의 복습

- 아래와 같은 예제들을 관찰하여 for문을 복습하자.

(예제1) [1,2,3,4]

for i in [1,2,33,4]:
    print(i)
1
2
33
4

(예제2) (1,2,3,4)

for i in (1,2,33,4):
    print(i)
1
2
33
4

(예제3) ‘1234’

for i in '123asdf':
    print(i)
1
2
3
a
s
d
f

(예제4) 5

for i in 5:
    print(i)
TypeError: 'int' object is not iterable

- 의문1:

for i in ???:
    print(i)

에서 ??? 자리에 올수 있는 것이 무엇일까?

(예제5) [[1,2,3,4],[3,4,5,6]], pd.DataFrame([[1,2,3,4],[3,4,5,6]])

lst = [[1,2,3,4],[3,4,5,6]]
lst
[[1, 2, 3, 4], [3, 4, 5, 6]]
for l in lst:
    print(l)
[1, 2, 3, 4]
[3, 4, 5, 6]
for l in np.array(lst):
    print(l)
[1 2 3 4]
[3 4 5 6]
for l in pd.DataFrame(lst):
    print(l)
0
1
2
3

- 데이터프레임인 경우는 colname이 반복

df = pd.DataFrame({'x':[1,2,3],'y':[2,3,4]})
df
x y
0 1 2
1 2 3
2 3 4
for d in df:
    print(d)
x
y

- 의문2: for의 출력결과는 어떻게 예측할 수 있을까?

for문의 동작원리

- 의문1의 해결: 아래의 ??? 자리에 올 수 있는 것은 dir()하여 __iter__가 있는 object이다.

for i in ???:
    print(i)

이러한 오브젝트를 iterable object라고 한다.

- 예제1~4 확인

lst = [1,2,3]
set(dir(lst)) & {'__iter__'}
{'__iter__'}
tpl = 1,2,3
set(dir(tpl)) & {'__iter__'}
{'__iter__'}
string = '123'
set(dir(string)) & {'__iter__'}
{'__iter__'}
a = 5
set(dir(a)) & {'__iter__'}
set()

- __iter__의 역할: iterable object를 iterator로 만들 수 있다!

lst = [1,22,-33]
lst_iterator = lst.__iter__() 
# lst_iterator = iter(lst)

- iterator가 되면 무엇이 좋은가? -> 숨겨진 기능 __next__가 열린다.

set(dir(lst_iterator)) & {'__next__'}
{'__next__'}

- 그래서 __next__의 기능은? -> 원소를 차례대로 꺼내준다 + 더 이상 꺼낼 원소가 없으면 StopIteration Error를 발생시킨다.

next(lst_iterator)
# lst_iterator.__next__() # 같은코드
1
next(lst_iterator)
# lst_iterator.__next__() # 같은코드
22
next(lst_iterator)
# lst_iterator.__next__() # 같은코드
-33
next(lst_iterator)
# lst_iterator.__next__() # 같은코드
StopIteration: 

- for문의 동작원리

for i in iterable:
    ...
  1. 이터레이터생성: .__iter__() 혹은 iter()을 이용하여 iterable을 iterator로 만든다.

  2. \(i\)생성 및 반복: 1에서 만들어진 iterator에서 .__next__()함수를 호출하고 결과를 \(i\)에 저장한뒤 for문 블락안의 내용 (들여쓰기 된 내용) 을 실행한다. \(\to\) 반복한다.

  3. 정지: .__next__()함수를 호출할때 StopIteration Error가 나오면 for문을 멈춘다.

flowchart LR
  A[iterable] --> |"__iter__()"| B(iterator)
  B --> |"__next__()"| C{stop?}
  C --> |NO| D[i] --> E[...] --> B
  C --> |YES| F[end]

- 아래의 구조도 잘 돌아갈까?

for i in iterator:
    print(i)

iterator의 iter가 자기자신을 리턴하도록 하는 트릭을 쓰면 “1.이터레이터생성 2.\(i\)생성 및 반복 3.정지”의 동작원리를 수정하지 않고 for문을 안전하게 돌릴 수 있다.

flowchart LR
  A(iterator) --> |"__iter__()"| B(iterator)
  B --> |"__next__()"| C{stop?}
  C --> |NO| D[i] --> E[...] --> B
  C --> |YES| F[end]

- 요약

  • iterable object는 숨겨진 기능으로 __iter__를 가진다.
  • iterator는 숨겨진 기능으로 __iter____next__를 가진다. 따라서 정의상 iterator는 그 자체로 iterable object가 된다!
  • iterator의 __iter__는 자기자신을 리턴한다.

- 의문2의 해결: for의 출력결과는 어떻게 예측할 수 있을까? iterator를 만들어서 .__next__()의 출력값을 확인하면 알 수 있다.

_df_itertor = iter(df)
next(_df_itertor)
'x'
next(_df_itertor)
'y'
next(_df_itertor)
StopIteration: 

사용자정의 이터레이터

- 내가 이터레이터를 만들어보자.

- 찌를 내는 순간 for문이 멈추도록 하는 이터레이터를 만들자.

class RPS_ITERATOR: # 찌를 내는순간 for문이 멈추도록 하는 이터레이터를 만들자
    def __init__(self): 
        self.candidate = ["묵","찌","빠"] 
    def __iter__(self):
        return self 
    def __next__(self):
        action = np.random.choice(self.candidate)
        if action == "찌":
            print("찌가 나와서 for문을 멈춥니다")
            raise StopIteration
        else:
            return action
a = RPS_ITERATOR()
a.__next__()
'빠'
a.__next__()
'묵'
a.__next__()
'묵'
a.__next__()
찌가 나와서 for문을 멈춥니다
StopIteration: 
for i in a:
    print(i)
빠
묵
묵
빠
빠
찌가 나와서 for문을 멈춥니다

range()

- 파이썬에서 for문을 처음 배울 때: range(5)를 써라!

for i in range(5):
    print(i)
0
1
2
3
4
  • range(5)가 도데체 무엇이길래?

- range(5)의 정체는 그냥 iterable object이다.

set(dir(range(5))) & {'__iter__','__next__'}
{'__iter__'}

- 그래서 언제든지 iterator로 바꿀 수 있다.

rtor= iter(range(5))
rtor
<range_iterator at 0x7f9a8e84f5d0>
set(dir(rtor)) &  {'__iter__','__next__'}
{'__iter__', '__next__'}

- for문에서 range(5)가 행동하는 방법?

rtor = iter(range(5))
rtor.__next__()
0
rtor.__next__()
1
rtor.__next__()
2
rtor.__next__()
3
rtor.__next__()
4
rtor.__next__()
StopIteration: 

zip

- 이터레이터의 개념을 알면 for문에 대한 이해도가 대폭 상승한다.

for i,j in zip([1,2,3],'abc'):
    print(i,j)
1 a
2 b
3 c
  • zip은 뭐지?
zip([1,2,3],'abc')
<zip at 0x7f9a8e7dd5c0>

- 어차피 for i in ????: 의 ???? 자리는 iterable object의 자리이다.

set(dir(zip([1,2,3],'abc'))) & {'__iter__','__next__'}
{'__iter__', '__next__'}
  • __next__()함수가 있음 \(\to\) zip([1,2,3],'abc')은 그자체로 iterator 였다!
z= zip([1,2,3],'abc')
z.__next__()
(1, 'a')
z.__next__()
(2, 'b')
z.__next__()
(3, 'c')
z.__next__()
StopIteration: 

또다른 이해: 그러고보니까 zip([1,2,3],'abc')은 뭐하는 문법이지?

zip?
Init signature: zip(self, /, *args, **kwargs)
Docstring:     
zip(*iterables) --> A zip object yielding tuples until an input is exhausted.
   >>> list(zip('abcdefg', range(3), range(4)))
   [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
The zip object yields n-length tuples, where n is the number of iterables
passed as positional arguments to zip().  The i-th element in every tuple
comes from the i-th iterable argument to zip().  This continues until the
shortest argument is exhausted.
Type:           type
Subclasses:     
  • 너 클래스였어?
iterator_from_zip = zip.__call__([1,2,3],'abc')
for i,j in iterator_from_zip:
    print(i,j)
1 a
2 b
3 c

파이썬의 작은 비밀: zip은 iterator를 찍어내는 클래스이다.

enumerate

- zip의 짝궁으로 enumerate가 있음

for i,s in enumerate('abc'):
    print(i,s)
0 a
1 b
2 c

- enumerate('abc')도 문법상 iterable object 아니면 iterator 임.

set(dir(enumerate('abc'))) & {'__iter__', '__next__'}
{'__iter__', '__next__'}
  • iterator 였군
iterator = enumerate('abc')
next(iterator)
(0, 'a')
next(iterator)
(1, 'b')
next(iterator)
(2, 'c')
next(iterator)
StopIteration: 

- 참고: enumerate 는 클래스임

enumerate?
Init signature: enumerate(iterable, start=0)
Docstring:     
Return an enumerate object.
  iterable
    an object supporting iteration
The enumerate object yields pairs containing a count (from start, which
defaults to zero) and a value yielded by the iterable argument.
enumerate is useful for obtaining an indexed list:
    (0, seq[0]), (1, seq[1]), (2, seq[2]), ...
Type:           type
Subclasses:     

파이썬의 작은 비밀: enumerate 역시 iterator를 찍어내는 클래스다.

파이썬의 비밀12: iterator나 iterable object만 for문과 함께 사용할 수 있다.

비밀을 알아서 좋은점

파이썬의 에러메시지 이해

TypeError: 타입이 맞지 않는 연산을 수행하려고 할 때 발생. 예를 들어, 숫자와 문자열을 더하려고 할 때 발생.

result = 10 + "20"  # TypeError: unsupported operand type(s) for +: 'int' and 'str'
TypeError: unsupported operand type(s) for +: 'int' and 'str'

SyntaxError: 코드의 구문이 잘못되었을 때 발생. 주로 오타, 괄호 불일치, 콜론(:) 빠뜨림 등의 문제로 인해 발생.

if x == 5
    print("x is equal to 5")  # SyntaxError: invalid syntax
SyntaxError: invalid syntax (<ipython-input-23-5259b3c47c58>, line 1)

NameError: 정의되지 않은 변수나 함수를 사용하려고 할 때 발생.

print(unknown_variable)  # NameError: name 'unknown_variable' is not defined
NameError: name 'unknown_variable' is not defined

ValueError: 함수나 메서드에 전달되는 인자의 값이 올바르지 않을 때 발생.

int_value = int("abc")  # ValueError: invalid literal for int() with base 10: 'abc'
ValueError: invalid literal for int() with base 10: 'abc'

AttributeError: 객체가 속성(attribute)이나 메서드(method)를 가지고 있지 않을 때 발생

my_list = [1, 2, 3]
my_list.append(4)
my_list.upper()  # AttributeError: 'list' object has no attribute 'upper'
AttributeError: 'list' object has no attribute 'upper'

ImportError: 모듈을 임포트하는 과정에서 문제가 발생할 때 발생. 임포트하려는 모듈이 존재하지 않거나, 임포트 경로 설정이 잘못된 경우에 해당.

import non_existent_module  # ImportError: No module named 'non_existent_module'
ModuleNotFoundError: No module named 'non_existent_module'

IOError: 파일 입출력 작업 중에 발생하는 에러. 파일이 존재하지 않거나, 파일에 쓰기 권한이 없는 경우 등에 해당.

file = open('file.txt', 'r')  # IOError: [Errno 2] No such file or directory: 'file.txt'
FileNotFoundError: [Errno 2] No such file or directory: 'file.txt'

KeyboardInterrupt: 사용자가 프로그램의 실행을 중단할 때 발생.

while True:
    pass  # 무한 루프
# KeyboardInterrupt: 
KeyboardInterrupt: 

IndexError: 유효하지 않은 인덱스를 사용하여 시퀀스(리스트, 튜플, 문자열 등)의 요소에 접근하려고 할 때 발생.

my_list = [1, 2, 3]
print(my_list[5])  # IndexError: list index out of range
IndexError: list index out of range

KeyError: 딕셔너리에서 존재하지 않는 키를 사용하여 요소에 접근하려고 할 때 발생.

my_dict = {'name': 'John', 'age': 25}
print(my_dict['address'])  # KeyError: 'address'
KeyError: 'address'

FileNotFoundError: 존재하지 않는 파일을 열려고 할 때 발생.

file = open('nonexistent_file.txt')  # FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent_file.txt'
FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent_file.txt'

ZeroDivisionError: 0으로 나누기 연산을 수행하려고 할 때 발생.

result = 10 / 0  # ZeroDivisionError: division by zero
ZeroDivisionError: division by zero

TypeError에 대한 심층분석

- .__add__ 가 올바르게 정의되지 않아서 생기는 경우

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[37], line 1
----> 1 boram + '등록'+ '휴학' + '등록' + '휴학'

TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

- .__getitem__이 정의되지 않은 오브젝트에서 인덱싱을 시도할때 생기는 오류

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[57], line 1
----> 1 a[0], a[1]

TypeError: 'RPS' object is not subscriptable

- .__setitem__이 정의되지 않은 오브젝트에서 할당을 시도할때 생기는 오류

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[77], line 1
----> 1 a[0] = '보' 

TypeError: 'RPS_Ver2' object does not support item assignment

- .__len__ 이 정의되지 않은 오브젝트에서 len을 이용할때 생기는 오류

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[122], line 1
----> 1 len(a)

TypeError: object of type 'RPS_Ver3' has no len()

- .__call__이 정의되지 않은 오브젝트를 함수처럼 이용하려 할때 생기는 오류

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[402], line 1
----> 1 a()

TypeError: 'Klass' object is not callable

- .__iter__가 정의되지 않은 오브젝트를 iterable 오브젝트처럼 사용하려 할때 생기는 오류

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[422], line 2
      1 a=5 
----> 2 for i in a:
      3     print(i) 

TypeError: 'int' object is not iterable

숙제

(1) 아래의 코드를 관찰하라.

iterator = enumerate('abcf',1)
next(iterator)
(1, 'a')
next(iterator)
(2, 'b')
next(iterator)
(3, 'c')
next(iterator)
(4, 'f')
next(iterator)
StopIteration: 

이를 바탕으로 enumerate('abcf',1)의 기능을 유추해볼 것.

(2) enumrate를 이용하여 아래의 코드를 개선하여라.

lst = ['아이언맨','토르','헐크','블랙위도우','로키']
for i in range(len(lst)):
    print('{}: {}'.format(i+1,lst[i]))
1: 아이언맨
2: 토르
3: 헐크
4: 블랙위도우
5: 로키

(3) 앞면과 뒷면이 나올 확률이 각각 1/2인 동전을 생각하자. 하니와 규빈은 이 동전을 연속으로 던져서 아래와 같은 룰을 정하여 내기를 하였다.

  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (앞면,앞면) 이 나오면 하니의 승리
  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (뒷면,뒷면) 이 나오면 규빈의 승리

예를들어 동전을 반복하여 던져 아래와 같이 나온다면 하니의 승리이다.

  • (앞면, 뒷면, 앞면, 뒷면, 앞면, 앞면)
  • (앞면, 뒷면, 앞면, 앞면)

동전을 반복하여 던져 아래와 같이 나온다면 규빈의 승리이다.

  • (앞면, 뒷면, 앞면, 뒷면, 뒷면)
  • (뒷면, 앞면, 뒷면, 뒷면)

이 내기는 하니가 유리한가? 규빈이 유리한가? 시뮬레이션을 통해 검증하라.

Note

hint: 당연히 5:5겠죠?

(4) 앞면과 뒷면이 나올 확률이 각각 1/2인 동전을 생각하자. 하니와 규빈은 이 동전을 연속으로 던져서 아래와 같은 룰을 정하여 내기를 하였다.

  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (뒷면,앞면) 이 나오면 하니의 승리
  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (뒷면,뒷면) 이 나오면 규빈의 승리

이 내기는 하니가 유리한가? 규빈이 유리한가? 시뮬레이션을 통해 검증하라.

Note

hint: 이 경우도 똑같이 유리합니다.

(5) 앞면과 뒷면이 나올 확률이 각각 1/2인 동전을 생각하자. 하니와 규빈은 이 동전을 연속으로 던져서 아래와 같은 룰을 정하여 내기를 하였다.

  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (앞면,뒷면) 이 나오면 하니의 승리
  • 동전을 연속으로 반복하여 던진다. 최근 2회의 결과가 (뒷면,뒷면) 이 나오면 규빈의 승리

이 내기는 하니가 유리한가? 규빈이 유리한가? 시뮬레이션을 통해 검증하라.

Note

hint: 이 내기는 하니가 유리합니다. 저는 1000회 시뮬레이션 결과

{'하니': 761, '규빈': 239} ## 하니가 761번 승리

와 같이 결과가 나왔습니다. 왜 하니가 유리한지 스스로 고민해보시기 바랍니다. (질문해도 되고요)