개발자/파이썬 Python

파이선 클래스 기초 3. 리얼파이선 20

지구빵집 2022. 3. 15. 09:29
반응형

 

 

파이선 클래스 기초 3. 리얼파이선 20 

 

상속

 

물론, 상속을 지원하지 않는다면 언어 기능은 《클래스》라는 이름을 붙일만한 가치가 없을 것입니다. 파생 클래스 정의의 문법은 이렇게 생겼습니다:

 

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

 

이름 BaseClassName 은 파생 클래스 정의를 포함하는 스코프에 정의되어 있어야 합니다. 베이스 클래스 이름의 자리에 다른 임의의 표현식도 허락됩니다. 예를 들어, 베이스 클래스가 다른 모듈에 정의되어 있을 때 유용합니다:

 

class DerivedClassName(modname.BaseClassName):

 

파생 클래스 정의의 실행은 베이스 클래스와 같은 방식으로 진행됩니다. 클래스 객체가 만들어질 때, 베이스 클래스가 기억됩니다. 이것은 어트리뷰트 참조를 결정할 때 사용됩니다: 요청된 어트리뷰트가 클래스에서 발견되지 않으면 베이스 클래스로 검색을 확장합니다. 베이스 클래스 또한 다른 클래스로부터 파생되었다면 이 규칙은 재귀적으로 적용됩니다.

 

파생 클래스의 인스턴스 만들기에 특별한 것은 없습니다: DerivedClassName() 는 그 클래스의 새 인스턴스를 만듭니다. 메서드 참조는 다음과 같이 결정됩니다: 대응하는 클래스 어트리뷰트가 검색되는데, 필요하면 베이스 클래스의 연쇄를 타고 내려갑니다. 이것이 함수 객체를 준다면 메서드 참조는 올바릅니다.

 

파생 클래스는 베이스 클래스의 메서드들을 재정의할 수 있습니다. 메서드가 같은 객체의 다른 메서드를 호출할 때 특별한 권한 같은 것은 없으므로, 베이스 클래스에 정의된 다른 메서드를 호출하는 베이스 클래스의 메서드는 재정의된 파생 클래스의 메서드를 호출하게 됩니다. (C++ 프로그래머를 위한 표현으로: 파이썬의 모든 메서드는 실질적으로 virtual 입니다.)

 

파생 클래스에서 재정의된 메서드가, 같은 이름의 베이스 클래스 메서드를 단순히 갈아치우기보다 사실은 확장하고 싶을 수 있습니다. 베이스 클래스의 메서드를 직접 호출하는 간단한 방법이 있습니다: 단지 BaseClassName.methodname(self, arguments) 를 호출하면 됩니다. 이것은 때로 클라이언트에게도 쓸모가 있습니다. (이것은 베이스 클래스가 전역 스코프에서 BaseClassName 으로 액세스 될 수 있을 때만 동작함에 주의하세요.)

 

파이썬에는 상속과 함께 사용할 수 있는 두 개의 내장 함수가 있습니다:

 

  • 인스턴스의 형을 검사하려면 isinstance() 를 사용합니다: isinstance(obj, int) 는 obj.__class__ 가 int 거나 int 에서 파생된 클래스인 경우만 True 가 됩니다.
  • 클래스 상속을 검사하려면 issubclass() 를 사용합니다: issubclass(bool, int) 는 True 인데, bool 이 int 의 서브 클래스이기 때문입니다. 하지만, issubclass(float, int) 는 False 인데, float 는 int 의 서브 클래스가 아니기 때문입니다.

 

다중 상속

 

파이썬은 다중 상속의 형태도 지원합니다. 여러 개의 베이스 클래스를 갖는 클래스 정의는 이런 식입니다:

 

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

 

대부분의 목적상, 가장 간단한 경우에, 부모 클래스로부터 상속된 어트리뷰트들의 검색을 깊이 우선으로, 왼쪽에서 오른쪽으로, 계층 구조에서 겹치는 같은 클래스를 두 번 검색하지 않는 것으로 생각할 수 있습니다. 그래서, 어트리뷰트가 DerivedClassName 에서 발견되지 않으면, Base1 에서 찾고, 그다음 (재귀적으로) Base1 의 베이스 클래스들을 검색합니다. 거기에서도 발견되지 않으면, Base2 에서 찾고, 이런 식으로 계속합니다.

 

