Python_Day.7

조현기·5일 전

Python_Day.7

__str__과 객체 관리 예제, 파일·JSON 다루기, 이터레이터와 제너레이터(iter/next/yield), 데코레이터 응용, sys.

Day.6까지 클래스, 예외, 클로저, 데코레이터를 정리했다. 오늘은 그동안 배운 것을 작은 프로그램(회원 관리, 성적 평균)에 적용해 보고, 새로 이터레이터와 제너레이터를 배웠다. 노트 마지막에는 파이썬 과정에서 중요하다고 짚어 둔 항목도 정리해 두었다.


1. 객체를 관리하는 클래스 만들기

회원(User) 객체를 만들고, 이를 리스트로 보관하며 추가/삭제/조회하는 UserManager를 만들었다.

1-1. __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에서 예외 클래스의 메시지를 정할 때 쓴 것과 같은 특수 메소드다.

1-2. 회원 관리 클래스

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의 참/거짓 판별)

1-3. 삭제 기능: 두 가지 방법

처음에는 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를 인자로 받게 바꿨다.


2. 파일에 점수 저장하고 평균 구하기

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만 잡기 때문에, 파일이 있지만 비어 있는 경우는 따로 처리해야 한다.

3. 이터레이터

3-1. 이터러블과 이터레이터

용어뜻예
이터러블(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)

3-2. 이터레이터 클래스 직접 만들기

__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 객체:가 하는 일은 이렇다.

  1. iter(객체)로 __iter__()를 호출해 이터레이터를 얻는다.
  2. 반복마다 next()로 __next__()를 호출해 값을 받아 item에 넣는다.
  3. StopIteration이 발생하면 반복을 끝낸다.

3-3. 숫자를 세는 이터레이터

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)를 두 번 하면 두 번째는 [])


4. 제너레이터

4-1. 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으로 직접 했던 일이다.

4-2. 이터레이터 클래스 vs 제너레이터

짝수를 끝없이 만들어 내는 코드를 둘 다로 써 보면 차이가 보인다.

# 제너레이터 버전
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로 끊어야 한다.)

4-3. 제너레이터를 쓰는 이유: 메모리

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만으로 만들 수 있다는 점이 제너레이터의 장점이다.


5. 데코레이터 응용: 로그인 확인

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를 확인하므로, 값이 바뀌면 같은 함수의 결과가 달라진다.


6. JSON 파일 다루기

딕셔너리와 리스트를 JSON 파일로 저장하고 읽어 오는 방법이다. JSON은 언어와 상관없이 데이터를 주고받을 때 쓰는 텍스트 형식이다.

저장: json.dump

import 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.load

def 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이 반환된다.


7. 구구단과 컴프리헨션

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)와 리스트 컴프리헨션이 같은 결과를 낸다. 한 줄로 쓸 수 있는 반복은 컴프리헨션이 간결하다.


8. 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는 출력되지 않는다.

9. 클래스 정리: 클래스 변수와 self

class 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와 같은 정정이다.)


10. 파이썬에서 중요한 것

이날 노트 끝에 정리해 둔 항목이다. 지금까지 배운 것의 기준점이 된다.

  • 파이썬 자료 구조들 (리스트, 튜플, 딕셔너리, 집합)
  • 조건/반복문
  • 함수
  • 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은 json.dump(저장)/json.load(읽기)를 쓰고, 한글은 ensure_ascii=False, 보기 좋게는 indent를 쓴다.
  • 클래스 변수는 모든 인스턴스가 공유하고, 인스턴스를 통해 대입하면 그 인스턴스만의 변수가 새로 생긴다.

Tags: Python 이터레이터 제너레이터 yield json 데코레이터 클래스 __str__ 개발자 학습기록

profile
난 뭘까

0개의 댓글