Notice
Recent Posts
Recent Comments
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
10-11 00:15
Archives
Today
Total
관리 메뉴

Developer_Neo

[python] 연산자 오버로딩, 정보은닉과 , __dict__ 본문

프로그래밍/Python

[python] 연산자 오버로딩, 정보은닉과 , __dict__

_Neo_ 2022. 1. 20. 14:46
반응형

연산자 오버로딩

  • __add__(self, other) : + 연산자(A + B또는 A += B)
  • __iadd__(self, other) : + 연산자(A += B)
  • __sub__(self, other) : - 연산자(A - B, A -= B)
  • __isub__(self, other) : - 연산자(A -= B)
  • __mul__(self, other) :  * 연산자(A * B, A *= B)
  • __truediv__(self, other) :  / 연산자
  • __floordiv__(self, other) :  // 연산자
  • __mod__(self, other) : % 연산자
  • __pow__(self, other) : ** 연산자(pow(A, B))
  • __and__(self, other) :  and 연산자
  • __not__(self) :  not 연산자
  • __abs__(self) : 절대값 연산자(abs(A))

비교연산자 

  • __lt__(self, other) : < 연산자
  • __le__(self, other) : <= 연산자
  • __gt__(self, other) : > 연산자
  • __ge__(self, other) : >= 연산자 
  • __eq__(self, other) : == 연산자 
  • __ne__(self, other) : != 연산자 

와 추가적으로 이외의 것까지 여러가지가 있다.

 

우리는 몇가지만 살펴보자

 __add__(self, other) , __sub__(self, other) , __call__(self)

class Account:     # 계좌 클래스
    def __init__(self, aid, abl):
        self.aid = aid       # 계좌 번호
        self.abl = abl       # 계좌 잔액
        
    def __add__(self, m):    # 입금
        self.abl += m
        print('__add__')

    def __sub__(self, m):     # 인출
        self.abl -= m
        print('__sub__')

    def __call__(self):      # 계좌 상황을 문자열로 반환
        print('__call__')
        return str(self.aid) + ':' + str(self.abl)


def main():
    acnt = Account('James01', 100)     # 계좌 개설
    acnt + 100     # 100원 입금
    # acnt.__add__(100)
    
    acnt - 50      # 50원 인출
    # acnt.__sub__(50)
    
    print(acnt())
    # print(acnt.__call__())

main()
'''
출력
__add__
__sub__
__call__
James01:150
'''

위와 같이 계좌 클래스를  작성했다고 하자 그러면 내가 만든 클래스라는 것이다.

 

내가 만든 클래스에서 원하는 더하기, 빼기, 문자열로 반환하는 연산자를 만들기 위해서는 오버로딩을 해야한다.

 

그래서 __add__메소드를 할때에 인자로 (self,m)을 받고 진행하고

 

__sub__메소드를 할때에 인자로 (self, m)을 받고 진행하고

 

__call__메소드를 할때에 인자로 (self)를 받고 진행한다

 

__call__메소드는 문자열을 반환하도록 해야한다.

 

그런데 추가적으로 드는 생각은 이게 어떻게 보면 오버라이딩이 아닌가하는 생각이 들었다.

 

왜냐하면 매개변수의 유형이 같았더라면.. 오버로딩은 아니기 때문이다.

 

 

오버로딩(Overloading) : 같은 이름의 메서드 여러개를 가지면서 매개변수의 유형과 개수가 다르도록 하는 기술

오버라이딩(Overriding) : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용

 

그런데 위에서 메소드를 진행할때에 단순히 덧셈, 뺄샘을 하는 것이 아닌 새로운 객체를 생성해서 반환할 수도 있다.

class Vector:     
    def __init__(self, x, y):
        self.x = x       
        self.y = y       
        
    def __add__(self, o):    # 벡터의 덧셈 연산
        return Vector(self.x + o.x, self.y + o.y)
 
    def __call__(self):      # 벡터 정보를 문자열로 반환
        return 'Vector({0}, {1})'.format(self.x, self.y)


