[Python] Class Structure & Special Method

Hyeseong·2020년 12월 6일
0

python

목록 보기
11/22
post-thumbnail

클래스 관련 메소드 심화

Private 속성 실습

파이썬에서도 객체의 클래스 안에 설계되어 있는 변수(속성 값)들을 private으로 만들 수 있는 데코레이터를 제공해요.

slot 예제

slot 예제는 추천하는 편이에요. 성능이 좋습니다. 성능 측정을 통해 이를 증명해 볼게요.

객체 슬라이싱, 인덱싱

객체 슬라이싱을 해볼게요.

ABC, 상속, 오버라이딩

ABC 메타클래스를 상속 받아서, 추상화, 오버라이딩이라는 개념을 배워볼게요.
사실 모든 언어에는 디자인 패턴이라는 부분이 있어요. 좋은 회사, 어느정도 인프라가 구축된 회사에서는 자주 물어 보는 패턴중 하나에요. 디자인 패턴을 알고 있는지, 얼마나 깊이 있게 아는지, 그냥 갖다 쓰는것만 할줄 아는건 아닌지 말이지요. 그래서 파이썬에서 제공하는 추상 메서드.


class 선언

클리스를 만든다는 것은 쉽게 말하자면 설계도를 만드는 것이에요. 다른 사람이 내가 만든 설계도를 보고

  • 잘 이해하고
  • 사용하기 쉽고
  • 효율성도 좋아야 하조
# class 선언 
class VectorP(object):
    def __init__(self, x, y):
        self.__x = float(x)  # 언더바가 두개(__)에요. 한개(_)가 아니라.
        self.__y = float(y)
    
    def __iter__(self): # iter 메소드를 이용하면 for문이나 next메소드로 출력이 가능한 객체로 만듬
        return (i for i in (self.__x, self.__y))                  
        # data 양이 많을 때는 generator가 좋고 그게 아니라면 통으로 리스트로 보내도 됨
# 객체 선언
v = VectorP(20,40)
print('ex1-1', v.__x, v.__y )

이제 코드를 실행해보면 아래와 같이 결과가 나와요. 언더바 두 개로 만든 변수 __x, __y가 없다고 하네요. 이는 파이썬의 특징중 하나인 언더바 두개로 만든 속성값은 직접 접근 할 수 없게 감추어 버린거에요.
output

AttributeError: 'VectorP' object has no attribute '__x'

언더바 두개로 정의한 변수들을 한개로 바꿀게요. 그리고 출력해보면 잘 됩니다.

class VectorP(object):
    def __init__(self, x, y):
        self._x = float(x)
        self._y = float(y)
    
    def __iter__(self): # iter 메소드를 이용하면 for문이나 next메소드로 출력이 가능한 객체로 만듬
        return (i for i in (self._x, self._y))                  

# 객체 선언
v = VectorP(20,40)
print('ex1-1', v._x, v._y )

# 출력결과: ex1-1 20.0 40.0

앞서 우리가 iter메서드로 구현했기에 for문으로 출력 되겠지요?

for val in v:
    print('ex1-2',val)
    
# 출력 결과:
# ex1-2 20.0
# ex1-2 40.0

만약 인스턴스 생성시 인자값y를 특정값(예. 30)보다 크게 하려면 어떻게 할까요?
기존 작성한 VectorP 클래스의 init 메소드안에 조건문으로 약간의 변형과 raise 키워드를 이용하면 가능 할 것 같아요.

class VectorP(object):
    def __init__(self, x, y):
        self._x = float(x)
        if y < 30:
            raise ValueError('Do Not Enter y value below 30')
        self._y = float(y)
    
    def __iter__(self): # iter 메소드를 이용하면 for문이나 next메소드로 출력이 가능한 객체로 만듬
        return (i for i in (self._x, self._y))                  

# 객체 선언
v = VectorP(20,29)
print('ex1-1', v._x, v._y )

# 출력결과:
# line 26, in __init__
#    raise ValueError('Do Not Enter y value below 30')
# ValueError: Do Not Enter y value below 30

이를 통해서 확인 할 수 있는 것은 초기생성자(__init__)를 통해서 무조건 값을 받는 것이아닌 내가 원하는 범위의 값을 둘 수 있어요.

예를들어 성별이 남자인 집합의 클래스를 표시 할 수도 있고, 자동차를 기준이라면 차종을 세단으로 받을 수있어요.
하지만 이렇게 self로 받는 것은 그렇게 흔한 케이스는 아니에요.

