Python_Day.5

조현기·5일 전

Python_Day.5

클래스 상속과 super(), 오버라이딩, 자주 쓰는 내장 함수, 표준 모듈(time, random, pickle), 모듈 직접 만들기.

Day.4에서 클래스의 기본 틀(__init__, self, 인스턴스 메소드)을 배웠다. 오늘은 그 위에 상속을 얹고, 파이썬이 기본으로 제공하는 내장 함수와 모듈, 그리고 내가 만든 파일을 모듈로 가져다 쓰는 방법까지 정리했다.


1. 클래스 복습

class Car:
    def __init__(self, make, model, year):   # 객체를 만들 때 자동으로 호출되는 생성자
        self.make = make
        self.model = model
        self.year = year
        self.speed = 0                        # 속성을 선언하면서 초기화

    # 인스턴스 메소드는 첫 번째 매개변수가 항상 self
    def name(self):
        return str(self.year) + " " + self.make + " " + self.model

    def speed1(self):
        print(str(self.speed) + '이다')


c1 = Car('테슬라', 'models', 2018)   # 객체명 = 클래스명(__init__의 매개변수)
print(c1.name())                      # 2018 테슬라 models
c1.speed = 15
c1.speed1()                           # 15이다

c1.name()처럼 인스턴스를 통해 호출하면 파이썬이 c1을 자동으로 self에 넣어 준다. 그래서 호출할 때는 인자를 안 넘기는데 정의에는 self가 있는 것이다.

self는 이름일 뿐이다

class Fruit:
    price = 20000                            # 클래스 변수

    def __init__(a, title, color):           # self 대신 a를 써도 동작한다
        a.title = title
        a.color = color

    def info(a):
        return "{} 과일은 {}색".format(a.title, a.color)

    def buy1(a, buy1):
        # buy1은 매개변수로 바로 쓰는 값이라 a.buy1처럼 속성에 붙일 필요가 없다
        return '{} 과일 {}에서 사야지~'.format(a.title, buy1)


a = Fruit('사과', '빨간')
b = Fruit('포도', '검은')

print(a.info())             # 사과 과일은 빨간색
print(b.info())             # 포도 과일은 검은색
print(a.buy1('코스트코'))    # 사과 과일 코스트코에서 사야지~
print(b.buy1('이마트'))      # 포도 과일 이마트에서 사야지~
  • 첫 번째 매개변수 이름이 꼭 self일 필요는 없다. 위치가 중요하고, 관례상 self를 쓴다.
  • 메소드 안에서만 쓰고 끝나는 값은 self.를 붙여 속성으로 만들 필요 없이 매개변수 그대로 쓰면 된다.
  • 파이썬의 모든 클래스는 최상위 클래스인 object를 상속받는다.

self가 없는 메소드

class A:
    def add(a, b):
        print(a + b)

A.add(3, 4)       # 7  (클래스 이름으로 호출)

A().add(3, 4)     # TypeError: A.add() takes 2 positional arguments but 3 were given

A.add(3, 4)는 일반 함수처럼 동작해서 7이 나온다. 반면 인스턴스로 호출하면 A()가 첫 번째 인자(a)로 자동 전달되어, 인자가 3개가 되어 버리기 때문에 에러가 난다.

필기에는 이 형태를 "클래스 메소드(정적 메소드)"로 적어 두었는데, 정확하게는 아니다. 정적 메소드는 @staticmethod를 붙여서 만드는 것이고, 그러면 인스턴스로 호출해도 에러가 나지 않는다. 위처럼 데코레이터 없이 self만 뺀 메소드는 "클래스 이름으로 부를 때만 우연히 동작하는" 형태다.

class B:
    @staticmethod
    def add(a, b):
        print(a + b)

B.add(3, 4)       # 7
B().add(3, 4)     # 7  (인스턴스로 호출해도 된다)

2. 클래스 안에 클래스 (객체를 속성으로 쓰기)

학생 객체를 만들고, 계산기 클래스가 학생들을 리스트로 갖고 있다가 평균을 구하는 예제다.

class Student:
    def __init__(self, name, id, score=0):    # 매개변수의 초기값(기본값) 설정
        self.name = name
        self.id = id
        self.score = score

    def getId(self):
        return self.id

    def getName(self):
        return self.name

    def setScore(self, score):
        self.score = score

    def getScore(self):
        return self.score