def main():
    v1 = Vector(3, 3)
    v2 = Vector(7, 7)
    v3 = v1 + v2
    print(v1())
    print(v2())
    print(v3())

위의 코드와 같이 __add__메소드의 반환값으로 객체를 생성해서 보내면 된다.

 

그리고 call메소드를 사용되는 경우는 생성된 객체의 이름 다음에 중괄호를 붙여서 전달하면 된다.

 


__str__ 메소드: 문자열을 반환하도록 정의해야한다. 

그리고 반환하는 문자열은 해당 객체의 정보를 답고 있어야한다.

 

예로 살펴보자

class Vector:     
    def __init__(self, x, y):
        self.x = x       
        self.y = y       
        
    def __add__(self, o):    # 벡터의 덧셈 연산
        return Vector(self.x + o.x, self.y + o.y)
 
    def __str__(self):      # 벡터 정보를 문자열로 반환
        return 'Vector({0}, {1})'.format(self.x, self.y)


def main():
    v1 = Vector(3, 3)
    v2 = Vector(7, 7)
    v3 = v1 + v2
    print(v1)
    print(v2)
    print(v3)

main()

'''
Vector(3, 3)
Vector(7, 7)
Vector(10, 10)
'''

str메소드의 경우 그냥 객체이름만 print함수의 인자로 전달하면 알아서 호출이 된다.

 

 

내가 만든 클래스를 선언했을때 이것에 해당하는 객체가 immutable객체인 경우

class Vector:     
    def __init__(self, x, y):
        self.x = x       
        self.y = y       
        
    def __add__(self, o):    # 벡터의 덧셈 연산
        return Vector(self.x + o.x, self.y + o.y)
 
    def __str__(self):      # 벡터 정보를 문자열로 반환
        return 'Vector({0}, {1})'.format(self.x, self.y)


def main():
    v1 = Vector(2, 2)
    v2 = Vector(7, 7)

    print(v1, id(v1))
    v1 += v2       # v1 = v1.__add__(v2)
    print(v1, id(v1))

main()

'''
결과
Vector(2, 2) 2289943716000
Vector(9, 9) 2289943719024
'''

위의 결과로 보았을때 +=연산에 대해서 진행했을때 __add__ 메소드 호출로 인해 v1에 대한 것이 새롭게 생성된 것을 볼 수 있다 

 

따라서 우리가 위의 코드로 만든 Vector클래스는 immutable한 것이 되는 것이다.

 

여기에서 나는 mutable한 것으로 만들고 싶어! 라고 한다면 +=연산을 했을 때 id()함수로 나타낸 정보가 같으면 된다.

 

이때 사용하는 것은 __iadd__메소드이다. 이것은 +=연산을 담당하게 된다.

 

그래서 +연산과 +=연산에 대한 메소드를 각각 정의해주어야한다.

class Vector:     
    def __init__(self, x, y):
        self.x = x       
        self.y = y       
        
    def __add__(self, o):    # 벡터의 + 연산
        return Vector(self.x + o.x, self.y + o.y)

    def __iadd__(self, o):    # 벡터의 += 연산
        self.x += o.x
        self.y += o.y
        return self
 
    def __str__(self):      # 벡터 정보를 문자열로 반환
        return 'Vector({0}, {1})'.format(self.x, self.y)


def main():
    v1 = Vector(2, 2)
    v2 = Vector(7, 7)

    print(v1, id(v1))     # v1과 v1에 저장된 객체의 주소 정보 출력
    v1 += v2       # v1 = v1.__iadd__(v2)
    print(v1, id(v1))     # v1과 v1에 저장된 객체의 주소 정보 출력

main()

'''
Vector(2, 2) 2289943718976
Vector(9, 9) 2289943718976
'''

따라서 mutable한 객체의 성격으로 두고자 하면 +=연산에 대한 결과가 달라져야하니 __iadd__메소드를 추가적으로 정의해주면 된다.

 

정리

__add__메소드만 정의했을 경우 : + , +=연산에 사용

