개발자/파이썬 Python

파이선 클래스 기초 2. 리얼파이선 19

지구빵집 2022. 3. 14. 09:28
반응형

 

파이선 클래스 기초 2. 리얼파이선 19

 

클래스와의 첫 만남

 

클래스는 약간의 새 문법과 세 개의 객체형과 몇 가지 새 개념들을 도입합니다.

 

클래스 정의 문법

 

클래스 정의의 가장 간단한 형태는 이렇게 생겼습니다:

 

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

 

함수 정의(def 문)처럼, 클래스 정의는 어떤 효과가 생기기 위해서는 먼저 실행되어야 합니다. (상상컨대 클래스 정의를 if 문의 분기나 함수 내부에 놓을 수 있습니다)

 

실재적으로, 클래스 정의 내부의 문장들은 보통 함수 정의들이지만, 다른 문장들도 허락되고 때로 쓸모가 있습니다 — 나중에 이 주제로 돌아올 것입니다. 클래스 내부의 함수 정의는 보통, 메서드 호출 규약의 영향을 받은, 특별한 형태의 인자 목록을 갖습니다. — 다시, 이것은 뒤에서 설명됩니다.

 

클래스 정의에 진입할 때, 새 이름 공간이 만들어지고 지역 스코프로 사용됩니다 — 그래서, 모든 지역 변수들로의 대입은 이 새 이름 공간으로 갑니다. 특히, 함수 정의는 새 함수의 이름을 이곳에 연결합니다.

 

클래스 정의가 (끝을 통해) 정상적으로 끝날 때, 클래스 객체 가 만들어집니다. 이것은 기본적으로 클래스 정의 때문에 만들어진 이름 공간의 내용물들을 감싸는 싸개입니다; 다음 섹션에서 클래스 객체에 대해 더 배우게 됩니다. 원래의 지역 스코프가 (클래스 정의에 들어가기 직전에 유효하던 것) 다시 사용되고, 클래스 객체는 클래스 정의 헤더에서 주어진 클래스 이름 (예에서 ClassName) 으로 여기에 연결됩니다.

 

클래스 객체

 

클래스 객체는 두 종류의 연산을 지원합니다: 어트리뷰트 참조와 인스턴스 만들기.

 

어트리뷰트 참조 는 파이썬의 모든 어트리뷰트 참조에 사용되는 표준 문법을 사용합니다: obj.name. 올바른 어트리뷰트 이름은 클래스 객체가 만들어질 때 클래스의 이름 공간에 있던 모든 이름입니다. 그래서, 클래스 정의가 이렇게 될 때:

 

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

 

MyClass.i 와 MyClass.f 는 올바른 어트리뷰트 참조고, 각기 정수와 함수 객체를 돌려줍니다. 클래스 어트리뷰트는 대입할 수도 있어서, 대입을 통해 MyClass.i 의 값을 변경할 수 있습니다. __doc__ 도 역시 올바른 어트리뷰트고, 클래스에 속하는 독스트링을 돌려줍니다: "A simple example class".

 

클래스 인스턴스 만들기 는 함수 표기법을 사용합니다. 클래스 객체가 클래스의 새 인스턴스를 돌려주는 매개변수 없는 함수인 체합니다. 예를 들어 (위의 클래스를 가정하면):

 

x = MyClass()

 

는 클래스의 새 인스턴스 를 만들고 이 객체를 지역 변수 x 에 대입합니다.

 

인스턴스 만들기 연산 (클래스 객체 《호출하기》) 은 빈 객체를 만듭니다. 많은 클래스는 특정한 초기 상태로 커스터마이즈된 인스턴스로 객체를 만드는 것을 좋아합니다. 그래서 클래스는 이런 식으로 __init__() 라는 이름의 특수 메서드 정의할 수 있습니다:

 

def __init__(self):
    self.data = []

 

클래스가 __init__() 메서드를 정의할 때, 클래스 인스턴스 만들기는 새로 만들어진 클래스 인스턴스에 대해 자동으로 __init__() 를 호출합니다. 그래서 이 예에서, 새 초기화된 인스턴스를 이렇게 얻을 수 있습니다:

 

x = MyClass()

 

물론, __init__() 메서드는 더 높은 유연성을 위해 인자들을 가질 수 있습니다. 그 경우, 클래스 인스턴스 만들기 연산자로 주어진 인자들은 __init__() 로 전달됩니다. 예를 들어,

 

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

 

인스턴스 객체

 

이제 인스턴스 객체로 무엇을 할 수 있을까? 인스턴스 객체가 이해하는 오직 한가지 연산은 어트리뷰트 참조입니다. 두 가지 종류의 올바른 어트리뷰트 이름이 있습니다: 데이터 어트리뷰트와 메서드.

 