코드가 무용지물 되는 케이스

기존 클래스를 그대로 가져올게요.

v = VectorP(20,0) # y값에 0을 넣으면 우리가 의도한 오류가 발생합니다.
print('ex1-1', v._x, v._y )

v = VectorP(20,40) 
print('ex1-2', v._x, v._y )

v._y = 10 
v = VectorP(20,40) # y값에 40이 할당되요. 
print('ex1-3', v._x, v._y )

v._y = 'abcde'
# 이렇게 되면 직접 인스턴스의 값을 바꾸어 우리가 짜둔 생성자의 if문이 무용지물이 되어 버려요.
# 심지어 우리가 의도한 실수형도 아니고 문자형으로 바뀌게 됩니다.

print('ex1-4',v._x, v._y)

# 출력 결과 
ex1-3:  20.0 40.0
ex1-4:  20.0 abcde

위와 같은 경우는 파이썬의 캡슐화에 위반되고 정보 은닉화에도 매우 좋지 않아요.


@property & @?.setter

앞서서 우리가 작성했던 코드가 무용지물이 되었는데요. 이를 해결 할 수 있는것중 하나가 property 데코레이터에요.

데코레이터 설명을 조그 하자면, 데코레이터가 있다는 것은 callback으로 선행해주는 함수가 먼저 동작한다는 것이에요. 즉, 여기 클래스 안에서 만들어지 property 데코레이터는 property 메서드를 선행하여 return값이 나오게 된다는 말이에요.

또한 데코레이터로 만들 메서드 이름은 인스턴스 변수명을 메서드 이름으로 지정해요. 여기선 __x를 x로 간단하게 만들게 되지요.

