상속이란
기존 클래스를 수정하지 않고 기능을 확장하기 위한 방법
- 부모 클래스의 모든 기능을 그대로 사용 가능 => 다형성
- 필요한 기능만 자식 클래스에서 추가 또는 변경 가능
- 라이브러리나 공용 클래스를 활용할 때 특히 중요
- 코드를 공유
- 여러 클래스를 한꺼번에 제어 가능 = 업캐스팅(Upcasting) 화살표가 부모 클래스를 향하게
🔁 업캐스팅(Upcasting)이란?
자식 클래스 객체를 부모 클래스 타입으로 참조하는 것을 말한다.
즉,객체는 자식 & 참조 변수 타입은 부모인 상태다.
parent = Child() # 업캐스팅
이때 화살표(참조 방향)는 항상 부모 클래스를 향한다.
Sub(자식) is a Super
👉 자식 클래스는 부모 클래스의 한 종류이다
- Sub 클래스는 Super 클래스에 포함된다
- 자식은 부모의 성질과 기능을 모두 가진다
- 따라서 자식 객체는 부모 타입으로 참조 가능 (업캐스팅 가능)

- Character : 공통 부모 클래스
- Player / Enemy : Character를 상속받는 자식 클래스
- Player is a Character
- Enemy is a Character
- 일반화
여러 클래스의 공통된 속성과 행위를 추출하여 상위 클래스로 묶는 것
👉 구체적인 것들에서 공통점을 뽑아 더 추상적인 개념으로 만드는 과정
설계 관점: Player, Enemy → Character (일반화) - 구체화
일반적인 클래스를 상속받아 더 구체적인 클래스로 확장하는 것
구현 관점: Character → Player, Enemy (구체화)
📌 설계화와 구현 방식 정리
설계화 : Bottom- Up & Top-Down
구현 : Top-Down
1️⃣ 설계화 – Bottom-Up 방식
- 구체적인 클래스부터 설계
- 세부 기능 → 공통 개념을 추출
- 여러 하위 클래스의 공통점을 묶어 상위 클래스(일반화) 생성
- Player, Enemy → 공통 요소 추출 → Character
- 현실 객체 분석에 유리
- 클래스 구조 도출에 적합
2️⃣ 설계화 – Top-Down 방식
- 가장 추상적인 개념부터 설계
- 상위 클래스 먼저 정의
- 이후 역할별로 하위 클래스(구체화) 분리
- Character → Player / Enemy
- 시스템 구조를 먼저 잡을 때 유리
- 전체 아키텍처 파악에 적합
3️⃣ 구현(코딩) – Top-Down 방식
- 부모 클래스부터 구현
- 공통 기능을 먼저 완성
- 이후 자식 클래스에서 기능 확장
- 이유 : 코드 중복 방지
- 상속·업캐스팅·다형성 활용 가능
- 변경 영향 최소화
import random
class Character:
def __init__(self):
self.__x = random.randint(0, 800) # __ : private : 자식에게 안 보여줌(외부접근 제어)
self.__y = random.randint(0, 600)
#self.__myShape = "Character"
def display(self):
print(self.__x, end='\t')
print(self.__y, end='\t')
#print(self.__myShape)
#print("Character")
def moveLeft(self,dist):
self.__x -=dist
self.display()
def moveRight(self,dist):
self.__x += dist
self.display()
def getX(self):#외부에서 쓸라고
return self.__x
def setX(self, x):
self.__x=x
return self.__x
def getY(self):
return self.__y
def sety(self, y):
self.__y=y
return self.__y
def moveUp(self,dist):
self.__y -=dist
self.display()
def moveDown(self,dist):
self.__y +=dist
self.display()
class Player(Character):
#override (함수 재정의 - 원형 유지 )
def display(self):
# print(self.__x, end='\t') : self.__x 불가
# print(self.getX(), end='\t')
# print(self.getY(), end='\t')
super().display() #한단계위 부모꺼 사용 가능
print("Player")
class Enemy(Character):
def display(self): #재정의
super().display() #한단계위 부모꺼 사용 가능
print("Enemy")
1️⃣ 전체 구조 한눈에 보기
Character
├─ private: __x, __y
├─ moveLeft(), moveRight()
├─ moveUp(), moveDown()
├─ display()
│
├─ Player (display 재정의)
│
└─ Enemy (display 재정의)
- Character: 모든 캐릭터의 공통 기능
- Player / Enemy: 출력 방식만 다른 구체 클래스
- 이동 로직은 모두 부모(Character)에 있음
- 👉 일반화 + 구체화가 매우 깔끔한 구조
2️⃣ Character 클래스 설명 (설계 포인트)
🔐 private 멤버
self.__x
self.__y
- __ → private
- 외부 클래스, 자식 클래스에서 직접 접근 불가
- 캡슐화 완벽
🧭 이동 기능을 부모에 둔 이유
def moveUp(self, dist):
self.__y -= dist
self.display()
✔ Player와 Enemy 모두 이동 가능
✔ 중복 제거
✔ 자식에서 private 접근 문제 없음
📌 중요한 설계 판단
“공통 기능 + private 접근이 필요한 로직은 부모에 둔다”
3️⃣ Player 클래스 설명
class Player(Character):
def display(self):
super().display()
print("Player")
- 오버라이딩의 정석
- 메서드 이름 동일 (display)
- 매개변수 동일
- 부모 기능 재사용 + 확장
super().display()
print("Player")
- private 직접 접근 ❌
- 부모 메서드 통해 간접 접근 ⭕
4️⃣ Enemy 클래스 설명
class Enemy(Character):
def display(self):
super().display()
print("Enemy")
- Player와 동일한 구조
- 출력 문자열만 다름
5️⃣ 실행 흐름 예시 (중요)
c = Character()
p = Player()
e = Enemy()
c.moveUp(10)
p.moveUp(10)
e.moveUp(10)
실제 동작 흐름
- moveUp() → Character의 메서드
- 내부에서 self.display() 호출
- 객체 타입에 따라
- Character → Character.display
- Player → Player.display
- Enemy → Enemy.display
이게 바로 다형성
📌 1️⃣ 단일 상속 (Single Inheritance)
: 하나의 클래스가 오직 하나의 부모 클래스만 상속받는 구조
구조
Parent
↑
Child
특징
- 부모 클래스 1개만 존재
- 구조가 단순하고 이해하기 쉬움
- 충돌 위험 없음
- Python에서 가장 기본적인 상속 형태
class Character:
pass
class Player(Character): # 단일 상속
pass
✔ Player는 Character 하나만 상속
✔ Player is a Character
📌 2️⃣ 조상을 갖는 단일 상속 (다단계 상속, Multilevel Inheritance)
: 단일 상속이 연속으로 이어져 조상 클래스까지 상속받는 구조
직접 부모는 1개
하지만 위로 올라가면 조상 클래스가 존재
구조
GrandParent
↑
Parent
↑
Child
- 각 단계마다 단일 상속
- 결과적으로 여러 세대의 기능을 모두 상속
class Character:
pass
class Enemy(Character):
pass
class BossEnemy(Enemy): # 조상을 갖는 단일 상속
pass
✔ BossEnemy → Enemy → Character
✔ BossEnemy는 Enemy와 Character의 모든 기능을 가짐
📌 3️⃣ 다중 상속 (Multiple Inheritance)
: 하나의 클래스가 두 개 이상의 부모 클래스를 동시에 상속받는 구조
👉 여러 부모의 기능을 한 번에 물려받는 상속 방식
구조
ParentA ParentB
\ /
\ /
Child
- Child 클래스는 ParentA, ParentB 모두의 자식
- IS-A 관계가 동시에 여러 개 성립
class Fly:
def move(self):
print("날아서 이동")
class Swim:
def move(self):
print("헤엄쳐서 이동")
class Duck(Fly, Swim): # 다중 상속
pass
d = Duck()
d.move()
✔ Duck은 Fly이면서 Swim
✔ 여러 부모의 기능을 동시에 가짐
❗ 다중 상속의 문제점 (중요)
1️⃣ 메서드 충돌 문제
위 예제에서처럼
부모 클래스 둘 다 move()를 가지고 있으면
👉 어떤 메서드를 호출할지 애매해짐
2️⃣ 다이아몬드 문제 (Diamond Problem)
A
/ \
B C
\ /
D
- B, C가 A를 상속
- D가 B와 C를 동시에 상속
- 👉 A의 메서드를 한 번 상속받는가?두 번 상속받는가?➡️ 구조가 복잡해지고 오류 가능성 증가
정규 표현식
: 문자열의 “패턴”을 표현하는 문법
“이런 형태의 문자열을 찾고/검증하고/치환해라”를 짧게 적는 방법
예) 주민번호 형태: \d{6}-\d{7}
왜 필요한가?
- 복잡한 문자열 처리(검사/추출/치환)를 짧고 직관적으로 구현 가능
- split → 조건 검사 → 재조립 같은 긴 코드를 정규식 한 줄로 줄일 수 있음
- 예) re.sub()로 뒷자리 마스킹
핵심 메타문자(기초)
1) 문자 클래스 [ ]
[abc] : a/b/c 중 한 글자
범위: [a-z], [0-9], [A-Z], [가-힣]
제외: [^0-9] (숫자 아닌 것)
2) 자주 쓰는 축약
\d 숫자 = [0-9]
\D 숫자 아님
\s 공백류(화이트스페이스)
\S 공백 아님
\w 문자/숫자/_
\W 그 외
3) 점 .
. : \n 제외 모든 문자 1개
줄바꿈 포함하려면 re.DOTALL(re.S)
4) 반복
* : 0회 이상
+ : 1회 이상
{m} : m회
{m,n} : m~n회
? : 0 또는 1회 ({0,1})
re 모듈 사용 흐름
컴파일
import re
p = re.compile('[a-z]+')
검색 메서드 4개
match() : 문자열 처음부터 매치
search() : 전체에서 첫 매치
findall() : 매치되는 모든 문자열을 리스트
finditer() : 매치 결과를 반복자(match 객체들)
match 객체 주요 메서드
group() : 매치된 문자열
start() / end() : 시작/끝 인덱스
span() : (start, end)
컴파일 옵션
re.DOTALL (re.S) : .이 줄바꿈 포함
re.IGNORECASE (re.I) : 대소문자 무시
re.MULTILINE (re.M) : ^, $가 각 줄 기준
re.VERBOSE (re.X) : 정규식을 여러 줄 + 주석으로 가독성 향상
역슬래시(\) 문제와 raw string
: \는 정규식에서도 특수 의미가 있어서 이스케이프가 꼬이기 쉬움
해결: raw string 사용 권장
re.compile(r'\\section')
예시
1) 정규 표현식 없이 구현
data = """
park 800905-1049118
kim 700905-1059119
"""
result = []
for line in data.split("\n"):
word_result = []
for word in line.split(" "):
if len(word) == 14 and word[:6].isdigit() and word[7:].isdigit():
word = word[:6] + "-" + "*******"
word_result.append(word)
result.append(" ".join(word_result))
print("\n".join(result))
2) 정규표현식으로 구현
data = """
park 800905-1049118
kim 700905-1059119
"""
import re
pattern = r'(\d{6})[-]?\d{7}'
masked_data = [re.sub(pattern, r'\1-*******', line)for line in data.split('\n')]
print (masked_data)
1️⃣ 정규식 패턴
pattern = r'(\d{6})[-]?\d{7}'
- (\d{6}) : 숫자 6자리 캡처 그룹 (앞자리)
- [-]? : 하이픈 -이 있어도 되고 없어도 됨
- \d{7} : 숫자 7자리 (뒷자리)
2️⃣ 리스트 컴프리헨션
[line별로 정규식 치환 for line in data.split('\n')]
- 입력 텍스트를 줄 단위로 처리
- 결과를 리스트로 반환
3️⃣re.sub() 치환
re.sub(pattern, r'\1-*******', line)
- \1 : 첫 번째 캡처 그룹(앞 6자리) 재사용 [그대로 라는 뜻]
- 뒤 7자리는 *******로 마스킹'
#문제 1