데이터 어트리뷰트 는 스몰토크의 《인스턴스 변수》 에, C++ 의 《데이터 멤버》 에 해당합니다. 데이터 어트리뷰트는 선언될 필요 없습니다; 지역 변수처럼, 처음 대입될 때 태어납니다. 예를 들어, x 가 위에서 만들어진 MyClass 의 인스턴스면, 다음과 같은 코드 조각은 트레이스 없이 값 16 을 인쇄합니다:

 

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

 

다른 인스턴스 어트리뷰트 참조는 메서드 입니다. 메서드는 객체에 《속하는》 함수입니다. (파이썬에서, 메서드라는 용어는 클래스 인스턴스에만 사용되지 않습니다; 다른 객체 형들도 메서드를 가질 수 있습니다. 예를 들어, 리스트 객체는 append, insert, remove, sort 등과 같은 메서드들을 갖습니다. 하지만, 앞으로의 논의에서, 명시적으로 언급하지 않는 한, 메서드라는 용어를 클래스 인스턴스 객체의 메서드에만 사용할 것입니다.)

 

인스턴스 객체의 올바른 메서드 이름은 그것의 클래스에 달려있습니다. 정의상, 함수 객체인 클래스의 모든 어트리뷰트들은 상응하는 인스턴스의 메서드들을 정의합니다. 그래서 우리의 예제에서, x.f 는 올바른 메서드 참조인데, MyClass.f 가 함수이기 때문입니다. 하지만 x.i 는 그렇지 않은데, MyClass.i 가 함수가 아니기 때문입니다. 그러나, x.f 는 MyClass.f 와 같은 것이 아닙니다 — 이것은 함수 객체가 아니라 메서드 객체입니다.

 

메서드 객체

 

보통, 메서드는 연결되자마자 호출됩니다:

 

x.f()

 

MyClass 예에서, 이것은 문자열 'hello world' 를 돌려줍니다. 하지만, 메서드를 즉시 호출할 필요는 없습니다: x.f 는 메서드 객체고, 저장된 후에 호출될 수 있습니다. 예를 들어:

 

xf = x.f
while True:
    print(xf())

 

는 영원히 계속 hello world 를 인쇄합니다.

 

메서드가 호출될 때 정확히 어떤 일이 일어날까? f() 의 함수 정의가 인자를 지정했음에도 불구하고, 위에서 x.f() 는 인자 없이 호출된 것을 알아챘을 것입니다. 인자는 어떻게 된 걸까? 확실히 파이썬은 인자를 필요로 하는 함수를 인자 없이 호출하면 예외를 일으킵니다 – 인자가 실제로는 사용되지 않는다 해도…

 

실제로, 여러분은 답을 짐작할 수 있습니다: 메서드의 특별함은 인스턴스 객체가 함수의 첫 번째 인자로 전달된다는 것입니다. 우리 예에서, 호출 x.f()는 정확히 MyClass.f(x) 와 동등합니다. 일반적으로, n 개의 인자들의 목록으로 메서드를 호출하는 것은, 첫 번째 인자 앞에 메서드의 인스턴스 객체를 삽입해서 만든 인자 목록으로 상응하는 함수를 호출하는 것과 동등합니다.

 

아직 메서드가 어떻게 동작하는지 이해하지 못했다면, 구현을 살펴보는 것이 아마도 문제를 분명하게 만들 수 있을 것입니다. 데이터 어트리뷰트가 아닌 인스턴스 어트리뷰트를 참조하면, 그것의 클래스가 검색됩니다. 만약 그 이름이 함수 객체인 올바른 클래스 어트리뷰트면, 인스턴스 객체와 방금 발견된 함수 객체를 (가리키는 포인터들을) 추상 객체에 함께 묶어서 메서드 객체를 만듭니다: 이것이 메서드 객체입니다. 메서드 객체가 인자 목록으로 호출되면, 인스턴스 객체와 인자 목록으로부터 새 인자 목록이 구성된 후, 함수 객체를 이 새 인자 목록으로 호출합니다.

 

클래스와 인스턴스 변수

 

일반적으로 말해서, 인스턴스 변수는 인스턴스별 데이터를 위한 것이고 클래스 변수는 그 클래스의 모든 인스턴스에서 공유되는 어트리뷰트와 메서드를 위한 것입니다:

 

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

 

이름과 객체에 관한 한마디에서 논의했듯이, 리스트나 딕셔너리와 같은 가변 객체가 참여할 때 공유 데이터는 예상치 못한 효과를 줄 가능성이 있습니다. 예를 들어, 다음 코드에서 tricks 리스트는 클래스 변수로 사용되지 않아야 하는데, 하나의 리스트가 모든 Dog 인스턴스들에 공유되기 때문입니다.

 

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

 