사실, 이것보다는 약간 더 복잡합니다; 메서드 결정 순서는 super() 로의 협력적인 호출을 지원하기 위해 동적으로 변경됩니다. 이 접근법은 몇몇 다른 다중 상속 언어들에서 call-next-method 라고 알려져 있고, 단일 상속 언어들에서 발견되는 super 호출보다 더 강력합니다.

 

동적인 순서가 필요한 이유는, 모든 다중 상속의 경우는 하나나 그 이상의 다이아몬드 관계 (적어도 부모 클래스 중 하나가 가장 바닥 클래스들로부터 여러 경로를 통해 액세스 되는 경우) 를 만들기 때문입니다. 예를 들어, 모든 클래스는 object 를 계승하기 때문에, 모든 다중 상속은 object 에 이르는 여러 경로를 제공합니다. 베이스 클래스들이 여러 번 액세스 되지 않게 하려고, 동적인 알고리즘이 검색 순서를 선형화하는데, 각 클래스에서 지정된 왼쪽에서 오른쪽으로 가는 순서를 보존하고, 각 부모를 오직 한 번만 호출하고, 단조적 (부모들의 우선순위에 영향을 주지 않으면서 서브 클래스를 만들 수 있다는 의미입니다) 이도록 만듭니다. 모두 함께 사용될 때, 이 성질들은 다중 상속으로 신뢰성 있고 확장성 있는 클래스들을 설계할 수 있도록 만듭니다. 더 자세한 내용을 참고하세요.

 

비공개 변수

 

객체 내부에서만 액세스할 수 있는 《비공개》 인스턴스 변수는 파이썬에 존재하지 않습니다. 하지만, 대부분의 파이썬 코드에서 따르고 있는 규약이 있습니다: 밑줄로 시작하는 이름은 (예를 들어, _spam) API의 공개적이지 않은 부분으로 취급되어야 합니다 (그것이 함수, 메서드, 데이터 멤버중 무엇이건 간에). 구현 상세이고 통보 없이 변경되는 대상으로 취급되어야 합니다.

 

클래스-비공개 멤버들의 올바른 사례가 있으므로 (즉 서브 클래스에서 정의된 이름들과의 충돌을 피하고자), 이름 뒤섞기 (name mangling) 라고 불리는 메커니즘에 대한 제한된 지원이 있습니다. __spam 형태의 (최소 두 개의 밑줄로 시작하고, 최대 한 개의 밑줄로 끝납니다) 모든 식별자는 _classname__spam 로 텍스트 적으로 치환되는데, classname 은 현재 클래스 이름에서 앞에 오는 밑줄을 제거한 것입니다. 이 뒤섞기는 클래스 정의에 등장하는 이상, 식별자의 문법적 위치와 무관하게 수행됩니다.

 

이름 뒤섞기는 클래스 내부의 메서드 호출을 방해하지 않고 서브 클래스들이 메서드를 재정의할 수 있도록 하는 데 도움을 줍니다. 예를 들어:

 

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

 

위의 예는 MappingSubclass가 __update 식별자를 도입하더라도 작동합니다. Mapping 클래스에서는 _Mapping__update로 MappingSubclass 클래스에서는 _MappingSubclass__update로 각각 대체 되기 때문입니다.

 

뒤섞기 규칙은 대체로 사고를 피하고자 설계되었다는 것에 주의하세요; 여전히 비공개로 취급되는 변수들을 액세스하거나 수정할 수 있습니다. 이것은 디버거와 같은 특별한 상황에서 쓸모 있기조차 합니다.

 

exec() 나 eval() 로 전달된 코드는 호출하는 클래스의 클래스 이름을 현재 클래스로 여기지 않는다는 것에 주의하세요; 이것은 global 문의 효과와 유사한데, 효과가 함께 바이트-컴파일된 코드로 제한됩니다. 같은 제약이 __dict__ 를 직접 참조할 때뿐만 아니라, getattr(), setattr(), delattr() 에도 적용됩니다.

 

잡동사니

 

때로 몇몇 이름 붙은 데이터 항목들을 함께 묶어주는 파스칼의 《record》 나 C의 《struct》 와 유사한 데이터형을 갖는 것이 쓸모 있습니다. 빈 클래스 정의가 훌륭히 할 수 있는 일입니다:

 

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

 

