Data_Analysis_Track_33/Python

Python_05-2(정보 은닉, property, decorator, 상속, 메소드 오버라이딩, isinstance)

lsc99 2023. 8. 21. 19:33

정보 은닉(Information Hiding) : Attribute의 값을 caller(객체 외부)가 바꾸는 것을 방지하기 위해 직접 호출을 막는다.
setter/getter 메소드를 통해 값을 변경/조회 하도록 한다.
- 데이터 보호가 주 목적이다.
Attribute 직접 호출 막기
보호하고 싶은 Attribute의 이름을 앞에 __(double underscore)붙이기
setter : Attribute의 값을 변경하는 메소드, set으로 시작
getter : Attribute의 값을 조회하는 메소드, get으로 시작

p = Person('홍길동', 20, '서울')
print(p.name, p.age, p.address)
p.print_info()

 

# 값 변경
p.age = 30
p.print_info()

 

p.age = '오십세'
p.print_info() # 나이의 값은 int로 받아야 하지만 동적타입 언어 특성상 문자열 '오십세'와 같은 값을 넣어도 작동한다.

 

p.add_age(2) # 그래서 add_age함수를 사용하여 나이에 2를 더하려고 해도 type이 맞지 않아 오류가 발생한다.

 

p.age = 3_000_000 # 나이에 비정상적인 숫자도 대입할 수 있기에 이를 막아줄 방법이 필요하다. -> 정보 은닉
p.add_age(5)
p.print_info()

 

# Person class에 정보 은닉 적용
# 1. attribute 변수들을 외부에서 호출 할 수 없도록 만들어 준다.
#  - self.__변수명 = 초기값
# 2. attribute변수들을 조회(getter), 변경(setter)하는 메소드를 정의한다.

class Person:
    
    def __init__(self, name, age, address):
        self.name = name
        self.__age = age # 직접 호출을 막는다.
        self.address = address
        self.email = None
        
    # age 값을 조회하는 메소드
    def get_age(self):
        return self.__age # 같은 class에서는 __age(원래이름)로 호출 가능
    
    # age 값을 변경하는 메소드
    def set_age(self, age):
        if 0 <= age <= 100: # 조건달기 -> 변경하려는 age의 값이 비정상적일 경우 변경하지 않도록 한다.
            self.__age = age
        else:
            print(f"{age}는 나이에 넣을 수 없습니다. 0 ~ 100 사이의 정수를 넣어주세요.")

 

p = Person('홍길동', 20, '서울')
print(p.name)
print(p.address, p.email)
# print(p.age) 직접 호출 막힘
print(p.get_age())

 

p.set_age(5000) # set_age의 조건에 충족하지 못함
p.__dict__ # age의 값이 변경되지 않는다.
print(p._Person__age) # 외부에서는 이 이름으로 접근 가능하다. -> 굳이 호출할 수 있는 방법은 있다.

 

p.name = '홍길동2'
p.__age = 30 # 원래의 age는 바뀌지 않고 다른 변수가 추가되었다.
p._Person__age = 40 # 원래의 age가 바뀐다 -> 완벽히 변경을 제한하지는 못한다.
p.__dict__


property 함수 : 정보은닉되었던 __Attribute들을 다시 변수를 사용하는 방식으로 호출할 수 있게 한다.
목적 : 정보은닉된 instance 변수의 값을 사용하려고 할때 조건을 달기 위해서
구문 : property(get메소드, set메소드) (조건 : getter/setter 메소드가 있어야함)

class Person2:
    
    def __init__(self, name, age, address):
        self.__name = name
        self.__age = age # 직접 호출을 막는다.
        self.address = address
        self.email = None
        
    # age 값을 조회하는 메소드
    def get_age(self):
        return self.__age # 같은 class에서는 __age(원래이름)로 호출 가능
    
    # age 값을 변경하는 메소드
    def set_age(self, age):
        if 0 <= age <= 100:
            self.__age = age
        else:
            print(f"{age}는 나이에 넣을 수 없습니다. 0 ~ 100 사이의 정수를 넣어주세요.")
            
    def get_name(self):
        return self.__name
    
    def set_name(self, name):
        # 이름은 두글자 이상일 경우만 변경가능
        if len(name) >= 2:
            self.__name = name
        else:
            print("이름은 두 글자 이상만 가능합니다.")
    
    name = property(get_name, set_name) # 변수 = property 선언
    age = property(get_age, set_age)

 

