__str__과 객체 관리 예제, 파일·JSON 다루기, 이터레이터와 제너레이터(iter/next/yield), 데코레이터 응용,sys.
Day.6까지 클래스, 예외, 클로저, 데코레이터를 정리했다. 오늘은 그동안 배운 것을 작은 프로그램(회원 관리, 성적 평균)에 적용해 보고, 새로 이터레이터와 제너레이터를 배웠다. 노트 마지막에는 파이썬 과정에서 중요하다고 짚어 둔 항목도 정리해 두었다.
회원(User) 객체를 만들고, 이를 리스트로 보관하며 추가/삭제/조회하는 UserManager를 만들었다.
__str__: 객체를 문자열로 바꾸기User 객체를 그냥 print 하면 객체의 주소 같은 값이 나온다.
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
u = User('app', 'Tom', 'tom@example.com')
print(u) # <__main__.User object at 0x...>
print(u.__dict__) # {'id': 'app', 'name': 'Tom', 'email': 'tom@example.com'}
__dict__는 객체의 속성을 딕셔너리로 보여 준다. 디버깅할 때 속성 값을 한눈에 볼 수 있다.
print(객체)를 하면 파이썬이 __str__을 자동으로 호출해서 그 반환값을 출력한다. 그래서 __str__을 정의하면 원하는 모양으로 출력할 수 있다.
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
def __str__(self): # print(객체)할 때 자동 호출
return f'{self.id}, {self.name}, {self.email}'
print(User('app', 'Tom', 'tom@example.com')) # app, Tom, tom@example.com
__str__은 반드시 문자열을 return 해야 한다. (print를 하는 게 아니다.) Day.6에서 예외 클래스의 메시지를 정할 때 쓴 것과 같은 특수 메소드다.
class UserManager:
def __init__(self):
self.user = [] # User 객체를 담을 리스트
def add_User(self, user):
self.user.append(user)
def list_User(self):
for i in self.user:
print(i) # User에 __str__이 있어야 보기 좋게 나온다
def find_user(self, user_id): # id로 회원 찾기
for i in self.user:
if user_id == i.id:
return i
return None # 없으면 None
manager = UserManager()
manager.add_User(User('app', 'Tom', 'tom@example.com'))
manager.add_User(User('ele', 'Amy', 'amy@example.com'))
manager.add_User(User('opqr112', 'Bob', 'bob@example.com'))
manager.list_User()
# app, Tom, tom@example.com
# ele, Amy, amy@example.com
# opqr112, Bob, bob@example.com
a = manager.find_user('app')
print('찾으시는 분은')
if a: # 찾았으면 객체(참), 못 찾았으면 None(거짓)
print(a.name) # Tom
else:
print('id에 맞는 user가 없음')
find_user는 못 찾으면 None을 반환하고, if a:에서 None은 거짓으로 취급된다. (Day.2의 참/거짓 판별)
처음에는 for로 돌면서 remove하는 방식으로 만들었다.
def del_User(self, del_id):
for i in self.user:
if i.id == del_id:
self.user.remove(i)
그리고 리스트 컴프리헨션으로 조건에 맞는 것만 남겨 새로 만드는 방식도 있다.
def delete_User(self, user_id):
self.user = [i for i in self.user if i.id != user_id]
두 방식은 결과가 항상 같지 않다. 리스트를 순회하는 도중에 그 리스트에서 remove하면 요소를 건너뛴다. id가 같은 회원이 둘 있을 때 확인해 보았다.
# 'dup' id를 가진 회원이 둘 + 'k' 회원이 하나
manager.del_User('dup') # for + remove
manager.list_User()
# dup, B, b@x.com ← 지워지지 않고 남았다
# k, C, c@x.com
manager.delete_User('dup') # 컴프리헨션
manager.list_User()
# k, C, c@x.com ← 둘 다 지워졌다
첫 번째 dup을 지우면 뒤 요소들이 한 칸씩 당겨지는데 for의 위치는 그대로 다음 칸으로 넘어가기 때문에, 바로 뒤에 있던 dup을 건너뛴다. id가 중복되지 않는다는 보장이 있으면 문제가 없지만, 그렇지 않다면 컴프리헨션으로 새 리스트를 만드는 쪽이 안전하다.
필기의
del_User는input()으로 id를 직접 입력받는 형태였다. 테스트하기 쉽도록 위에서는 id를 인자로 받게 바꿨다.
Day.4의 파일 입출력을 활용한 예제다.
def score1(name, score):
with open('score1.txt', 'a', encoding='utf-8') as f: # 'a': 이어쓰기
f.write(f'{name},{score}\n')
def average():
total = 0
cnt = 0
try:
with open('score1.txt', 'r', encoding='utf-8') as f:
for line in f:
line = line.strip() # 양쪽 공백과 \n 제거
name, score = line.split(',') # 쉼표로 나눠 언패킹
total += int(score)
cnt += 1
return total / cnt
except FileNotFoundError: # 파일이 없을 때
print('파일이 없습니다')
average() # 파일이 없습니다 (반환값은 None)
score1('Tom', 96)
score1('Amy', 55)
print(average()) # 75.5
Tom,96 같은 한 줄이 쌓인다. 'a' 모드라서 실행할 때마다 줄이 계속 추가된다.strip()은 문자열 양쪽의 공백과 줄바꿈을 지운다. lstrip()은 왼쪽만, rstrip()은 오른쪽만 지운다.line.split(',')의 결과가 두 개일 때만 name, score로 언패킹된다. 두 개가 아니면 ValueError가 난다.sum으로 썼는데, 내장 함수 sum을 가리는 이름이라 total로 바꿨다.cnt가 0이라 ZeroDivisionError가 난다. 지금 except는 FileNotFoundError만 잡기 때문에, 파일이 있지만 비어 있는 경우는 따로 처리해야 한다.| 용어 | 뜻 | 예 |
|---|---|---|
| 이터러블(iterable) | 요소를 하나씩 꺼낼 수 있는 객체. iter()에 넣을 수 있다 | 리스트, 튜플, 문자열, 딕셔너리, 집합, range |
| 이터레이터(iterator) | next()로 다음 값을 하나씩 꺼내 주는 객체 | iter(리스트)의 결과 |
a = [1, 2, 3]
la = iter(a) # 리스트로부터 이터레이터를 만든다
print(type(la)) # <class 'list_iterator'>
print(next(la)) # 1
print(next(la)) # 2
print(next(la)) # 3
print(next(la)) # StopIteration 예외 (더 이상 값이 없다)
next()로 값을 꺼낼 때마다 다음 위치로 이동하고, 끝나면 StopIteration 예외가 발생한다. for문은 이 예외를 알아서 처리해서 반복을 멈춘다.list()나 for에 넣으면 빈 결과가 나온다.print([x for x in la]) # [] (이미 다 소비했다)
필기에서는 next(it)가 안 되는 이유를 "iter() 선언을 하지 않았기 때문"이라고 적었다. 정확히는 리스트는 이터러블이지 이터레이터가 아니기 때문이다.
a = [1, 2, 3]
next(a) # TypeError: 'list' object is not an iterator
next(iter(a)) # 1
그리고 [1,2,3].__iter__()는 iter([1,2,3])과 같은 동작이다. iter(x)는 x.__iter__()를, next(it)는 it.__next__()를 호출하는 것이다.
it = [1, 2, 3, 4, 5].__iter__()
print(next(it)) # 1
print(it.__next__()) # 2
b = 'hello'.__iter__()
print(b.__next__()) # h
print({'a': 1, 'b': 3}.__iter__()) # <dict_keyiterator object ...> (딕셔너리는 키를 순회)
print({1, 2, 3}.__iter__()) # <set_iterator object ...>
이터레이터 객체에 iter()를 다시 하면 자기 자신이 돌아온다. (iter(la) is la가 True)
__iter__와 __next__를 구현한 클래스는 for문에 넣을 수 있다.
class MyIterator:
def __init__(self, data):
self.data = data
self.position = 0
def __iter__(self):
return self # 자기 자신이 이터레이터
def __next__(self): # __iter__를 만들었다면 같이 만들어야 한다
if self.position >= len(self.data):
raise StopIteration # 끝났다는 신호
result = self.data[self.position]
self.position += 1
print('지금!')
return result
for item in MyIterator([1, 2, 3]):
print(item)
# 지금!
# 1
# 지금!
# 2
# 지금!
# 3
지금!과 값이 번갈아 나온다. 리스트를 미리 다 만들어 두는 것이 아니라 for가 한 번 돌 때마다 __next__가 호출되어 그때그때 값을 만든다는 것을 보여 준다.
for item in 객체:가 하는 일은 이렇다.
iter(객체)로 __iter__()를 호출해 이터레이터를 얻는다.next()로 __next__()를 호출해 값을 받아 item에 넣는다.StopIteration이 발생하면 반복을 끝낸다.class Cnt:
def __init__(self, stop):
self.stop = stop
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current < self.stop:
result = self.current
self.current += 1
return result
else:
raise StopIteration # 예외를 일부러 발생시켜 끝을 알린다
c = Cnt(5)
for i in c:
print(i) # 0 1 2 3 4
필기에는 "
for문이 객체를 받을 때 자동으로__init__을 호출한다"는 주석이 있는데,__init__은Cnt(5)로 객체를 만들 때 호출된다.for가 호출하는 것은__iter__와__next__다.
c는 한 번 순회하면 current가 5가 되어 있어서, 같은 c를 다시 for에 넣으면 아무것도 출력되지 않는다. (list(c)를 두 번 하면 두 번째는 [])
yield이터레이터를 만들려면 클래스에 __iter__, __next__, 위치 관리까지 써야 해서 번거롭다. 제너레이터는 함수에 yield만 쓰면 이터레이터가 만들어진다.
def gen1():
yield 0 # 0을 바깥으로 전달하고, 실행을 잠시 멈춘다
yield 1
yield 2
g1 = gen1() # 함수가 실행되는 게 아니라 제너레이터 객체만 만들어진다
print(next(g1)) # 0
print(next(g1)) # 1
print(next(g1)) # 2
print(next(g1)) # StopIteration
for i in gen1(): # 새 제너레이터 객체를 만들어 순회. __iter__, __next__가 자동 호출된다
print(i) # 0 1 2
yield는 값을 바깥에 전달하면서 함수의 실행을 거기서 멈추고, 다음 next()가 오면 멈춘 자리부터 이어서 실행한다. 이 동작은 출력 순서로 확인할 수 있다.
def gen_trace():
print('a')
yield 1
print('b')
yield 2
gt = gen_trace()
print('생성') # 생성 ← 아직 a는 안 나온다
print(next(gt)) # a, 1
print(next(gt)) # b, 2
gen_trace()를 호출했을 때는 본문이 실행되지 않고, 첫 next()에서 첫 yield까지 실행된다.
return은 함수를 끝내지만, yield는 상태(지역 변수와 실행 위치)를 유지한 채 일시 정지한다. 그래서 필기에 "상태 유지가 자동으로 된다"고 적어 둔 것이다. 이터레이터 클래스에서는 self.position으로 직접 했던 일이다.
짝수를 끝없이 만들어 내는 코드를 둘 다로 써 보면 차이가 보인다.
# 제너레이터 버전
def even_gen():
num = 0
while True:
yield num
num += 2
# 이터레이터 클래스 버전
class EvenIt:
def __init__(self):
self.num = 0
def __iter__(self):
return self
def __next__(self):
result = self.num
self.num += 2
return result
g = EvenIt()
print(next(g), next(g), next(g), next(g), next(g), next(g)) # 0 2 4 6 8 10
제너레이터 쪽이 훨씬 짧다. 둘 다 무한 반복이지만 next()로 필요한 만큼만 꺼내 쓰기 때문에 프로그램이 멈추지 않는다. (for로 그냥 돌리면 끝나지 않으니 break나 itertools.islice로 끊어야 한다.)
nums = [i**2 for i in range(1, 10000)] # 9999개를 메모리에 전부 저장
nums = (i**2 for i in range(1, 10000)) # 필요할 때마다 계산 (제너레이터 표현식)
리스트 컴프리헨션은 [], 제너레이터 표현식은 ()를 쓴다. 제너레이터 표현식은 값을 저장하지 않으므로 데이터가 아무리 커져도 객체 크기가 거의 그대로다. (확인해 보니 sys.getsizeof로 200바이트 안팎이었고, 리스트는 개수에 비례해서 커진다. 환경에 따라 수치는 다를 수 있다.)
def get():
for i in range(1, 100000):
yield i * 2
g = get() # 제너레이터 객체만 생긴다
print(next(g)) # 2 (한 개씩 계산되어 나온다)
print(next(g)) # 4
print(next(g)) # 6
| 상황 | 선택 |
|---|---|
값에 여러 번 접근, 인덱싱, len(), 슬라이싱이 필요 | 리스트 (컴프리헨션) |
| 값을 한 번씩 순서대로 쓰고 버림, 데이터가 매우 크거나 무한 | 제너레이터 |
제너레이터는 이터레이터의 한 종류다. 모든 제너레이터는 이터레이터이고, 그 반대는 아니다. __iter__/__next__ 없이 yield만으로 만들 수 있다는 점이 제너레이터의 장점이다.
Day.6의 데코레이터를 실제 상황에 적용한 예제다. 로그인하지 않았으면 함수가 실행되지 않게 막는다.
login_state = False
def login(func):
def wrapper():
if not login_state: # 로그인 안 했으면
print('로그인')
return # 원래 함수를 실행하지 않고 끝
return func() # 로그인 했으면 원래 함수 실행
return wrapper
@login # write_post = login(write_post)
def write_post():
print('hide on bush')
write_post() # 로그인
login_state = True
write_post() # hide on bush
글쓰기 함수 본문은 건드리지 않고, @login 한 줄로 "로그인 확인"이라는 공통 기능을 덧붙였다. 이런 공통 기능을 여러 함수에 붙일 수 있다는 것이 데코레이터의 쓸모다. wrapper가 매번 login_state를 확인하므로, 값이 바뀌면 같은 함수의 결과가 달라진다.
딕셔너리와 리스트를 JSON 파일로 저장하고 읽어 오는 방법이다. JSON은 언어와 상관없이 데이터를 주고받을 때 쓰는 텍스트 형식이다.
json.dumpimport json
def add(data):
with open('stu.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
datas = [{'name': 'Tom', 'score': 100}, {'name': '한글', 'score': 63}]
add(datas)
print('저장')
만들어진 stu.json은 이렇다.
[
{
"name": "Tom",
"score": 100
},
{
"name": "한글",
"score": 63
}
]
| 옵션 | 역할 |
|---|---|
ensure_ascii=False | 한글이 한글 같은 코드로 바뀌는 것을 막는다 |
indent=2 | 들여쓰기로 보기 좋게 정렬한다 |
dump를 dumps로 바꾸면 파일이 아니라 JSON 문자열을 돌려준다.
print(json.dumps({'name': '한글'})) # {"name": "한글"}
print(json.dumps({'name': '한글'}, ensure_ascii=False)) # {"name": "한글"}
json.loaddef loading():
try:
with open('stu.json', 'r', encoding='utf-8') as f:
return json.load(f) # 파일 → 파이썬 객체(리스트/딕셔너리)
except FileNotFoundError:
print('파일이 없다')
data = loading()
print(type(data)) # <class 'list'>
print(data[1]['name']) # 한글
필기에서는 loading()만 호출하고 반환값을 받지 않았는데, return json.load(f)의 결과를 쓰려면 data = loading()처럼 변수에 받아야 한다. 파일이 없을 때는 메시지만 출력하고 None이 반환된다.
def gugu(n):
result = []
i = 1
while i < 10:
result.append(n * i)
i += 1
return result
result = [i * 2 for i in range(1, 10)]
print(result) # [2, 4, 6, 8, 10, 12, 14, 16, 18]
print(gugu(2)) # [2, 4, 6, 8, 10, 12, 14, 16, 18]
while로 만든 gugu(2)와 리스트 컴프리헨션이 같은 결과를 낸다. 한 줄로 쓸 수 있는 반복은 컴프리헨션이 간결하다.
sys: 실행 제어import sys
print(sys.path) # 모듈을 찾는 경로 목록 (파이썬 패키지 위치)
print(type(sys.path)) # <class 'list'>
print('start')
sys.exit() # 프로그램을 즉시 종료한다
print('end') # 실행되지 않는다
sys.path는 import할 때 파이썬이 모듈을 찾아보는 폴더 목록(리스트)이다. Day.6에서 만든 패키지가 import 되려면 이 경로 어딘가에 있어야 한다.sys.exit()는 SystemExit 예외를 발생시켜 프로그램을 끝낸다. 그래서 end는 출력되지 않는다.selfclass Dog(object): # (object)는 생략 가능. 모든 클래스는 object를 상속한다
species = 'firstdog' # 클래스 변수: 모든 인스턴스가 공유
def __init__(self, name, age):
self.name = name # 인스턴스 변수: 객체마다 따로 존재
self.age = age
a = Dog('Bori', 2)
b = Dog('Coco', 4)
print(id(a) == id(b)) # False (서로 다른 객체)
print(Dog.species) # firstdog (클래스 이름으로 접근)
print(a.species) # firstdog (인스턴스로도 접근)
print(b.species) # firstdog
클래스 변수를 클래스에서 바꾸면 모든 인스턴스에 반영된다. 반면 인스턴스를 통해 대입하면 그 인스턴스에만 같은 이름의 변수가 새로 생긴다.
Dog.species = 'x'
print(a.species) # x
a.species = 'mine' # a 에만 인스턴스 변수가 생긴다
print(a.species, b.species, Dog.species) # mine x x
self가 있는 메소드와 없는 메소드class SelfTest:
def func1(): # self가 없다
print('Func1 called')
def func2(self): # 호출하면 인스턴스가 self로 자동 전달된다
print('Func2 called')
f = SelfTest()
f.func2() # Func2 called
SelfTest.func1() # Func1 called (클래스 이름으로 호출하면 된다)
SelfTest.func2(f) # Func2 called (self 자리에 인스턴스를 직접 전달)
f.func1() # TypeError: func1() takes 0 positional arguments but 1 was given
SelfTest.func2() # TypeError: func2() missing 1 required positional argument: 'self'
f.func1()은 파이썬이 func1(f)로 호출하려 해서 에러가 난다.SelfTest.func2()는 self로 들어갈 인스턴스가 없어서 에러가 난다.SelfTest.func2(f)처럼 인스턴스를 직접 넘기면 동작한다. 이게 f.func2()가 내부적으로 하는 일이다.필기에서는
func1을 "클래스 메소드"라고 적었지만, 정확히는@classmethod나@staticmethod를 붙이지 않은 그냥 함수다. (Day.4, Day.5와 같은 정정이다.)
이날 노트 끝에 정리해 둔 항목이다. 지금까지 배운 것의 기준점이 된다.
lambda, filter, map …__init__, __str__, __next__, __iter__, __call__ …)특수 메소드(__xxx__)를 한 번에 모아 보면 이렇다.
| 메소드 | 호출되는 시점 |
|---|---|
__init__ | 객체를 만들 때 |
__str__ | print(객체), str(객체) |
__iter__ | iter(객체), for문 시작 |
__next__ | next(객체), for문의 매 반복 |
__call__ | 객체()처럼 함수처럼 호출할 때 |
__str__을 정의하면 print(객체)가 읽기 좋은 문자열로 나온다. __dict__는 속성을 딕셔너리로 보여 준다.remove하면 요소를 건너뛸 수 있다. 조건에 맞는 것만 남기는 컴프리헨션이 안전하다.iter()에 넣을 수 있음)과 이터레이터(next()로 꺼냄)는 다르다. 이터레이터는 한 번 소비하면 끝이고, 끝에서 StopIteration이 발생한다.yield로 만드는 이터레이터다. 실행을 멈췄다 이어가면서 상태를 유지하고, 값을 필요할 때만 계산해서 메모리를 아낀다.json.dump(저장)/json.load(읽기)를 쓰고, 한글은 ensure_ascii=False, 보기 좋게는 indent를 쓴다.Tags: Python 이터레이터 제너레이터 yield json 데코레이터 클래스 __str__ 개발자 학습기록