import re
phone_list= ["01012345678", "010-987-6543", "010.1111.2222", "010 5555 4444"]
result_phone = []
for line in phone_list:
only_number = re.sub(r'[^0-9]','',line)
pattern = r'(\d{3})(\d{3,4})(\d{4})'
format_number = re.sub(pattern, r'\1-\2-\3', only_number)
result_phone.append(format_number)
print(result_phone)
핵심 1단계: 숫자만 남기기
only_number = re.sub(r'[^0-9]','', line)
정규표현식 해석
- [^0-9]
- [] : 문자 집합
- ^ (집합 안에서) : 부정
- 0-9 : 숫자
→ “숫자가 아닌 모든 문자”
동작
- 숫자가 아닌 문자를 전부 ''(빈 문자열)로 치환
- 즉, 제거 효과
변환 예시
| 원본 문자열 | 결과 (only_number) |
| 01012345678 | 01012345678 |
| 010-987-6543 | 0109876543 |
| 010.1111.2222 | 01011112222 |
| 010 5555 4444 | 01055554444 |
3) 핵심 2단계: 전화번호 패턴 정의
pattern = r'(\d{3})(\d{3,4})(\d{4})'
패턴 구성
- (\d{3})
- \d : 숫자 1개
- {3} : 정확히 3개
→ 국번 (예: 010)
- (\d{3,4})
- 숫자 3개 또는 4개
→ 중간 번호 - 010-123-4567 (3자리)
- 010-1234-5678 (4자리)
- 숫자 3개 또는 4개
- (\d{4})
- 숫자 4개
→ 끝 번호
전체 의미
- 숫자 4개
010 | 123 or 1234 | 4567
4) 핵심 3단계: 하이픈 형식으로 재조합
format_number = re.sub(pattern, r'\1-\2-\3', only_number)
치환 문자열 해석
- \1 : 첫 번째 그룹 ((\d{3}))
- \2 : 두 번째 그룹 ((\d{3,4}))
- \3 : 세 번째 그룹 ((\d{4}))
→ 그룹 사이에 - 삽입 - 예시 변환
| 숫자 문자열 | 변환 결과 |
| 01012345678 | 010-1234-5678 |
| 0109876543 | 010-987-6543 |
| 01011112222 | 010-1111-2222 |
| 01055554444 | 010-5555-4444 |
5) 최종 출력 결과
['010-1234-5678',
'010-987-6543',
'010-1111-2222',
'010-5555-4444']
6) 이 코드의 핵심 포인트 정리
- 입력 형식이 제각각이어도 문제 없음: -, ., 공백 등 어떤 구분자든 제거
- 숫자 길이 기반으로 재구성: 중간 번호를 {3,4}로 처리해 유연성 확보
- **정규식 그룹 + 치환 참조(\1, \2, \3)**의 전형적인 활용 예
#문제 2

