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

6 min read

오늘 수업에 사용된 코드는 Github에 업로드했습니다.

오늘 다룬 내용들은

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

입니다.

이터레이터

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

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)

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

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


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

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

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

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

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

map

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

# 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씩 더해서 출력하는 코드입니다. listset의 경우와 마찬가지로 map(add_one, l)의 타입이 이터레이터이기 때문에 in의 오른쪽에 온 것입니다.

filter, str.join

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뒤의 표현식이 복잡해도 동작은 같습니다.

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


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인데요, 아래와 같이 쓸 수 있습니다.

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: 사이에 오는 건 함수의 파라미터로, 이름은 자유지만 개수는 중요합니다.

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로 거르는 것도 가능합니다.

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

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

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

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

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을 활용해서도 가능한데, 코드가 예쁘지 않아서 잘 쓰이진 않습니다.

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 타입은 아래와 같이 초기화 할 수 있습니다.

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

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

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

print(alpha)

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

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

클래스

클래스 상속

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

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__으로, 아래 코드처럼 사용하면 됩니다.

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

class Default:
    pass

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

Iterator

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

class NotIter:
    pass

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

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

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

yield

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

구현

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이 출력됩니다.