p3 = Person2('유재석', 40, '인천')
p3.address = '새주소'
p3.email = '새 이메일 주소'
p3.name = '새이름'
p3.age = 20

print(p3.name, p3.age, p3.address, p3.email)

 

p3.name = '강' # 조건 충족 X
p3.age = 5000

p2 = Person2('유재석', 40, '인천')
# name, age 사용 -> 메소드
p2.set_name('류재석') # set_name(property함수)를 통해 이름 변경, 조건에 맞지 않다면 변경되지 않는다.
print(p2.get_age())
# address, email 사용 -> 변수를 호출
print(p2.address)
p2.email = 'afiepg@af.com'
p2.__dict__


데코레이터(decorator) 이용해 property 지정 : setter/getter 구현 + property()를 이용해 변수 등록 하는 것을 더 간단하게 구현하는 방식
getter메소드: @property 데코레이터를 선언
setter메소드: @getter메소드이름.setter 데코레이터를 선언.
! 반드시 getter 메소드를 먼저 정의한다.
setter메소드 이름은 getter와 동일해야 한다.

class Person3:
    
    def __init__(self, name, age, address):
        self.__name = name
        self.__age = age # 직접 호출을 막는다.
        self.address = address
        self.email = None
        
    # getter 메소드에 @property, 메소드이름은 변수처럼 지정.
    @property
    def age(self):
        return self.__age # 같은 class에서는 __age(원래이름)로 호출 가능
    
    # setter 메소드에 @getter이름.setter, 메소드 이름은 getter와 동일하게 지정. getter 메소드가 먼저 입력되어야 함.
    @age.setter
    def age(self, age):
        if 0 <= age <= 100:
            self.__age = age
        else:
            print(f"{age}는 나이에 넣을 수 없습니다. 0 ~ 100 사이의 정수를 넣어주세요.")
        
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self, name):
        # 이름은 두글자 이상일 경우만 변경가능
        if len(name) >= 2:
            self.__name = name
        else:
            print("이름은 두 글자 이상만 가능합니다.")

 

p4 = Person3('박명수', 10, '부산')
print(p4.name, p4.age, p4.address, p4.email)
p4.__dict__

 

p4.name = '이순신' # 변경하는 방식을 똑같게 만들었지만 name과 age 변경에 대한 조건을 거친다.
p4.age = 50
p4.address = '서울'
p4.email = 'iajfi@e.com'
p4.__dict__



상속(Inheritance) : 기존 클래스를 확장하여 새로운 클래스를 구현한다.
ex) 동물 : 먹을 수 있다. 사람 : 말할 수 있다. 고양이 : 높이 점프할 수 있다. -> 사람/고양이는 동물을 상속한다.
분류의 개념으로 보았을 때 동물 : 상위 분류, 사람/고양이 : 하위 분류가 된다.

기반(Base) 클래스, 상위(Super) 클래스, 부모(Parent) 클래스
- 물려주는 클래스, 상속하는 클래스에 비해 더 추상적인 클래스가 된다.
- 상속하는 클래스의 데이터 타입이 된다.
파생(Derived) 클래스, 하위(Sub) 클래스, 자식(Child) 클래스
- 상속하는 클래스, 상속을 해준 클래스 보다 좀더 구체적인 클래스가 된다.
* is a 관계 ex) 사람(자식)은 동물(부모)이다(O), 동물(부모)은 사람(자식)이다.(X)

상위 클래스와 하위 클래스는 계층관계를 이룬다.
- 상위 클래스는 하위 클래스 객체의 타입이 된다.

class Person:
    
    def go(self):
        print('간다.')
    
    def eat(self):
        print('먹는다.')

 

# Person을 상속해서 Student를 정의
# class 클래스이름(상속할 클래스이름(, 상속할 클래스이름2, ....))
class Student(Person):
    def study(self):
        print('학생은 공부한다.')

 

# Person을 상속해서 Teacher를 정의
class Teacher(Person):
    def teach(self):
        print('수업을 가르친다.')

 

# 객체 생성
s = Student()
s.study() # Student 클래스의 기능
s.eat()   # 부모 클래스의 기능을 사용
s.go()    # 부모 클래스의 기능을 사용

 

t = Teacher()
t.teach() # Teacher 클래스의 기능
t.eat()   # 부모 클래스의 기능을 사용
t.go()    # 부모 클래스의 기능을 사용