class Cal:
    def __init__(self):
        self.stu = []                  # 학생 객체를 담을 리스트

    def add(self, student):
        self.stu.append(student)

    def avg(self):
        total = 0
        for i in self.stu:
            total += i.score           # i.getScore()로 써도 같다
        average = total / len(self.stu)
        return average, len(self.stu)  # 값 두 개를 튜플로 반환


a = Student('Tom', 'qwe12')            # score는 기본값 0
a.setScore(70)

a2 = Student('Amy', 'NTE', 60)
a3 = Student('Bob', 'B01', 90)
a4 = Student('Cat', 'C01', 10)

c = Cal()
c.add(a)
c.add(a2)
c.add(a3)
c.add(a4)

p, s = c.avg()                         # 반환된 두 값을 각각 p, s에 받는다
print('{1}명의 평균은 {0}'.format(p, s))   # 4명의 평균은 57.5
  • score=0처럼 기본값이 있는 매개변수는 생략할 수 있다. a는 점수를 안 넘겨서 0으로 만들어졌고, 나중에 setScore(70)으로 바꿨다.
  • return average, len(self.stu)는 값을 두 개 돌려주는 것처럼 보이지만 사실 튜플 하나를 반환한다. 받는 쪽에서 p, s = ...로 언패킹했다. (Day.2의 패킹/언패킹)
  • Cal은 내부에 Student 객체들을 갖고 있다. 이렇게 객체를 다른 객체의 속성으로 담는 구조는 이후에도 자주 나온다.
  • 함수 안에서 sum을 변수 이름으로 쓰면 내장 함수 sum()을 가려 버린다. 위 코드에서는 total로 바꿨다.

3. 상속

상속은 부모 클래스의 속성과 메소드를 자식 클래스가 그대로 물려받는 것이다.

class 자식클래스(부모클래스):
    ...

3-1. super() 로 부모의 __init__ 호출하기

class Person:
    def __init__(self):
        print('안녕')


class Student(Person):
    def __init__(self):
        print('학생은 공부를 해야지')
        super().__init__()        # 부모(Person)의 __init__을 호출


a = Student()
# 학생은 공부를 해야지
# 안녕

자식 클래스에서 __init__을 다시 정의하면, 부모의 __init__은 자동으로 호출되지 않는다. 부모의 초기화가 필요하면 super().__init__()으로 직접 불러야 한다.

필기에는 "덮어쓰기 되는 식"이라고 적어 뒀는데, 이게 맞는 이해다. 호출하지 않으면 어떻게 되는지 확인해 보자.

class P2:
    def __init__(self):
        self.x = 1

class S2(P2):
    def __init__(self):       # super().__init__()을 빼먹음
        self.y = 2

s = S2()
print(s.y)    # 2
print(s.x)    # AttributeError: 'S2' object has no attribute 'x'

부모의 __init__이 실행되지 않았으니 x라는 속성이 만들어지지 않았다.

3-2. 부모의 __init__에 값 넘기기

class Camping(Car):                                  # 위에서 만든 Car 상속
    def __init__(self, make, model, year, bed):
        super().__init__(make, model, year)          # 공통 속성은 부모에게 맡김
        self.bed = bed                               # 자식만의 속성 추가


camp1 = Camping('벤츠', '스프린터', 2024, 2)

print(camp1.name())    # 2024 벤츠 스프린터   (부모의 메소드를 그대로 사용)
camp1.speed1()         # 0이다               (speed도 부모가 초기화해 줬다)
print(camp1.bed)       # 2

c1 = Car('현대', '아반떼', 1998)
print(c1.name())       # 1998 현대 아반떼
c1.speed = 60
c1.speed1()            # 60이다

Camping은 name()과 speed1()을 새로 만들지 않았는데도 쓸 수 있다. 부모에게 있는 것은 물려받고, 자식에만 필요한 bed만 추가한 것이다. 코드 중복이 줄어드는 게 상속의 장점이다.

3-3. 오버라이딩 (메소드 재정의)

부모에 있는 메소드를 자식이 같은 이름으로 다시 정의하는 것을 오버라이딩이라고 한다.

class Super:
    def a(self):
        print('super')


