HiddenBeginner

안녕하세요. 방문해 주셔서 감사합니다. 아직 실력이 많이 부족하지만 언젠가 고수 반열에 들어서 이 세상에 많은 기여를 하겠습니다.

[Python] 파이썬 잡학사전

22 Feb 2022 » Python

지금까지 인공지능 및 데이터 과학 분야를 공부하면서 생각보다 많은 파이썬 지식이 필요하지는 않았다. 하지만, 잘 만들어진 깃헙 레포지토리의 코드를 가져와서 내 연구에 맞게 코드를 수정하는 일이 늘어나면서 깊이 있는 파이썬 공부가 필요하다고 느꼈다. 이제 논문을 작성하면 내 코드가 만천하에 공개될텐데, 내 코드를 읽는 사람이 나를 친절한 연구자 또는 인간 세계에 내려온 천사라고 여길 정도로 코드를 짜고 싶다. 지금은 Effective Python 2nd 책을 읽으며 그때 그때 유익한 정보들을 찾아 정리하고 있다.


목차



클래스를 상속 받을 때 super().__init__()이 필요한 경우: 부모 클래스의 __init__ 메서드를 오버라이딩 할 때

class Parent:
    def __init__(self):
        self.x = "{}는 Attribute x를 갖고 있습니다.".format(self.__class__.__name__)
      
    
class CaseA(Parent):
    """
    부모 클래스의 __init__ 메서드를 오버라이딩 하지 않는 경우
    """
    def no_init(self):
        return None
    
    
class CaseB(Parent):
    """
    부모 클래스의 __init__ 메서드를 오버라이딩할 때 super().__init__()을 사용을 사용한 경우
    """
    def __init__(self):
        super().__init__()
        
        
class CaseC(Parent):
    """
    부모 클래스의 __init__ 메서드를 오버라이딩할 때 super().__init__()을 사용을 사용하지 않는 경우
    """
    def __init__(self):
        return None
        
        
parent = Parent()
caseA = CaseA()
caseB = CaseB()
caseC = CaseC()

print(parent.x)
print(caseA.x)
print(caseB.x)
print(caseC.x)
Parent는 Attribute x를 갖고 있습니다.
CaseA는 Attribute x를 갖고 있습니다.
CaseB는 Attribute x를 갖고 있습니다.



---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-1-42b3dbe2aae1> in <module>
     36 print(caseA.x)
     37 print(caseB.x)
---> 38 print(caseC.x)


AttributeError: 'CaseC' object has no attribute 'x'



비공개 속성 (Private attribute)

누군가가 내가 만든 클래스를 사용할 때 접근하지 못했으면 하는 속성 (attribute)가 있을 수 있다. 속성의 이름 앞에 언더바 2개를 붙이면 비공개 속성을 만들 수 있으며, 비공개 속성은 인스턴스를 통해 접근할 수 없다. 일단은. 아래 코드를 실행해보자 foo.__private_field를 실행하면 그런 속성은 없다고 에러 메세지를 보내온다. 이렇게 만든 속성은 자동완성탭에서도 보이지 않는다.

class MyObject:
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

        
foo = MyObject()
print(foo.public_field)
print(foo.__private_field)
5



---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-2-6fcabe9fd5a7> in <module>
      7 foo = MyObject()
      8 print(foo.public_field)
----> 9 print(foo.__private_field)


AttributeError: 'MyObject' object has no attribute '__private_field'


사실 이런 비공개 속성은 마음만 먹으면 다 접근할 수 있다. 이름은 비공개 속성이지만 다 알 수 있는 모순적인 친구이다. 클래스의 __dict__ 속성에 접근해보자. _MyObject__private_field 키에 값 10이 있는 것을 확인할 수 있다.

print(foo.__dict__)
{'public_field': 5, '_MyObject__private_field': 10}


따라서 다음과 같이 비공개 속성에 접근할 수 있다.

print(foo._MyObject__private_field)
10


그럼 도대체 왜 이를 비공개 속성이라고 부를까? 사실 파이썬에서 완전한 비공개 속성을 만드는 것을 굉장히 어렵다고 한다. 이는 파이썬이 만들어진 철학과 관련이 있는데, 코드를 꽁꽁 감춰놓기보다는 공개해서 널리 이롭게 하자 이런 마인드인 것 같다. 비공개 속성을 사용하는 순간 누군가 나의 코드에서 에러가 발생할 때 디버깅하기 어려울 것이다. 다른 이유로는 비공개 속성은 상속이 되지 않는다는 점이 있다. 이 역시 누군가 내 클래스를 상속해서 사용하고 싶을 때, 비공개 속성은 상속 받지 못하여 원활한 코드 사용을 저해할 수 있다.