특정한 추상적인 데이터형을 기대하는 파이썬 코드 조각은, 종종 그 데이터형의 메서드를 흉내 내는 클래스를 대신 전달받을 수 있습니다. 예를 들어, 파일 객체로부터 데이터를 포맷하는 함수가 있을 때, 대신 문자열 버퍼에서 데이터를 읽는 메서드 read() 와 readline() 을 제공하는 클래스를 정의한 후 인자로 전달할 수 있습니다.

 

인스턴스 메서드 객체도 어트리뷰트를 갖습니다: m.__self__ 는 메서드 m() 과 결합한 인스턴스 객체이고, m.__func__ 는 메서드에 상응하는 함수 객체입니다.

 

이터레이터

 

지금쯤 아마도 여러분은 대부분의 컨테이너 객체들을 for 문으로 루핑할 수 있음을 눈치챘을 것입니다:

 

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

 

이런 스타일의 액세스는 명료하고, 간결하고, 편리합니다. 이터레이터를 사용하면 파이썬이 보편화하고 통합됩니다. 무대 뒤에서, for 문은 컨테이너 객체에 대해 iter() 를 호출합니다. 이 함수는 메서드 __next__() 를 정의하는 이터레이터 객체를 돌려주는데, 이 메서드는 컨테이너의 요소들을 한 번에 하나씩 액세스합니다. 남은 요소가 없으면, __next__() 는 StopIteration 예외를 일으켜서 for 루프에 종료를 알립니다. next() 내장 함수를 사용해서 __next__() 메서드를 호출할 수 있습니다; 이 예는 이 모든 것들이 어떻게 동작하는지 보여줍니다:

 

>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

 

이터레이터 프로토콜의 뒤에 있는 메커니즘을 살펴보면, 여러분의 클래스에 이터레이터 동작을 쉽게 추가할 수 있습니다. __next__() 메서드를 가진 객체를 돌려주는 __iter__() 메서드를 정의합니다. 클래스가 __next__() 를 정의하면, __iter__() 는 그냥 self 를 돌려줄 수 있습니다.

 

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

 

>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

 

 

제너레이터

 

제너레이터 는 이터레이터를 만드는 간단하고 강력한 도구입니다. 일반적인 함수처럼 작성되지만 값을 돌려주고 싶을 때마다 yield 문을 사용합니다. 제너레이터에 next() 가 호출될 때마다, 제너레이터는 떠난 곳에서 실행을 재개합니다 (모든 데이터 값들과 어떤 문장이 마지막으로 실행되었는지 기억합니다). 예는 제너레이터를 사소할 정도로 쉽게 만들 수 있음을 보여줍니다:

 

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

 

>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

 

제너레이터로 할 수 있는 모든 것은 앞 절에서 설명했듯이 클래스 기반 이터레이터로도 할 수 있습니다. 제너레이터가 간단한 이유는 __iter__() 와 __next__() 메서드가 저절로 만들어지기 때문입니다.

 

또 하나의 주요 기능은 지역 변수들과 실행 상태가 호출 간에 자동으로 보관된다는 것입니다. 이것은 self.index 나 self.data 와 같은 인스턴스 변수를 사용하는 접근법에 비교해 함수를 쓰기 쉽고 명료하게 만듭니다.

 

자동 메서드 생성과 프로그램 상태의 저장에 더해, 제너레이터가 종료할 때 자동으로 StopIteration 을 일으킵니다. 조합하면, 이 기능들이 일반 함수를 작성하는 것만큼 이터레이터를 만들기 쉽게 만듭니다.

 

제너레이터 표현식

 

간단한 제너레이터는 리스트 컴프리헨션과 비슷하지만, 대괄호 대신 괄호를 사용하는 문법을 사용한 표현식으로 간결하게 코딩할 수 있습니다. 이 표현식들은 둘러싸는 함수가 제너레이터를 즉시 사용하는 상황을 위해 설계되었습니다. 제너레이터 표현식은 완전한 제너레이터 정의보다 간결하지만, 융통성은 떨어지고, 비슷한 리스트 컴프리헨션보다 메모리를 덜 쓰는 경향이 있습니다.

 

예:

 

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

 

 

파이선 클래스 정복 3

 

 

 

반응형