class Sub(Super):
    def a(self):                 # 오버라이딩: 부모의 a()를 재정의
        print('자식 클래스')

    def b(self):                 # 자식에만 있는 메소드
        print('sub')


a = Super()
b = Sub()

a.a()     # super
b.a()     # 자식 클래스     (자식 버전이 우선)
b.b()     # sub

a.b()     # AttributeError: 'Super' object has no attribute 'b'
  • 같은 이름의 메소드가 있으면 자식 쪽이 우선한다.
  • 상속은 한 방향이다. 자식은 부모의 것을 쓸 수 있지만, 부모는 자식에만 있는 메소드를 쓸 수 없다.

3-4. 오버라이딩 + super() 로 부모 기능 재활용

class Cal:
    def __init__(self, num1, num2):
        self.num1 = num1
        self.num2 = num2

    def div(self):
        return self.num1 / self.num2


class NewCal(Cal):
    def div(self):
        if self.num2 == 0:
            return 0                  # 0으로 나누는 경우만 새로 처리
        else:
            return super().div()      # 나머지는 부모의 div()를 그대로 사용


a = NewCal(6, 0)
b = NewCal(6, 3)

print(a.div(), b.div())    # 0 2.0

else에서 부모의 계산식을 다시 쓰지 않고 super().div()로 가져왔다. 부모 기능은 그대로 쓰고, 예외 상황만 자식에서 덧붙이는 방식이다.

필기 코드의 부모 div()는 num1 + num2를 반환하고 있었다. 이름이 div니까 나누기(/)로 고쳤다. 나누기로 고쳐야 num2 == 0을 따로 막는 이유(ZeroDivisionError)도 설명이 된다.


4. 클래스 문제 풀이

Q1. 클래스를 상속받고 메소드 추가하기

Calculator를 상속하는 UpgradeCal을 만들고, 값을 빼는 minus 메소드를 추가한다.

class Calculator:
    def __init__(self):
        self.value = 0

    def add(self, val):
        self.value += val


class UpgradeCal(Calculator):
    def minus(self, val):
        self.value -= val


cal = UpgradeCal()
cal.add(10)
cal.minus(7)
print(cal.value)    # 3

Q2. 값이 100을 넘지 않는 계산기

Calculator를 상속해서, add로 값을 더할 때 100이 넘으면 100으로 고정하는 MaxLimitCal을 만든다.

필기에서는 이 문제를 풀다가 add의 첫 매개변수를 self가 아닌 n이나 val로 쓰고, 메소드 안에서 value = 0처럼 지역 변수를 만들어 버려서 결과가 이상했다. 포인트는 두 가지다.

  • 첫 매개변수가 꼭 self여야 하는 것은 아니지만, 인스턴스가 들어오는 자리이므로 인스턴스 속성에 접근하려면 그 이름으로 n.value처럼 써야 한다.
  • 매개변수 없이 def add(val):만 쓰면 인스턴스가 들어올 자리가 없다. 이 상태에서 value = 0을 쓰면 인스턴스 속성이 아니라 함수 안에서만 쓰이는 지역 변수가 된다.

그래서 self.value를 쓰는 형태로 고치면 이렇게 된다.

class MaxLimitCal(Calculator):
    def add(self, val):
        super().add(val)             # 부모의 add로 먼저 더하고
        if self.value >= 100:        # 100 이상이면
            self.value = 100         # 100으로 고정


m = MaxLimitCal()
m.add(50)
m.add(70)
print(m.value)    # 100

Q6. 리스트 항목마다 3을 곱해서 반환하기

a = [1, 2, 3, 4]
print(list(map(lambda x: x * 3, a)))    # [3, 6, 9, 12]

5. 내장 함수

Day.4에 이어 자주 쓰는 내장 함수를 정리했다.

print(abs(-3))                  # 3  (절댓값)

all, any

iterable의 요소를 검사해서 참/거짓을 돌려준다.

print(all([1, 2, 3]))    # True   모두 참
print(all([1, 2, 0]))    # False  0이 하나 있으니까
print(any([1, 2, 0]))    # True   하나라도 참이면
print(any([0, 0]))       # False  전부 거짓일 때만
함수True가 되는 조건
all모든 요소가 참
any하나라도 참

조건식과 조합하면 더 유용하다.