import re
def pw_checker(pw):
pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$'
p = re.compile(pattern)
check = p.match(pw)
if check:
print ('조건을 만족합니다.: ', check.group())
else:
print('조건을 만족하지 않습니다.', pw)
pw_checker("Abcdefg123!2@")
pw_checker("abcdefg")
설명
# 1. 정규표현식 패턴 정의 (가장 중요한 부분)
# ^ : 문자열의 시작
# (?=.*[A-Z]) : [조건1] 대문자가 하나라도 있는가? (검사만 하고 커서 제자리)
# (?=.*[a-z]) : [조건2] 소문자가 하나라도 있는가? (검사만 하고 커서 제자리)
# (?=.*[0-9]) : [조건3] 숫자가 하나라도 있는가? (검사만 하고 커서 제자리)
# (?=.*[!@#$%^&*]) : [조건4] 특수문자가 하나라도 있는가? (검사만 하고 커서 제자리)
# .{8,} : [최종] 위 조건을 다 만족하면, 아무 글자나 8개 이상 가져와라.
# $ : 문자열의 끝
정규표현식 패턴을 조각내서 해석
패턴:
^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$
(1) ^ ... $ : 문자열 전체를 검사
- ^ : 문자열의 시작
- $ : 문자열의 끝
즉, “문자열 일부만”이 아니라 비밀번호 전체가 이 규칙에 맞아야 합니다.
(2) (?= ... ) : 전방탐색(lookahead)
- (?=...) 는 “이 조건이 뒤에 존재해야 한다” 를 의미하지만, 실제로 그 내용을 소비(consuming)하지는 않습니다.
즉, 자리 이동 없이 “포함 여부”만 검사하는 장치입니다. - (?=.*[A-Z])
- .* : 어떤 문자든(개행 제외) 0개 이상
- [A-Z] : 대문자 1개
→ “어딘가에 대문자가 최소 1개 있어야 함” - (?=.*[a-z])
→ “어딘가에 소문자가 최소 1개 있어야 함” - (?=.*[0-9])
→ “어딘가에 숫자가 최소 1개 있어야 함” - (?=.*[!@#$%^&*])
→ “어딘가에 !@#$%^&* 중 하나가 최소 1개 있어야 함”
(주의: 여기서 허용하는 특수문자는 이 집합에 있는 것만 “필수 포함” 조건으로 검사합니다.
(3) .{8,}
- . : 임의의 문자 1개
- {8,} : 8개 이상
→ “전체 길이가 최소 8자 이상이어야 함”
정리하면 이 패턴은:
길이 8자 이상, 대문자 최소 1, 소문자 최소 1, 숫자 최소 1, 지정 특수문자(!@#$%^&*) 최소 1
'LG DX DATA SCHOOL' 카테고리의 다른 글
| 12/30 기술 통계학 + R (0) | 2025.12.30 |
|---|---|
| 12/29 통계 기반 데이터 분석 (넘파이/ 판다스) (0) | 2025.12.29 |
| 12/23 객체 지향 복습 / 판다스 (Pandas) (0) | 2025.12.23 |
| 12/22 객체 지향 (0) | 2025.12.22 |
| 12/19 (0) | 2025.12.19 |