class MyBaseClass:
    def __init__(self, value):
        self.__value = value
        
    def get_value(self):
        NotImplementedError
    
    
class MyIntegerClass(MyBaseClass):
    def __init__(self, value):
        super().__init__(value)
        
    def get_value(self):
        return int(self.__value)
    
foo = MyIntegerClass(5)
print(foo.get_value())
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-5-c8aa6443195a> in <module>
     15 
     16 foo = MyIntegerClass(5)
---> 17 print(foo.get_value())


<ipython-input-5-c8aa6443195a> in get_value(self)
     12 
     13     def get_value(self):
---> 14         return int(self.__value)
     15 
     16 foo = MyIntegerClass(5)


AttributeError: 'MyIntegerClass' object has no attribute '_MyIntegerClass__value'


그 이유는 __value 속성은 부모 클래스인 MyBaseClass__init__ 메서드에서 정의되었기 때문이다. 마찬가지로 foo.__dict__을 출력해보자. _MyBaseClass__value 키에 값 5가 저장되어 있는 것을 확인할 수 있다. 따라서 get_value 메서드에서 부모 클래스의 __value에 접근하고 싶다면, self._MyBaseClass__value로 접근해야 할 것이다. 혹여라도 부모 클래스 이름이 바뀌면 자식 클래스에서 호출을 위해 사용한 코드를 모두 수정해야할 것이다. 시간적 여유가 있으면 self.__value 대신 self.value를 사용했을 경우에는 foo.__dict__의 출력값이 어떻게 달라지는지 한번 살펴보길 바란다.

print(foo.__dict__)
{'_MyBaseClass__value': 5}


아무튼 비공개 속성은 최대한 사용하지 않는 것이 파이썬 코딩 관습이라고 한다. 나는 자동완성탭에 눈에 보이는 속성들을 줄이기 위하여 비공개 속성을 많이 사용했었는데 이 책을 읽으면서 창피함에 얼굴이 빨개졌다. 앞으로 주의해야겠다. 한편, 코드 제작자가 비공개 속성처럼 다루고 싶은 속성은 앞에 언더바 하나를 쓰는 관습이 있다고 한다 (self._protect_field). 앞에 언더바를 포함한 속성은 코딩할 때 클래스 내부적으로만 사용할 목적 (internal API)으로 만든 속성이며 사용자는 굳이 접근하지 않아도 된다는 의미를 포함하고 있다고 한다.



@property@property.setter 데코레이터

파이썬에서는 보통 다음과 같이 객체의 속성 (attribute)에 접근하거나 값을 새롭게 할당한다.

class MyClass:
    def __init__(self, sequence):
        self.sequence = sequence
   

foo = MyClass([1, 2, 3, 4, 5])
# sequence 속성 접근
print("sequence 속성: ", foo.sequence)

# sequence 속성 할당
foo.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("sequence 속성: ", foo.sequence)
sequence 속성:  [1, 2, 3, 4, 5]
sequence 속성:  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


때론 객체의 속성에 접근하거나 값을 할당할 때 단순히 데이터를 주고 받는 것을 넘어서 더 많은 것을 수행하고 싶을 수 있다. 예를 들어, 위의 sequence 속성에 있는 원소의 개수를 나타내는 num_elements 속성을 만들고 싶다고 하자. 생각할 수 있는 가장 단순한 방법은 다음과 같이 __init__ 함수에서 num_elements 속성을 만드는 것이다.

class MyClass:
    def __init__(self, sequence):
        self.sequence = sequence
        self.num_elements = len(self.sequence)
   

foo = MyClass([1, 2, 3, 4, 5])
# sequence 속성 접근
print("num_elements 속성: ", foo.num_elements)

# sequence 속성 할당
foo.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("num_elements 속성: ", foo.num_elements)
num_elements 속성:  5
num_elements 속성:  5


하지만, sequence가 새로운 리스트로 바뀌어도 num_elements 속성에는 이전에 연산된 값이 그대로 저장되어 있다. 다음으로 생각해볼 수 있는 방법은 num_elements라는 메서드가 호출될 때 sequence의 길이를 계산하는 것이다.

class MyClass:
    def __init__(self, sequence):
        self.sequence = sequence

    def num_elements(self):
        return len(self.sequence)
   

foo = MyClass([1, 2, 3, 4, 5])
# sequence 속성 접근
print("num_elements 메서드 출력값: ", foo.num_elements())

# sequence 속성 할당
foo.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("num_elements 메서드 출력값: ", foo.num_elements())
num_elements 메서드 출력값:  5
num_elements 메서드 출력값:  10