대신, 클래스의 올바른 설계는 인스턴스 변수를 사용해야 합니다:

 

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

 

기타 주의사항

 

인스턴스와 클래스 모두에서 같은 어트리뷰트 이름이 등장하면, 어트리뷰트 조회는 인스턴스를 우선합니다:

 

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

 

데이터 어트리뷰트는 메서드뿐만 아니라 객체의 일반적인 사용자 (《클라이언트》)에 의해서 참조될 수도 있습니다. 달리 표현하면, 클래스는 순수하게 추상적인 데이터형을 구현하는 데 사용될 수 없습니다. 사실, 파이썬에서는 데이터 은닉을 강제할 방법이 없습니다 — 모두 관례에 의존합니다. (반면에, C로 작성된 파이썬 구현은 필요하다면 구현 상세를 완전히 숨기고 객체에 대한 액세스를 제어할 수 있습니다; 이것은 C로 작성된 파이썬 확장에서 사용될 수 있습니다.)

 

클라이언트는 데이터 어트리뷰트를 조심스럽게 사용해야 합니다 — 클라이언트는 데이터 어트리뷰트를 건드려서 메서드들에 의해 유지되는 불변성들을 망가뜨릴 수 있습니다. 클라이언트는 이름 충돌을 피하는 한 메서드들의 유효성을 손상하지 않고도 그들 자신의 데이터 어트리뷰트를 인스턴스 객체에 추가할 수도 있음에 유의하세요 — 다시 한번, 명명 규칙은 여러 골칫거리를 피할 수 있게 합니다.

 

메서드 안에서 데이터 어트리뷰트들(또는 다른 메서드들!)을 참조하는 줄임 표현은 없습니다. 저는 이것이 실제로 메서드의 가독성을 높인다는 것을 알게 되었습니다: 메서드를 훑어볼 때 지역 변수와 인스턴스 변수를 혼동할 우려가 없습니다.

 

종종, 메서드의 첫 번째 인자는 self 라고 불립니다. 이것은 관례일 뿐입니다: 이름 self 는 파이썬에서 아무런 특별한 의미를 갖지 않습니다. 하지만, 이 규칙을 따르지 않을 때 여러분의 코드가 다른 파이썬 프로그래머들이 읽기에 불편하고, 클래스 브라우저 프로그램도 이런 규칙에 의존하도록 작성되었다고 상상할 수 있음에 유의하세요.

 

클래스 어트리뷰트인 모든 함수는 그 클래스의 인스턴스들을 위한 메서드를 정의합니다. 함수 정의가 클래스 정의에 텍스트 적으로 둘러싸일 필요는 없습니다: 함수 객체를 클래스의 지역 변수로 대입하는 것 역시 가능합니다. 예를 들어:

 

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

 

이제 f, g, h 는 모두 함수 객체를 가리키는 클래스 C 의 어트리뷰트고, 결과적으로 이것들은 모두 C 의 인스턴스들의 메서드입니다 — h 는 정확히 g 와 동등합니다. 이런 방식은 프로그램의 독자들에게 혼란을 주기만 한다는 점에 주의하세요.

 

메서드는 self 인자의 메서드 어트리뷰트를 사용해서 다른 메서드를 호출할 수 있습니다:

 

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

 

메서드는 일반 함수들과 마찬가지로 전역 이름을 참조할 수 있습니다. 메서드에 결합한 전역 스코프는 그것의 정의를 포함하는 모듈입니다. (클래스는 결코 전역 스코프로 사용되지 않습니다.) 메서드에서 전역 데이터를 사용할 좋은 이유를 거의 만나지 못하지만, 전역 스코프를 정당하게 사용하는 여러 가지 경우가 있습니다: 한 가지는, 전역 스코프에 정의된 함수와 메서드뿐만 아니라, 그곳에 임포트 된 함수와 모듈도 메서드가 사용할 수 있다는 것입니다. 보통, 메서드를 포함하는 클래스 자신은 이 전역 스코프에 정의되고, 다음 섹션에서 메서드가 자신의 클래스를 참조하길 원하는 몇 가지 좋은 이유를 보게 될 것입니다.

 

각 값은 객체고, 그러므로 클래스 (형 이라고도 불린다) 를 갖습니다. 이것은 object.__class__ 에 저장되어 있습니다. 

 

다음 시간에는 클래스를 사용하는 데 꼭 필요한 개념과 유용한 클래스 자료 몇 가지를 살펴보겠습니다.  

 

 

파이선 클래스 정복

 

 

반응형