2020년 2학기 3차 파이썬 수업 정리
오늘 수업에 사용된 코드는 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)
우선 list
나 set
처럼 잘 알려진(?) 타입들이 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씩 더해서 출력하는 코드입니다.
list
나 set
의 경우와 마찬가지로 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
뒤의 표현식이 복잡해도 동작은 같습니다.
그런 뒤 저 함수를 사용해서 filter
와 str.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
이 출력됩니다.