# 2020년 2학기 3차 파이썬 수업 정리



오늘 수업에 사용된 코드는 [Github](https://github.com/skku-overflow/python-2020-2/tree/main/week3)에 업로드했습니다.

오늘 다룬 내용들은

- 이터레이터
- 람다 함수
- list 초기화
- dict 생성 방법
- 클래스 상속
- 클래스의 특수 메소드
- Iterator 구현 및 yield

입니다.

## 이터레이터

파이썬에서 `for`문을 사용하려면 `in`의 오른쪽에 오는 식이 이터레이터여야합니다.

```python
print('List')
l = [1, 3, 5, 7, 9]

# l: 배열, for문이 작동하는 건 l이 이터레이터
# (배열이 이터레이터의 한 종륨)
for i in l:
    print(i)

print('Set')

# set은 중복을 허용하지 않음.
# set도 이터레이터 => for문의 오른쪽에 올 수 있음
s = set([1, 2, 3, 4, 5, 1, 2, 3])

for i in s:
    print(i)
```

우선 `list`나 `set`처럼 잘 알려진(?) 타입들이 `in`의 오른쪽에 올 수 있는 이유를 설명했습니다.
해당 타입들이 이터레이터 인터페이스를 구현했기 때문에 사용할 수 있는 건데요, 이 인터페이스를 만족하는 타입은 다 올 수 있습니다.

예를 들어, 아래 코드처럼 `dict`의 키 목록을 순회할 수도 있습니다.

```python

# dict 자료 타입
d = {
    'use': '사용하다',
    'add': '더하다',
}

# d['use']: 인덱싱
print(d['use'])
# 키 에러 (사전에 없는 단어)
# print(d['used'])

# dict.keys(): 이터레이터 반환
for k in d.keys():
    print(k)


```

마찬가지로 range 함수의 반환 값도 이터레이터 인터페이스를 만족합니다.

```python
# range: 이터레이터 반환하는 함수
for i in range(10):
    print(i)
```

### map

`map` 함수 역시 이터레이터를 반환합니다. 이 함수는 인자를 두개 받는데요, 첫번째 인자는 함수, 두번째 인자는 이터레이터입니다.
반환된 이터레이터는 인자로 받은 이터레이터의 각 원소에 인자로 받은 함수를 적용한 뒤 그 값을 뱉습니다.

```python
# e: 원소
def add_one(e):
    return e + 1


l = [1, 2, 3, 4, 5]

print('Base')
for i in l:
    print(i)

# 1 ~ 5 출력

print('Mapped')
for i in map(add_one, l):
    print(i)

# 2 ~ 6 출력

```

위는 각 원소에 1씩 더해서 출력하는 코드입니다.
`list`나 `set`의 경우와 마찬가지로 `map(add_one, l)`의 타입이 이터레이터이기 때문에 `in`의 오른쪽에 온 것입니다.

### filter, str.join

```python
sl = ['a', 'b', 'c', 'd', 'e']

print(', '.join(sl))


print('Strip')
print('a'.strip(), 'a')
print(' b '.strip(), 'b')
print('c '.strip(), 'c')

assert ' b '.strip() == 'b'
```

우선 `str.strip`이라는 메소드와 `assert` 문의 기능에 대해서 간단히 설명했습니다.

`str.strip`은 문자열 앞뒤의 공백을 없애주는 메소드입니다.

`assert`문은 `assert` 뒤의 표현식이 `False`이면 실행을 중지하고 에러를 뱉도록 합니다.
예를 들어, `assert False`를 하면 에러를 뱉고 아래의 코드는 실행되지 않고, `assert`뒤의 표현식이 복잡해도 동작은 같습니다.

그런 뒤 저 함수를 사용해서 `filter`와 `str.join`을 사용해봤는데요, `str.join`은 `','.join([1, 2])`처럼 쓰며 이 경우 결과값은 `1,2`가 됩니다.

```python

def is_not_empty(s):
    return s.strip() != ''


sl = ['a', '  ', ' ', 'b', 'c']


print(', '.join(sl))
print(', '.join(filter(is_not_empty, sl)))

```

`filter` 함수르 사용하면 이터레이터에서 원하지 않는 원소를 제외할 수 있습니다.

## lambda

위의 `filter` 사용례를 보면, 해당 줄만 보고는 이터레이터가 뭘 뱉을지 알 수 없어서 코드의 가독성도 떨어지고 함수를 정의해야해서 생산성도 떨어짐을 알 수 있습니다.
그럴 때 사용하는 게 `lambda`인데요, 아래와 같이 쓸 수 있습니다.

```python
def is_not_empty(s):
    return s.strip() != ''


sl = ['a', '  ', ' ', 'b', 'c']

# 이 식은, 함수 정의도 필요하고, 아래 문장만 봐선 이게 뭔지 정확히 알 수 없음
print(', '.join(filter(is_not_empty, sl)))

# 람다식 = 함수 정의
print(', '.join(filter(lambda s: s.strip() != '', sl)))
print(', '.join(filter(lambda a: a.strip() != '', sl)))


# 위에 3개의 print문읜 완전히 같음
```

람다식에서 `lambda`와 `:` 사이에 오는 건 함수의 파라미터로, 이름은 자유지만 개수는 중요합니다.

```python
def call(f):
    return f(1, 2, 3, 4, 5)


# 인자 개수 오류
# print(call(lambda a, b, c: a))

print(call(lambda a, b, c, d, e: a))
print(call(lambda a, b, c, d, e: [a, b, c, d, e]))
```

위의 코드에서 `print(call(lambda a, b, c: a))` 이 부분의 주석을 해제하면 오류를 뱉습니다.
이는 `call`이라는 함수가 `f`를 5개의 인자로 호출하기 때문입니다.

## for in list

파이썬은 배열을 초기화하는 아주 편리한 문법을 지원합니다. `[`와 `]`에 `for`문이 올 수 있는데요, 필요에 따라 `if`로 거르는 것도 가능합니다.

```python
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 15, 16]
copied = [i for i in l]
```

이렇게 `for`만 사용하는 것도 가능합니다.

특정 배열에서 짝수인 원소만 뽑아내고 싶다면 아래와 같이 하면 됩니다.

```python
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16]
even_list = [i for i in l if i % 2 == 0]
print('Even: ', even_list)
```

`if`문제어 짝수인 `i`만 선택하라는 조건을 달아줬기에 짝수면 남는 것입니다.

`for`의 왼쪽에 오는 식 역시 변형이 가능한데요, 홀수는 제외하고 짝수는 제곱을 한 배열은 다음과 같은 코드로 얻을 수 있습니다.

```python
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16]
squared = [i * i for i in l if i % 2 == 0]
print('Even, squared: ', squared)
```

`map`을 활용해서도 가능한데, 코드가 예쁘지 않아서 잘 쓰이진 않습니다.

```python
squared_map = [i for i in map(lambda elem: elem * elem, l) if i % 2 == 0]
print('Even, squared, map: ', squared_map)
```

## dict

### 사용

키가 숫자가 아닌 경우, 혹은 키가 연속되지 않고 지나치게 떨어진 (sparse) 한 경우 사용합니다.

### dict 초기화 방법

기본적으로 `dict` 타입은 아래와 같이 초기화 할 수 있습니다.

```python
alpha = {
    0: 'a',
    1: 'b',
    2: 'c',
    3: 'd',
    4: 'e',
    5: 'f',
    # ~ `25: 'z'`까지 `
}
print(alpha)
```

그런데 `dict` 타입은 아래와 같이 튜플의 배열을 이용해서도 초기화할 수 있습니다.

```python
alpha = dict([
    (0, 'a'),
    (1, 'b'),
    (2, 'c'),
    (3, 'd'),
    (4, 'e'),
    (5, 'f')
])