다중 상속과 단일 상속
다중상속 : 여러 클래스로부터 상속할 수 있다
단일상속 : 하나의 클래스로 부터만 상속할 수 있다.
파이썬은 다중상속을 지원한다.

MRO (Method Resolution Order) : 다중상속시 메소드 호출할 때 그 메소드를 찾는 순서.
순서 : 자기자신, 상위클래스(하위에서 상위로 올라간다)
다중상속의 경우 먼저 선언한 클래스 부터 찾는다. (왼쪽->오른쪽)
MRO 순서 조회 : Class이름.mro()

Student.mro() # 다중 상속시 메소드를 찾는 순서

 

class E: # object클래스를 상속하고 있다.
    pass

class F:
    pass

class G:
    pass

 

class C(E, F):
    pass

class D(G):
    pass

 

class A(C, D):
    pass

 

A.mro() # object 클래스는 최상위 계층


Method Overriding(메소드 재정의) : 상위 클래스의 메소드의 구현부를 하위 클래스에서 다시 구현하는 것을 말한다.
상위 클래스는 모든 하위 클래스들에 적용할 수 있는 추상적인 구현밖에는 못한다.
이 경우 하위 클래스에서 그 내용을 자신에 맞게 좀 더 구체적으로 재구현할 수 있게 해주는 것을 Method Overriding이라고 한다.
방법은 하위 클래스에서 overriding할 메소드의 선언문은 그대로 사용하고 그 구현부는 재구현하면 된다.

상속하고 있는 하위 클래스에서 메소드를 재정의(Overriding)하여 구현부를 구체화(메소드 이름은 같게 사용)
메소드 이름을 다르게 사용하면 호출할 메소드의 종류가 많아서 복잡해진다. -> 같은 이름으로 오버라이딩 할 것.
메소드 재정의 -> 부모 클래스에서 정의된 메소드와 똑같은 이름으로 재정의할 시 하위 클래스에서 정의된 메소드가 작동한다.

class Person2:
    
    def go(self):
        self.eat()
        print('간다.')
        # print('스쿨버스를 타고 간다.') -> Student방식
    
    def eat(self):
        print('먹는다.', Person2) # Student와 Teacher에 공통적
        # print('급식을 먹는다.') -> Student방식

 

class Student2(Person2):
    # go()메소드를 Student 클래스에 맞게 좀 더 구체화된 내용으로 재정의(Overriding)한다.
    # 상위클래스에 정의된 메소드와 동일한 선언(이름)으로 메소드를 구현.
    def go(self):
        print("스쿨버스를 타고 등교한다.")
    
    def eat(self):
        print("학교 식당에 간다.")
        print("급식을 받는다.")
        print("먹는다.")     # 자식클래스에 정의한 eat()을 실행시킨다.
        super().eat()        # super() : 부모클래스를 가리킨다. -> 부모클래스에서 정의한 eat() 실행
    def study(self):
        print("학생이 공부한다. 2")

 

class Teacher2(Person2):
    def teach(self):
        print("교사가 가르친다. 2")

 

t = Teacher2()
t.go()
t.eat()

 

s = Student2()
s.go() # Student2클래스에서 오버라이딩한 go()함수 실행(우선순위를 가진다.)
print('-' * 20)
s.eat() # Student2클래스에서 오버라이딩한 eat()함수 실행(우선순위를 가진다.)

 

Student2.mro()

# 상속과 Attribute
class Person3:
    
    def __init__(self, name, age, address = None):
        self.__name = name
        self.__age = age
        self.__address = address
    
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self, name):
        if name: # None이 아닐 경우
            self.__name = name
        else:
            print("이름을 변경 못함")
            
    @property
    def age(self):
        return self.__age
    
    @age.setter
    def age(self, age):
        if 8 <= age <= 19: # 나이 변경 조건
            self.__age = age
        else:
            print('나이를 변경 못함')
            
    @property
    def address(self):
        return self.__address
    
    @address.setter
    def address(self, address):
        if address: # None이 아닐 경우
            self.__address = address
        else:
            print('주소 변경 못함')
            
    # 나이를 더하는 메소드
    def add_age(self, age):
        self.age = self.age + age
        # @age.setter : age = @property : age + 파라미터 age
        
    # Person객체의 속성값들을 하나의 문자열로 묶어서 반환.
    def get_info(self): # getter 메소드 호출.
        return f"이름 : {self.name}, 나이 : {self.age}, 주소 : {self.address}"

 