__iadd__메소드 , __add__메소드 두개 정의했을 경우 : += , + 연산 각각을 담당하게 된다.

 

+ __isub__메소드 또한 정의할 수 있다 ( -=연산 )

 

 


정보은닉

객체 외부에서 객체 내에 있는 변수에 직접 접근못하도록 하는 것이다

 

왜냐하면 바꾸어지면 안되는 정보(ex) 사람의 고유번호... )가 있는데 코드를 짤 때에 실수가 발생해 바뀌어질 수도 있기

 

때문에 정보은닉이라는 것이 쓰인다.

 

방법

1.  __변수 (언더바 2개사용)

2. _변수   (언더바 1개 사용)

 

 

__변수 (언더바 2개사용)경우를 먼저 보자

class Person:
    def __init__(self, n, a):
        self.__name = n       # 이름 정보
        self.__age = a       # 나이 정보

    def add_age(self, a):
        if(a < 0):
            print('나이 정보 오류')
        else:
            self.__age += a

    def __str__(self):
        return '{0}: {1}'.format(self.__name, self.__age)


def main():
    p = Person('James', 22)       # 22살의 James
    # p.__age += 1     # 이 문장을 실행하면 오류가 발생함
    p.add_age(1)
    print(p)

변수이름을 __name, __age로 설정하였는데 여기서 발생하는 것이 있다.

 

그것은 객체.변수이름으로 접근하는 것이 불가능 해진다는 것이다.

 

즉 외부접근을 강제로 막는다는 것이다.

 

그런데 프로그래머가 객체 내에 있는 변수를 접근 안하면 되는데 굳이 외부접근을 강제로 막는 필요가 있을까라는 의문

 

점에 해결방법이라고하기에 좀 그런 방법 하나가 더 있다. 그것은 언더바 1개를 사용하되 규칙을 두는 것이다.

 

 

_변수   (언더바 1개 사용) 의 경우

 

규칙

  • 객체 내 변수 이름 앞에 언더바를 하나만 붙이면 이 변수에 접근하지 말자
  • 언더바 하나가 앞에 붙어있는 변수에 접근하면 규칙을 어긴것이다.
class Person:
    def __init__(self, n, a):
        self._name = n       # 이름 정보
        self._age = a       # 나이 정보

    def add_age(self, a):
        if(a < 0):
            print('나이 정보 오류')
        else:
            self._age += a

    def __str__(self):
        return '{0}: {1}'.format(self._name, self._age)


def main():
    p = Person('James', 22)       # 22살의 James
    # p._age += 1     # 이렇게 안쓰기로 약속했다.
    p.add_age(1)
    print(p)

main()

2개의 규칙에 의하여  p._age 와 같은 코드는 쓰지 않기로 했으니 쓰면 안된다 그래서 메소드로만 접근을 해야한다.

 

 


__dict__ 메소드

- 객체 내에는 해당 객체의 변수 정보를 담고 있는 딕셔너리가 하나 존재한다.

 

그 딕셔너리는 객체가 하나씩 생성될 때마다 각각 하나씩 가지고 있게 된다.

 

우리가 따로 __dict__메소드를 정의할 필요는 없으며 그냥 이 메소드를 쓰기만 한다.

 

예로 봐보자

class Person:
    def __init__(self, n, a):
        self._name = n       # 이름 정보
        self._age = a       # 나이 정보

def main():
    p = Person('James', 22)       # 22살의 James
    print(p.__dict__)

main()

'''
결과
{'_name': 'James', '_age': 22}
'''

위의 코드로 본대로 객체.__dict__사용한다.

 

객체.__dict__는 반환 값을 딕셔너리형태로 전달 해준다. 

 

그리고 추가적으로 객체.__dict__ 딕셔너리형태로 반환되니 사전자료형처럼 자료 수정이 가능하다.

 

객체.__dict__[key] = 수정할 값 , 객체.__dict__[key] += 더할 값 의 형태로 사용이 가능하다

 