그리고 return 값으로 `self.__x'를 하게되구요.

class VectorP(object):
    def __init__(self, x, y):
        self.__x = float(x)
        if y < 30:
            raise ValueError('Do Not Enter y value below 30')
        self.__y = float(y)
    
    def __iter__(self): # iter 메소드를 이용하면 for문이나 next메소드로 출력이 가능한 객체로 만듬
        return (i for i in (self.__x, self.__y))

    @property  
    def x(self):  
        return self.__x
      
v = VectorP(20,40)
# print('ex1-5: ', v.__x) # 직접 접근시 오류 발생
# 출력결과 : 'VectorP' object has no attribute '__x'

print('ex1-6: ', v.x) # 참고할 것이 우리가 \__init\__안에 정의한 변수명으로 호출하면 안되요.
#데코레이터 이름으로 호출해야해요.
# 출력결과 : ex1-6:  20.0

앞에서 우리는 get 메소드를 이용해서 변수를 호출 할 수 있도록 만든건데요.
이제는 set! 설정 할 수 있는 방법을 살펴 본건데 이를 setter!라고 해요.

@함수명.setter라고 만들면되요.
메소드명은 데코레이터 명과 동일하고 더불어 해당 getter메서드의 이름과 같아야 해요.
바꾸고자 하는 변수 인수를 2번째 인자로 설정할게요.


class VectorP(object):
    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)
    
    def __iter__(self): 
        return (i for i in (self.__x, self.__y))

    @property 
    def x(self):      
        print('Called Property x')
        return self.__x

    @x.setter
    def x(self, v): 
        print('Called Property x setter')
        self.__x = v

v = VectorP(20,40)

v.x = {'name':'이혜성'} 
# 암묵적으로 하는 규칙임. v.__x = {'name':'이혜성'} 이렇게 하면 설정 되긴해요.

print('ex1-7: ', v.x) 
# 출력 결과값:
# Called Property x setter
# Called Property x
# ex1-7:  {'name': '이혜성'}

자!

class VectorP(object):
    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)
    
    def __iter__(self): # iter 메소드를 이용하면 for문이나 next메소드로 출력이 가능한 객체로 만듬
        return (i for i in (self.__x, self.__y))

    @property # 데코레이터가 있다는 것은 callback으로 선행해주는 함수가 먼저 동작한다는 것이에요. 즉, 여기서 property 데코레이터는 property 메서드를 선행하여 return값이 나오게되요.
    def x(self):       # 메서드 이름은 통상 변수 이름으로 하게되요.
        print('Called Property x')
        return self.__x

    @x.setter
    def x(self, v):
        print('Called Property x setter')
        self.__x = v

    @property 
    def y(self):
        if self.__y < 30:
            raise ValueError('Below 30 is not possible')
        print('Called Property y')
        return self.__y

    @y.setter 
    def y(self, v):
        if v < 30:
            raise ValueError('Below 30 is not possible')
        print('Called Property y setter')
        self.__y = float(v)
        
v = VectorP(10,40)
print('ex1-8',v.x,v.y)

v.y = 0 # 오류 발생
# 결과: line 106, in y
#    raise ValueError('Below 30 is not possible')
# ValueError: Below 30 is not possible

v = VectorP(10,0) # 30이하의 값이 처음 클래스 값의 인자로 넣고 인스턴스를 생성할때 오류가 발생하게 되요. property 메서도안에 if문을 설정해주었기 때문이조?
# 결과: line 101, in y
#    raise ValueError('Below 30 is not possible')
# ValueError: Below 30 is not possible
# Getter, Setter
print('EX1-10 -', dir(v), v.__dict__)

output

EX1-10 - ['_VectorP__x', '_VectorP__y', '__class__', '__delattr__',.....'x', 'y']

VectorPx, VectorPy가 만들어져 있는데요. 그건 생성자안에 변수를 만들 때 자동으로 __dict__에 추가되어 만들어지게 되요.

그리고 맨 마지막에는 'x', 'y'가 각각 있는데 property에 의해 생성되요.

또한 딕셔너리도 만들어져요. {'_VectorP\__x': 10.0, '_VectorP\__y': 40.0} 알아서 언더바 하나(_), 언더바 두개(__) 이렇게 말이지요.


__slot__

파이썬 인터프리터에게 통보하는 역할을 해요.
런타임에서 실행되고 해당 클래스가 가지는 속성을 제한함.

파이썬의 모든 클래스는 보통 인스턴스 속성을 가져요. 위에서 __dict__로 속성들이 관리되요. 헤쉬 값!(중복을 확인하기 위해서), 헤쉬 테이블 엔진을 사용하므로 메모리를 많이 잡아먹는 단점이 있어요.

그래서 클래스를 100개, 1000천, 10000개 선언해서 리스트에 담을때 그 때마다 딕셔너리가 생성되기 때문에 많은 데이터가 아니라면 문제가 되지 않지만 최적화하기 위해서는 이 딕셔너리는 램 문제가 조금 있는 자료 구조라는거!

그래서 __sloat__을 사용하여 __dict__ 속성을 최적화합니다.엄밀히 이야기하면 딕셔너리를 사용하지 않고 집합타입인 set으로 바꾸는 그런 과정이에요.
다수 객체 생성시 메모리 사용 공간을 대폭 감소시켜요.

해당 클래스에 만들어진 인스턴스 속성 관리에 딕셔너리 대신 set 형태를 사용함.

class TestA(object):
    __slots__ = ('a',) #  튜플 형태, 이름, 성별, 장학금 대상자 유무와 같이 그것만 사용해서 만듬

class TestB(object):
    pass # __slots__을 사용하지 않았기 때문에 해당 attribute를 모두 dictionary로 관리하게됨.

use_slot = TestA()
no_slot = TestB()
print('ex2-1', use_slot)
# print('ex2-2', use_slot.__dict__)
# ex2-2 출력결과 : AttributeError: 'TestA' object has no attribute '__dict__'

print('ex2-3', no_slot.__dict__)
# ex2-2 출력결과 : AttributeError: 'TestA' object has no attribute '__dict__'

제한을 두어 정확히 어떤 속성을 사용할지를 개발전에 미리 선정하고 작업을 하면 이런 `__slots__'을 사용하는 것도 매우 좋음.

하지만 운영하다보면 새로운 것들이 추가되는데요. 그때는 딕셔너리를 이용하는것이 유용하지만, 그때그때 추가해줄거라면 __slots__이 좋아요. 머신러닝과 같이 클래스를 많이 사용하는 패키지, 그런 류들의 프레임워크를 보면 대부분 __slots__을 사용하고 있어요.


객체 슬라이싱, 인덱싱

class Objects:
    def __init__(self):
        self._numbers = [n for n  in range(1, 1000, 3)]

    #def __len__(self): #len()메소드의 인자로 객체가 들어가 사용될 수 있게 커스터 마이징 할 수 있어요.
    #    return len(self._numbers)

    def __getitem__(self, idx): #객체를 리스트처럼 인덱싱하고 슬라이싱 가능하게 만들어줘요.
        return self._numbers[idx]
        
        
s = Objects()

print('ex3-1',s.__dict__ )
print('ex3-2',len(s) )

output
ex3-1 {'_numbers': [1, 4, 7, ..., 994, 997]}
ex3-2 333

