클래스 상속과
super(), 오버라이딩, 자주 쓰는 내장 함수, 표준 모듈(time,random,pickle), 모듈 직접 만들기.
Day.4에서 클래스의 기본 틀(__init__, self, 인스턴스 메소드)을 배웠다. 오늘은 그 위에 상속을 얹고, 파이썬이 기본으로 제공하는 내장 함수와 모듈, 그리고 내가 만든 파일을 모듈로 가져다 쓰는 방법까지 정리했다.
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를 상속받는다.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 (인스턴스로 호출해도 된다)
학생 객체를 만들고, 계산기 클래스가 학생들을 리스트로 갖고 있다가 평균을 구하는 예제다.
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로 바꿨다.상속은 부모 클래스의 속성과 메소드를 자식 클래스가 그대로 물려받는 것이다.
class 자식클래스(부모클래스):
...
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라는 속성이 만들어지지 않았다.
__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만 추가한 것이다. 코드 중복이 줄어드는 게 상속의 장점이다.
부모에 있는 메소드를 자식이 같은 이름으로 다시 정의하는 것을 오버라이딩이라고 한다.
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'
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)도 설명이 된다.
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]
Day.4에 이어 자주 쓰는 내장 함수를 정리했다.
print(abs(-3)) # 3 (절댓값)
all, anyiterable의 요소를 검사해서 참/거짓을 돌려준다.
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, ordprint(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, mapa = [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, typeprint(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'>
rangeprint(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, minprint(max([1, 2, 3])) # 3
print(max('ABCD')) # D (사전순으로 가장 뒤)
print(min('ABCD')) # A (사전순으로 가장 앞)
print(max(1, 2, 3)) # 3 (리스트 없이 값을 바로 나열해도 된다)
roundprint(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, zipprint(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의 같은 위치 요소를 묶어 주는 함수다.time, random, pickle내장 함수와 달리 import로 불러와서 쓰는 모듈들이다.
timeimport 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 | 초 |
randomimport 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가 붙은 것)load 하지 않는 게 좋다. 불러오는 과정에서 코드가 실행될 수 있어서 보안상 위험하다.webbrowserimport webbrowser
webbrowser.open('http://google.com') # 이미 열린 브라우저에서 사이트 열기
webbrowser.open_new('http://google.com') # 새 브라우저 창으로 열기
| 용어 | 뜻 |
|---|---|
| 모듈 | 파이썬 코드가 들어 있는 .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__"이 된다.__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 mymath | mymath.add() | 어느 모듈의 함수인지 분명하다 |
from mymath import add | add() | 짧게 쓸 수 있다 |
from mymath import * | add() | 편하지만 이름이 겹칠 수 있다 |
import *는 이름 충돌 위험이 있어서, 필요한 것만 명시해서 가져오는 쪽이 안전하다.
이날 나머지 시간은 문제 풀이였고, 그중 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().메소드()로 부모 기능을 재활용할 수 있다. 부모는 자식에만 있는 메소드를 쓸 수 없다.@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 개발자 학습기록