원하는 결과가 출력되기는 하지만 num_elements가 메서드라는 것이 마음에 들지 않는다. 뒤에 괄호 ()를 써줘야만 한다. @property 데코레이터를 사용하면 우리가 원하는 목적을 이룰 수 있다. @property 데코레이터와 함께 원하는 이름으로 메서드를 만들면, 그 메서드를 속성처럼 사용할 수 있다.

class MyClass:
    def __init__(self, sequence):
        self.sequence = sequence

    @property
    def num_elements(self):
        return len(self.sequence)
   

foo = MyClass([1, 2, 3, 4, 5])
# sequence 속성 접근
print("num_elements 속성: ", foo.num_elements)

# sequence 속성 할당
foo.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("num_elements 속성: ", foo.num_elements)
num_elements 속성:  5
num_elements 속성:  10


위에서 객체.num_elements를 호출하면 num_elements 메서드가 호출된다. 속성에 접근하는 것이기 때문에 뒤에 괄호를 써주지 않아도 된다. @property를 사용해서 속성에 접근하는 메서드를 getter 메서드라고도 부른다. 지금까지 속성에 접근할 때 원하는 연산을 하는 예시를 살펴보았다. 다음으로 속성에 값을 할당할 때, 원하는 연산을 할 수 있게 만들어보자. 예를 들어, sequence 속성에 리스트 자료형만 할당할 수 있도록 예외처리를 만들어보겠다.

class MyClass:
    def __init__(self, sequence):
        self.sequence = sequence
        
    @property
    def sequence(self):
        return self._sequence
        
    @sequence.setter
    def sequence(self, sequence):
        if not isinstance(sequence, list):
            raise ValueError(f"sequence must be list type; got {type(sequence)}")
        self._sequence = sequence
   
foo = MyClass([1, 2, 3, 4, 5])
# sequence 속성 접근
print("sequence 속성: ", foo.sequence)

# sequence 속성 할당
foo.sequence = 100.0
print("sequence 속성: ", foo.sequence)
sequence 속성:  [1, 2, 3, 4, 5]



---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-11-e7710084d63f> in <module>
     18 
     19 # sequence 속성 할당
---> 20 foo.sequence = 100.0
     21 print("sequence 속성: ", foo.sequence)


<ipython-input-11-e7710084d63f> in sequence(self, sequence)
     10     def sequence(self, sequence):
     11         if not isinstance(sequence, list):
---> 12             raise ValueError(f"sequence must be list type; got {type(sequence)}")
     13         self._sequence = sequence
     14 


ValueError: sequence must be list type; got <class 'float'>


foo.sequence = 100.0와 같은 속성 할당이 발생할 때 @sequence.setter의 메서드가 호출된다. @sequence.setter 같은 setter를 사용하기 위해서는 먼저 @property를 사용하여 만들어줘야 한다. 이렇게 작성하면 좋은 점이 __init__ 메서드에 있는 self.sequence = sequence를 실행할 때도 setter 메서드가 호출되면서 입력값 sequence가 리스트인지 아닌지 확인한다. 즉, 객체를 선언할 때 입력 받는 값의 자료형을 검증할 수 있다.

foo = MyClass(100)
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-12-cce4024098a0> in <module>
----> 1 foo = MyClass(100)


<ipython-input-11-e7710084d63f> in __init__(self, sequence)
      1 class MyClass:
      2     def __init__(self, sequence):
----> 3         self.sequence = sequence
      4 
      5     @property


<ipython-input-11-e7710084d63f> in sequence(self, sequence)
     10     def sequence(self, sequence):
     11         if not isinstance(sequence, list):
---> 12             raise ValueError(f"sequence must be list type; got {type(sequence)}")
     13         self._sequence = sequence
     14 


ValueError: sequence must be list type; got <class 'int'>

이렇게 @property@property.setter를 사용할 때 관용상 주의할 점이 있다. 먼저, 두 메서드 안에서는 최대한 관련있는 속성만 다뤄줘야 한다. 특히, @property 메서드 안에서는 속성들의 값을 바꾸는 행위를 자제해야 한다. 어떤 속성 A에 접근했을 뿐인데 다른 속성 B가 바뀌어버리면 사용자 입장에서는 영문도 모른채 데이터가 바뀌어 버리는 것이 된다. 그리고 두 메서드는 속성처럼 사용되기 때문에 그만큼 이해하기 쉽고 실행이 빨라야 한다. 속성에 접근/할당할 뿐인데 파일 입출력이 실행된다거나, 다른 라이브러리를 불러온다거나, 무거운 데이터베이스 처리를 한다거나 하는 것은 없어야 할 것이다.



불쌍한 대학원생에게 커피 한 잔 사주기

댓글