만약 class안에 던더len 메소드를 구현하지 않고 len(s)를 호출하면 오륟가 발생합니다.
던더 len 메소드가 없더라도 아래와 같이 속성값에 바로 len메서드를 이용하면 크기를 확인 할 수 있어요.

```python
print('ex3-3',len(s._numbers))
# 결과: ex3-3 333		

Objects로 만든 클래스의 객체 s에 바로 슬라이싱도 가능해요.

print('ex3-4',s[1:100])

output

ex3-4 [4, 7, 10, ... ,298]
print('ex3-4',s[1:10])
print('ex3-5',s._numbers[1:10])

print('ex3-6',s[-1]) #
print('ex3-7',s._numbers[-1]) #

output

ex3-4 [4, 7, 10, 13, 16, 19, 22, 25, 28]
ex3-5 [4, 7, 10, 13, 16, 19, 22, 25, 28]
ex3-6 997
ex3-7 997

파이썬 추상클래스

참고 : https://docs.python.org/3/library/collections.abc.html

  • 자체적으로 객체 생성 불가
  • 상속을 통해서 자식 클래스에서 인스턴스를 생성해야함
  • 개발과 관련된 공통된 내용(필드, 메소드) 추출 및 통합해서 공통된 내용으로 작성하게 하는 것

휴대폰 -> 걸다, 끊다, 배터리 충전 -> 갤럭시s9, v30


sequence

아래 클래스를 우선 만들고 설명할게요.


class IterTestA():
    def __getitem__(self, idx):
        return range(1, 50, 2)[idx] # range(1,50,2)

i1 = IterTestA()
print('ex4-1', i1[4])
# 출력결과 : ex4-1 9

print('ex4-1', i1[4:10])
# ex4-2 range(9, 21, 2)

Sequence 상속 받지 않았지만, 자동으로 __iter__, __contain__(in연산자 사용시) 기능 작동 - > 왜? 현재 IterTestA클래스 안에 __getitem__ 메소드를 상속 받았기 때문에 파이썬에서 튜플이나, 인덱스에 접근할 때 호출되는 매직 메소드라는 점을 정상 참작하여 smart하게 해준거에요.

다시 말하지만, 파이썬 엔진이 객체 전체를 자동으로 조사 하여 시퀀스 프로토콜이 작동하여 __iter__, __contain__ 기능 작동까지 알아서 해주는 거에요.


class IterTestA():
    def __getitem__(self, idx):
        return range(1, 50, 2) # [idx]가 없다면?

i1 = IterTestA()
print('ex4-1', i1[4])
# 출력결과 : ex4-1 9

print('ex4-2', i1[4])
print('ex4-3', i1[4:10])

#  출력결과 : ex4-3 range(1, 50, 2)
#  출력결과 :ex4-4 range(1, 50, 2)

우리가 구현하지 않은 __contain__ 메소드가 있는지 없는지 확인해 볼게요.

print('ex4-4', 3 in i1[1:10])   # 요때는 구현하지 않은 __containe__이 사용됨
print('ex4-5', [i for i in i1][-1]) # 이 때는 구현하지 않은 __iter__가 사용됨

Sequence 상속

  • 요구사항인 추상 메서드를 모두 구현해야 동작
    즉, 추상메소드를 구현해야 진짜 클래스가 작동하는지 증명해 볼게요.

어떤 차이때문에 그럴까요? 바로 FM대로 상속 받았기 때문에 상속 이후에는 모두 필요한 기능들을 하나하나 작성해줘야 해요.

이번에는 위 예제와 다르게 Sequence클래스를 임포트할게요.

from collections.abc import Sequence

class IterTestB(Sequence):
    def __getitem__(self, idx):
        return range(1, 50, 2)[idx]
        
        

# 여기까지 IterTestA vs IterTestB의 차이점은 IterTestB에 Sequence가 인자값으로 들어간거에요.
i2 = IterTestB() 

위 소스코드를 실행하면 아래와 같이 오류가 떠요.

output

TypeError: Can't instantiate abstract class IterTestB with abstract methods __len__

그럼 까짓거 던더 len 메소드 말들어 볼게요.

from collections.abc import Sequence

class IterTestB(Sequence):
    def __getitem__(self, idx):
        return range(1, 50, 2)[idx] 

    def __len__(self, idx):
        return len(range(1, 50, 2)[idx])
    

    

# 여기까지 IterTestA vs IterTestB의 차이점은 IterTestB에 Sequence가 인자값으로 들어간거에요.
i2 = IterTestB() 

print('ex4-5', i2[4])
print('ex4-6', i2[4:10])
print('ex4-7', 3 in i2[4:10])   # 요때는 구현하지 않은 __containe__이 사용됨
print('ex4-8', [i for i in i2][-1])

output

ex4-5 9
ex4-6 range(9, 21, 2)
ex4-7 False
ex4-8 49

abc 활용예제

우리가 한번 추상클래스를 만들어 볼게요.
뽑기 기계를 한번 만들어 볼게요.

import abc

class RandomMachine(abc.ABC): 

    # 추상 메소드
    # 추상 메소드를 사용할 때 데코레이터는 
    @abc.abstractmethod
    def load(self, iterobj):
        '''Iterable 항목 추가'''
    
    # 추상메소드
    @abc.abstractmethod
    def pick(self, iterobj):
        '''무작위 항목 뽑기'''

    def inspect(self):
        items = [] # self가 아니조?
        while True:
            try:
                items.append(self.pick())  # self.pick()은 자식 클래스가 동작하겠조?
            except LookupError:
                break
            return tuple(sorted(items))

import random

# 추상메소드인 load와 pick은 반드시 자식에서 구현해야해요.왜? 부모에서는 이름만 있지 구현부가 없기 때문이조.
class CreanMachine(RandomMachine):
    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self, items):
        self._items.extend(items)        
        self._randomizer.shuffle(self._items)        
      
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('Empty Crane Box')
    
    def __call__(self):
        return self.pick()

# 서브 클래스 확인, 해당 부모의 자식인지 확인하는 메서드가 있어요.
print('ex5-1', issubclass(RandomMachine, CreanMachine)) # 랜덤머신이<- CraneMachine의 자식이니?
# 출력결과 : False

print('ex5-2', issubclass(CreanMachine, RandomMachine)) # CraneMachine이<- RandomMachine의 자식이니?
# 출력결과 : True

# 상속 구조 확인 메서드, 해당 클래스의 가계도를 보여줍니다.
print('ex5-3', CreanMachine.__mro__)
# 출력결과: (<class '__main__.CreanMachine'>, <class '__main__.RandomMachine'>, <class 'abc.ABC'>, <class 'object'>)

cm = CraneMachine(range(1,100)) # 추상 메서드 구현하지 않을 시 오류

만약 여기서 깜빡하고 자식 클래스에서 pick메서드를 구현하지 않았다고 해볼게요.(실제로 pick메소드 전부 주석해보세요) 그러 아래와 같은 오류 메시지가 나타날거에요.

TypeError: Can't instantiate abstract class CreanMachine with abstract methods pick

즉 부모클래스에서 작성한 추상 메소드는 반드시 자식클래스 안에서 구현하여야 오류가 발생하지 않아요.

부모 클래스만 보고도 자식클래스를 유추하거나 자식클래스를 보고도 부모 클래스를 유추하는 그런 클래스 네이밍 센스가 우선 필요하고 소스코드 작성이 필요합니다.

print('ex5-4', cm._items)

output

[87, 25, 37, ... 6, 35]

하나를 뽑아볼게요.

print('ex5-5', cm.pick())
# 출력결과: 35
print('ex5-6', cm())        # callable이 됬는지 확인 할 수 있어요. 던더 call 메서드 덕분에
print('ex5-7', cm.inspect()) # 파이썬의 매력! 부모에게 있는 메서드를 자식에서 사용할 수 있음

마무리

private

오늘도 많은걸 배웠는데요. private속성을 구현하고자, property데코레이터로 데이터를 은닉화(숨김)를 구현했고, 유효성검사(validation)도 했조? 30이하이면 오류 발생시키는 분기도 만들었으니깐요.

직접 접근하여 수정하는 걸 막기위해서 @?.setter데코레이터도 구현해보았어요.

slot

딕셔너리로 관리하는 모든 오브젝트에 어떤 속성값을 튜플로! 집합형, set 형식으로 관리하기 때문에 메모리의 절감을 이루어 낼 수 있었어요.많이 복사될 것으로 예상되는 객체는 slot 옵션을 넣어 주는 것이 거의 관례화 되어 있어요.

객체 슬라이싱, 인덱싱

간단해요. 던더 getitem메서드를 이용하여 객체를 마치 리스트 처럼 슬라이싱 하고, 인덱싱하여 사용했었조?

abc 추상화

즉 부모클래스에서 작성한 추상 메소드는 반드시 자식클래스 안에서 구현하여야 오류가 발생하지 않아요.

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글