a = [1, 60, 4, 10, 50]

print(all(i < 20 for i in a))    # False  (60, 50이 20 이상)
print(any(i > 30 for i in a))    # True   (60, 50이 30 초과)

필기에는 any([1,2,0])이 True인 이유를 "하나라도 False(1,2)이기 때문"이라고 적어 뒀는데, 정확히는 하나라도 참(1, 2)이 있기 때문이다. 참고로 빈 리스트는 all([])이 True, any([])가 False다.

chr, ord

print(chr(65))     # A   (아스키 코드 → 문자)
print(ord('A'))    # 65  (문자 → 아스키 코드)

enumerate

인덱스와 요소를 함께 돌려준다.

for i, name in enumerate(['body', 'foo', 'bar']):
    print(i, name)
# 0 body
# 1 foo
# 2 bar

필기에서는 {'body','foo','bar'}라는 set에 enumerate를 썼는데, set은 순서가 없어서 매번 출력 순서가 달라질 수 있다. 인덱스가 의미가 있으려면 리스트처럼 순서가 있는 자료형에 쓰는 게 좋다.

filter, map

a = [1, 60, 4, 10, 50]

def num(x):
    return abs(x) > 3

print(list(filter(num, a)))                      # [60, 4, 10, 50]
print(list(filter(lambda x: x > 3, a)))          # [60, 4, 10, 50]

print(list(map(lambda x: x + 2, range(1, 7))))   # [3, 4, 5, 6, 7, 8]
  • filter(함수, iterable): 함수가 참을 돌려주는 요소만 추출한다.
  • map(함수, iterable): 모든 요소에 함수를 적용한 결과를 돌려준다.
  • 반복 가능한 객체는 리스트, 튜플, 문자열, 딕셔너리, range, 파일 객체 등이다.
  • 결과를 보려면 list()로 감싸야 한다. (Day.4에서 정리한 대로 map/filter는 지연 평가라서 그대로 출력하면 객체 주소만 나온다.)

id, len, type

print(id(10))           # 객체의 주소값 (실행 환경마다 다름)
print(len('파이썬'))     # 3
print(len([1, 2, 3, 4, 5]))    # 5

print(type(3))     # <class 'int'>
print(type({}))    # <class 'dict'>
print(type(()))    # <class 'tuple'>
print(type([]))    # <class 'list'>

range

print(range(5))                  # range(0, 5)     (리스트가 아니라 range 객체)
print(range(1, 10, 2))           # range(1, 10, 2)
print(list(range(1, 10, 2)))     # [1, 3, 5, 7, 9]  (시작, 끝(미포함), 증감)

# 9부터 0까지 1씩 감소
print(list(range(9, -1, -1)))    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

max, min

print(max([1, 2, 3]))    # 3
print(max('ABCD'))       # D   (사전순으로 가장 뒤)
print(min('ABCD'))       # A   (사전순으로 가장 앞)
print(max(1, 2, 3))      # 3   (리스트 없이 값을 바로 나열해도 된다)

round

print(round(4.6))         # 5
print(round(3.6432, 2))   # 3.64  (소수점 둘째 자리까지)

주의할 점이 있다. 파이썬의 round는 우리가 학교에서 배운 사사오입(반올림)이 아니라 가장 가까운 짝수로 보내는 방식이다.

print(round(2.5))    # 2
print(round(3.5))    # 4
print(round(0.5))    # 0

딱 .5인 경우에는 결과가 짝수 쪽으로 가기 때문에, 평균 점수처럼 정확한 반올림이 필요한 곳에서는 헷갈릴 수 있다.

sorted, sum, zip

print(sorted([5, 4, 3, 2, 1, 6]))                  # [1, 2, 3, 4, 5, 6]
print(sorted([5, 4, 3, 2, 1, 6], reverse=True))    # [6, 5, 4, 3, 2, 1]
print(sorted(['p', 'y', 't', 'h', 'o', 'n']))      # ['h', 'n', 'o', 'p', 't', 'y']

print(sum([1, 2, 3, 4, 5]))    # 15
print(sum(range(1, 11)))       # 55