print(alpha)
```

배열 초기화 문법을 활용해서 2에서 19까지의 소수:제곱 쌍을 저장하는 `dict`는 다음과 같이 매우 간단한 코드로도 만들 수 있습니다.

```python
l = [2, 3, 5, 7, 9, 11, 13, 17, 19]
squared = dict([(i, i * i) for i in l])
```

## 클래스

### 클래스 상속

저번 수업 내용을 아직 공부하지 않으셨다고 하셔서 간단히 설명했습니다.

```python
class Animal:
    def walk(self):
        print('Animal.walk')


class Dog(Animal):
    pass


class Cat(Animal):
    # 오버라이딩
    def walk(self):
        print('Cat.walk')

        # 부모 클래스의 메소드 호출
        # super().walk()
```

자세한 개념은 이전 포스트를 참조하시면 될 것 같습니다.

### 특수 메소드

파이썬 문법에서 사용되는 경우 특수한 메소드가 호출됩니다. `+`연산의 왼쪽에 오면 `__add__`가 호출되고, `*`연산의 왼쪽에 오면 `__mul__`이 오는 그런 식인데요, `print` 했을 때 출력값을 바꾸는 함수는 `__str__`으로, 아래 코드처럼 사용하면 됩니다.

```python
class Overriden:
    def __str__(self):
        return 'Overriden'

class Default:
    pass

print(Overriden())
print(Default())
```

## Iterator

특수 메소드에 대한 내용을 설명한 건 이터레이터 때문인데요, 자기가 선언한 클래스가 `for`문의 오른쪽에 올 수 있게 하고 싶다면 `__iter__`라는 이름의 메소드를 정의하면 됩니다.

```python
class NotIter:
    pass

# __iter__을 구현하지 않은 클래스는 for문의 오른쪽에 올 수 없음
for i in NotIter():
    print(i)
```

이렇게 작성하면 `NotIter`은 이터레이터가 아니라는 에러가 납니다.

그런데 `__iter__`을 구현하려면 `yield`의 개념을 알아야합니다.

### yield

`yield` 키워드는 함수의 실행을 중지합니디. 제너레이터의 개념은 아직 이른 것 같아서 설명하지 않았고, `__iter__`에서 사용하는 것만 살펴봤습니다.
`__iter__`의 경우 루프문이 다음 원소를 요청할 때까지 함수가 중단되고, 만약 `break`등이 루프문의 실행을 끝내면 `__iter__`은 더이상 실행되지 않습니다.

### 구현

```python
class MyIter:
    def __iter__(self):
        print('Before 1')
        yield 1

        print('After 1')

        yield 2
        # 실행 끝남
        #
        # 실행이 끝났으므로 출력 X. iterator: Lazy
        print('After 2')

        yield 4

        yield 5


for i in MyIter():
    print('From loop: ', i)
    if i == 2:
        break


print('Done')
```

위 코드를 실행하면 우선 `for`의 오른쪽에 왔으므로 `__iter__` 실행돼서 `Before 1`이 출력됩니다.
그런 뒤 `yield 1`이 실행돼서 `__iter__`은 멈추고 `for`루프의 본문을 실행하는데요, 그 결과 `From loop: 1`이 출력됩니다.

루프문의 본문 실행이 끝나면, `for`문이 다음 원소를 요청하게 되면서 `__iter__`이 재시작되는데, 시작점은 `yield 1`의 뒤입니다.
그래서 `After 1`이 출력되고, `yield 2`에서 다시 루프문의 본문이 실행됩니다.
근데 이 경우엔 `break` 때문에 루프문이 끝나게 되고, `After 2`는 출려되지 않습니다.
대신 마지막에 있는 `Done`이 출력됩니다.