class Simple:
    def __init__(self, n, s):
        self._n = n       # 단순 정수
        self._s = s       # 단순 문자열

    def __str__(self):
        return '{0}: {1}'.format(self._n, self._s)


def main():
    sp = Simple(10, 'my')
    print(sp)

    sp.__dict__['_n'] += 10
    sp.__dict__['_s'] = 'your'
    print(sp)

main()

'''
10: my
20: your
'''

위의 코드 처럼 가능하다는 것이다.

 

그런데 위의 코드들을 보면 변수이름에 있어서 언더바 하나만 붙인 것을 알 수 있다.

 

만약 언더바를 2개 붙였을때의 __dict__메소드의 출력 값은 어떻게 될까?

 

class Person:
    def __init__(self, n, a):
        self.__name = n       # 이름 정보
        self.__age = a       # 나이 정보

def main():
    p = Person('James', 22)       # 22살의 James
    print(p.__dict__)

main()

'''
결과
{'_Person__name': 'James', '_Person__age': 22}
'''

사전자료형으로 출력 된다는 것은 같지만 key값에 있어서 변수이름을 출력을 할때에 __name만 출력하는 것이 아닌 

 

_클래스이름 + 변수이름 형태로 출력이 된다는 것이다.

 

따라서 언더바를 2개 붙였을때의 __dict__메소드의 출력 값은 사전자료형의 key값_ClassName__AttriName형태로 출력이 된다는 것을 알아두자.

 

 


우리가 아까 객체가 하나씩 생성될 때마다 각각 하나씩 가지고 있게 된다. 라고 말했던걸 기억해보자

 

이렇게 된다면 객체가 엄청나게 많이 생성이 된다면 딕셔너리가 엄청많이 생기게 된다. 그래서 이것이 문제라는 것이다.

 

이게 왜 문제가 되냐!

 

딕셔너리는 리스트나 튜플에 비해 메모리 사용량이 많다 왜냐하면 키를 이용해서 값을 바로얻을 수 있또록 하기 위해서

 

파이썬이 더 많은 정보를 유지하기 때문이다.

 

이렇기 때문에 너무나 많은 딕셔너리는 시스템에 부담을 준다는 것이다.

 

그러면 이 부담을 줄여주는 방법은 무엇일까?

 

부담을 줄이기 위한 방법은 

 

__slots__ 라는 것을 사용한다는 것이다.

 

__slots__ 의 효과

- __slots__ 라는 것을 사용하게 되면 이것이 사용된 클래스를 기반으로 생성한 객체의 변수는 ?,?,?로 제한한다 라는 것이다.

 

예를 통해서 알아보자

class Point3D:
    __slots__ = ('x', 'y', 'z')
    
    def __init__(self, x, y, z):
        self.x = x       # x 좌표
        self.y = y       # y 좌표
        self.z = z       # z 좌표
    def __str__(self):
        return '({0}, {1}, {2})'.format(self.x, self.y, self.z)


def main():
    p1 = Point3D(1, 1, 1)      # 3차원 좌표상의 한 점
    p2 = Point3D(24, 17, 31)       # 3차원 좌표상의 한 점
    print(p1)
    print(p2)

위의 코드 처럼 하게 된다면 Point3D를 기반으로 생성한 객체의 변수는 x, y, z로 제한한다 라는 뜻이다.

 

이렇게 된다면 객체.변수 = 값 으로 생성되는 클래스의 변수가 없다는 것이다 즉 변수의 갯수를 제한한다.

 

그리고 __slots를 통해 제한하면 객체별로 딕셔너리가 생기지 않는다. 또한 객체별로 __slots__를 하나씩 갖는 것도 아니다.

 

즉 클래스당 하나의 __slots__ 와 하나의 __dict__이자 사전자료형을 갖는다라는 것이다 마치 static처럼!

 

그래서 상황에 따라서 매우 큰 메모리 상의 이득을 볼 수 있다.

 

참고한 책 : 윤성우의 열혈파이썬 중급편

 

반응형
Comments