print(list(zip([10, 20, 30], [40, 50, 60])))
# [(10, 40), (20, 50), (30, 60)]   (같은 인덱스끼리 튜플로 묶는다)
  • sorted는 새 리스트를 반환하고 원본은 바뀌지 않는다. 필기에서는 list(reversed(sorted(...)))로 내림차순을 만들었는데, reverse=True 옵션을 쓰면 더 간단하다.
  • zip은 여러 iterable의 같은 위치 요소를 묶어 주는 함수다.

6. 표준 모듈: time, random, pickle

내장 함수와 달리 import로 불러와서 쓰는 모듈들이다.

time

import time

print(time.time())                  # 1970-01-01부터 지금까지의 초 (실수)
print(time.localtime(time.time()))  # 연/월/일/시/분/초 등을 담은 구조체
print(time.ctime())                 # 사람이 읽는 형태의 현재 시간 문자열

# 가장 많이 쓰는 형식
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
# 예) 2026-09-11 17:30:05
코드의미
%Y연도(4자리)
%m월
%d일
%H시(24시간)
%M분
%S초

random

import random

print(random.random())        # 0.0 이상 1.0 미만의 실수
print(random.randint(1, 45))  # 1 이상 45 이하의 정수 (둘 다 포함)

필기에는 random.random()이 "1.0까지"라고 되어 있는데, 1.0은 포함하지 않는다. randint(1, 45)는 두 번째 인자도 포함이다. 필기에 "미만이 아니라 포함임!"이라고 적어 둔 부분이 이 점이다.

d = [1, 2, 3, 4, 5]
r = random.shuffle(d)    # 리스트를 섞는다. 원본이 바뀐다.
print(r)                 # None  (반환값은 없다)
print(d)                 # 섞인 리스트

print(random.choice(d))  # 하나를 무작위로 선택

shuffle은 원본 리스트를 직접 바꾸고 None을 반환한다. 그래서 d = random.shuffle(d)처럼 쓰면 d가 None이 되어 버린다.

pickle

파이썬 객체를 그대로 파일에 저장하고 다시 읽어 올 때 쓴다. 저장하는 것을 직렬화라고 한다.

import pickle

with open('test.obj', 'wb') as f:      # 쓰기용 바이너리 모드
    obj = {1: '파이썬', 2: '자바스크립트', 3: '리액트'}
    pickle.dump(obj, f)                # 객체를 파일에 저장

with open('test.obj', 'rb') as f:      # 읽기용 바이너리 모드
    data = pickle.load(f)
    print(data)                        # {1: '파이썬', 2: '자바스크립트', 3: '리액트'}
  • 텍스트가 아니라 바이너리로 저장하기 때문에 모드가 'wb', 'rb'다. (Day.4의 'w', 'r'에 b가 붙은 것)
  • 딕셔너리, 리스트, 클래스 인스턴스 등 자료형 그대로 저장되고 복원된다.
  • 출처를 모르는 pickle 파일은 load 하지 않는 게 좋다. 불러오는 과정에서 코드가 실행될 수 있어서 보안상 위험하다.

webbrowser

import webbrowser

webbrowser.open('http://google.com')        # 이미 열린 브라우저에서 사이트 열기
webbrowser.open_new('http://google.com')    # 새 브라우저 창으로 열기

7. 모듈, 패키지, 라이브러리

용어뜻
모듈파이썬 코드가 들어 있는 .py 파일 하나
패키지여러 모듈을 폴더로 묶은 것
라이브러리여러 패키지와 모듈을 모아 놓은 것 (예: pandas, numpy)

내가 만든 파일을 모듈로 쓰기

덧셈, 곱셈 함수를 담은 파일을 하나 만든다. 필기에서는 test.py라고 했지만, test는 파이썬 표준 라이브러리에도 있는 이름이라 충돌할 수 있다. 여기서는 mymath.py로 정했다.

# mymath.py
def add(x, y):
    return x + y

def multi(x, y):
    return x * y

if __name__ == "__main__":
    print(add(1, 3))
    print(multi(1, 3))

if __name__ == "__main__": 의 의미

파이썬 파일을 실행하면, 인터프리터가 그 파일에 __name__이라는 이름표를 붙여 준다.

  • 직접 실행한 파일은 __name__이 "__main__"이 된다.
  • 다른 파일에서 import 한 파일은 __name__이 "파일명"(여기서는 mymath)이 된다.