# Student의 속성 : name, age, address -> 공통속성, grade(성적. 학생의 속성)
class Student3(Person3):
    
    def __init__(self, name, age, address, grade): # 자식 클래스도 공통속성과 클래스만의 속성 모두 받아야 한다.
        # name, age, address -> 부모클래스인 Person3의 속성
        super().__init__(name, age, address) # super() -> 부모클래스의 initializer 호출 -> 부모클래스에게 공통속성인 name, age, address를 넘긴다.
        self.__grade = grade
        
    @property
    def grade(self):
        return self.__grade
    
    @grade.setter
    def grade(self, grade):
        if grade > 0:
            self.__grade = grade
        else:
            print('grade 변경 못함')
            
    def get_info(self): # method overriding -> Person3에 정의된 get_info()에 grade까지 return 되도록 재정의
        i = super().get_info() # 이름, 나이, 주소(공통속성)들은 Person3(부모클래스)에 정의된 메소드 호출
        return f"{i}, 성적 : {self.grade}" # getter : grade 호출

 

s = Student3('김학생', 17, '서울', 3)
s.name, s.age, s.address, s.grade # getter 호출

 

s.add_age(-3)
s.age

 

info = s.get_info() # s : Student 객체
print(info)

 

s.age = 120 # 나이 변경 조건에 맞지 않음
print(s.get_info())

 

s.grade = 12 # grade 변경
s.grade

 

s.name = None # 변경 조건에 충족되지 못하는 경우
s.grade = -50

 

# Teacher의 속성 : name, age, address -> 공통속성, subject(과목. Teacher의 속성), job 직첵 -> 특별한 대입조건이 없다.
# 상속 속성 : name, age, address , 
#    메소드 : add_age(), get_info() 메소드
class Teacher3(Person3):
    def __init__(self, name, age, address, subject, job = None): # job = 직책
        # name, age, address -> 부모클래스의 __init__을 이용해서 초기화.
        super().__init__(name, age, address)
        self.__subject = subject
        self.job = job # setter가 필요 없는 변수
        
    # subject의 getter/setter 구현
    @property
    def subject(self):
        return self.__subject
    
    @subject.setter
    def subject(self, subject):
        if subject: # None이 아닐 경우
            self.__subject = subject
        else:
            print('과목 변경 못함')
        
    # Teacher 객체의 attribute들을 반환 -> get_info()를 method overriding
    def get_info(self):
        # 이름, 나이, 주소 -> Person3의 get_info()를 사용
        i = super().get_info()
        return f"{i}, 담당과목 : {self.subject}, 담당직책 : {self.job if self.job else '없음'}"

 

t = Teacher3('박선생', 30, '부산', '수학', '학생주임')
info = t.get_info()
print(info)

 

t.subject = None
t.job = None
info = t.get_info()
print(info)


객체 관련 유용한 내장 함수, 특수 변수
isinstance(객체, 클래스이름-datatype) : 결과값 -> True or False
- 조건을 달아줄 때 유용하게 사용한다.

 

p = Person3('이름', 30, '주소')
t = Teacher3('이선생', 40, '부산', '영어', '교감')
s = Student3('김학생', 14, '서울', 1)

 

# 변수 p의 타입이 무엇인지?
type(p) == Person3 # 클래스 이름이 타입

 

type(30) == int # 위와 같은 개념

 

print(isinstance(p, Person3)) # p가 Person3의 인스턴스(타입)인가? -> True
print(isinstance(p, Person), isinstance(p, str))

s1 = 30
print(type(s1) == int)
print(isinstance(s1, int))
print(isinstance(s1, float))
print(isinstance(s1, (int, float))) # s가 int 또는 float 타입인가? -> True

 

# isinstance 예시
def function(value):
    if isinstance(value, (int, float)):
        print(value ** 2)
    else:
        print('계산 못하는 타입', type(value))

 

function(10)
function(3.3)
function('aaa')

 

isinstance(t, Teacher3), isinstance(s, Student3)
# 상위클래스가 자식클래스 객체의 타입이 된다.
isinstance(t, Person3), isinstance(s, Person3)

 

def func(value):
    # Student, Teacher 객체를 받아서 add_age() 이용해서 나이를 변경하고 attribute들을 출력
    if isinstance(value, Person3): # value가 Person3의 클래스의 타입인가?
        value.add_age(2)
        i = value.get_info()
        print(i)

 

func(s)

s.__dict__

s.__class__.__name__ # class 이름을 문자열로 반환