그래서 if __name__ == "__main__": 아래의 코드는 그 파일을 직접 실행할 때만 동작하고, import 해서 쓸 때는 실행되지 않는다. 모듈 안에 간단한 테스트 코드를 넣어 두는 용도로 많이 쓴다.

직접 실행하면 이렇게 나온다.

$ python mymath.py
4
3

다른 파일에서 import 하면 위 print는 실행되지 않고, 함수만 쓸 수 있다.

다른 파일에서 불러오기

# 방법 1: 모듈 전체를 import
import mymath
print(mymath.add(1, 2))      # 3
print(mymath.multi(1, 2))    # 2

# 방법 2: 필요한 함수만 import (모듈 이름 생략 가능)
from mymath import add, multi
print(add(1, 2))             # 3
print(multi(1, 2))           # 2

# 방법 3: 모두 import (* 는 모든 함수를 의미)
from mymath import *
print(add(1, 2))             # 3
print(multi(1, 2))           # 2
방식호출특징
import mymathmymath.add()어느 모듈의 함수인지 분명하다
from mymath import addadd()짧게 쓸 수 있다
from mymath import *add()편하지만 이름이 겹칠 수 있다

import *는 이름 충돌 위험이 있어서, 필요한 것만 명시해서 가져오는 쪽이 안전하다.


8. 문제 풀이: 로또 번호 생성

이날 나머지 시간은 문제 풀이였고, 그중 Q12. 로또 번호 생성하기가 막혔다. 1~45 사이의 중복 없는 번호 6개를 뽑는 문제다.

처음 푼 방식

import random
a = []

while True:
    num = random.randint(1, 45)
    a.append(num)
    a = set(a)         # set으로 바꿔서 중복 제거
    a = list(a)        # 다시 list로
    if len(a) > 6:
        while True:
            if len(a) >= 7:
                a.pop()    # 7개가 되면 하나 빼서 6개로
            else:
                break
        break
    elif len(a) <= 6:
        continue

print(a)

돌아가긴 하지만 복잡하다. 매번 set과 list를 오가며 중복을 제거하고, 7개가 됐다가 다시 하나를 빼는 식으로 6개를 맞췄다.

not in 을 쓰면

Day.2에서 배운 in / not in을 쓰면 훨씬 간단하다.

import random
result = []

while len(result) < 6:
    a = random.randint(1, 45)
    if a not in result:        # 아직 없는 번호만 추가
        result.append(a)

print(result)    # 예) [6, 7, 10, 17, 42, 44]

이미 뽑은 번호인지 not in으로 검사하고, 6개가 될 때까지만 반복하면 끝이다. while 조건에 len(result) < 6을 두었기 때문에 따로 break도 필요 없다.

참고로 이런 용도로 만들어진 함수도 있다.

print(sorted(random.sample(range(1, 46), 6)))    # 중복 없이 6개를 뽑아 정렬

random.sample(모집단, 개수)는 중복 없이 지정한 개수만큼 뽑아 준다.

이미 배운 in / not in을 떠올렸다면 훨씬 짧게 풀 수 있는 문제였다.


마무리

  • 상속: class 자식(부모)로 부모의 속성과 메소드를 물려받는다. 자식이 __init__을 다시 정의하면 super().__init__()으로 부모 초기화를 직접 불러야 한다.
  • 오버라이딩: 같은 이름의 메소드를 자식에서 재정의하면 자식 버전이 우선한다. super().메소드()로 부모 기능을 재활용할 수 있다. 부모는 자식에만 있는 메소드를 쓸 수 없다.
  • self가 없는 메소드는 정적 메소드가 아니다. 정적 메소드는 @staticmethod로 만든다.
  • 내장 함수: all/any, enumerate, filter/map, range, max/min, round, sorted, sum, zip. round는 .5일 때 짝수 쪽으로 가고, shuffle은 None을 반환한다는 점을 주의한다.
  • 모듈: time, random, pickle을 import 해서 쓰고, 내 파일도 모듈로 만들어 import나 from ... import로 가져올 수 있다. if __name__ == "__main__":은 직접 실행할 때만 동작하는 영역을 만든다.

Tags: Python 클래스 상속 super 오버라이딩 내장함수 모듈 pickle 개발자 학습기록

profile
난 뭘까

0개의 댓글