Python 객체 시스템 기본

Beginner

This tutorial is from open-source community. Access the source code

소개

Python 객체 시스템은 주로 딕셔너리를 활용한 구현을 기반으로 합니다. 이 섹션에서는 이에 대해 논의합니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 중급 레벨의 실험이며 완료율은 73%입니다.학습자들로부터 100%의 긍정적인 리뷰율을 받았습니다.

딕셔너리, 다시 살펴보기

딕셔너리는 명명된 값들의 모음이라는 것을 기억하세요.

stock = {
    'name' : 'GOOG',
    'shares' : 100,
    'price' : 490.1
}

딕셔너리는 간단한 데이터 구조에 일반적으로 사용됩니다. 하지만, 인터프리터의 중요한 부분에도 사용되며, Python 에서 가장 중요한 데이터 타입일 수 있습니다.

딕셔너리와 모듈

모듈 내에서 딕셔너리는 모든 전역 변수와 함수를 저장합니다.

## foo.py

x = 42
def bar():
    ...

def spam():
    ...

foo.__dict__ 또는 globals()를 검사하면 딕셔너리를 볼 수 있습니다.

{
    'x' : 42,
    'bar' : <function bar>,
    'spam' : <function spam>
}

딕셔너리와 객체

사용자 정의 객체 또한 인스턴스 데이터와 클래스 모두에 딕셔너리를 사용합니다. 사실, 전체 객체 시스템은 대부분 딕셔너리 위에 추가된 레이어입니다.

딕셔너리는 인스턴스 데이터, __dict__를 저장합니다.

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG', 'shares' : 100, 'price': 490.1 }

self에 할당할 때 이 딕셔너리 (및 인스턴스) 를 채웁니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

인스턴스 데이터, self.__dict__는 다음과 같습니다.

{
    'name': 'GOOG',
    'shares': 100,
    'price': 490.1
}

각 인스턴스는 자체적인 개인 딕셔너리를 갖습니다.

s = Stock('GOOG', 100, 490.1)     ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)     ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

어떤 클래스의 인스턴스를 100 개 생성했다면, 데이터를 저장하는 100 개의 딕셔너리가 존재합니다.

클래스 멤버

별도의 딕셔너리는 또한 메서드를 저장합니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

딕셔너리는 Stock.__dict__에 있습니다.

{
    'cost': <function>,
    'sell': <function>,
    '__init__': <function>
}

인스턴스와 클래스

인스턴스와 클래스는 서로 연결되어 있습니다. __class__ 속성은 클래스를 다시 참조합니다.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

인스턴스 딕셔너리는 각 인스턴스에 고유한 데이터를 저장하는 반면, 클래스 딕셔너리는 모든 인스턴스에서 공통으로 공유되는 데이터를 저장합니다.

속성 접근 (Attribute Access)

객체로 작업할 때, . 연산자를 사용하여 데이터와 메서드에 접근합니다.

x = obj.name          ## Getting
obj.name = value      ## Setting
del obj.name          ## Deleting

이러한 연산은 내부적으로 존재하는 딕셔너리와 직접적으로 연결됩니다.

인스턴스 수정 (Modifying Instances)

객체를 수정하는 연산은 내부 딕셔너리를 업데이트합니다.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50       ## Setting
>>> s.date = '6/7/2007' ## Setting
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## Deleting
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

속성 읽기 (Reading Attributes)

인스턴스에서 속성을 읽는다고 가정해 봅시다.

x = obj.name

속성은 두 곳에 존재할 수 있습니다:

  • 로컬 인스턴스 딕셔너리.
  • 클래스 딕셔너리.

두 딕셔너리 모두 확인해야 합니다. 먼저, 로컬 __dict__를 확인합니다. 찾을 수 없으면, __class__를 통해 클래스의 __dict__를 살펴봅니다.

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

이 조회 방식은 클래스의 멤버가 모든 인스턴스에 의해 공유되는 방식입니다.

상속의 작동 방식 (How inheritance works)

클래스는 다른 클래스로부터 상속받을 수 있습니다.

class A(B, C):
    ...

기본 클래스는 각 클래스에 튜플로 저장됩니다.

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

이것은 부모 클래스에 대한 링크를 제공합니다.

상속을 사용한 속성 읽기 (Reading Attributes with Inheritance)

논리적으로, 속성을 찾는 과정은 다음과 같습니다. 먼저, 로컬 __dict__를 확인합니다. 찾을 수 없으면, 클래스의 __dict__를 살펴봅니다. 클래스에서도 찾을 수 없으면, __bases__를 통해 기본 클래스를 살펴봅니다. 하지만, 이에 대한 몇 가지 미묘한 측면이 다음에 논의됩니다.

단일 상속을 사용한 속성 읽기 (Reading Attributes with Single Inheritance)

상속 계층 구조에서, 속성은 상속 트리를 위로 올라가면서 순서대로 찾아집니다.

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

단일 상속의 경우, 최상위 클래스까지의 경로는 하나입니다. 첫 번째 일치하는 항목에서 멈춥니다.

메서드 결정 순서 (Method Resolution Order, MRO)

Python 은 상속 체인을 미리 계산하여 클래스의 _MRO_ 속성에 저장합니다. 이를 확인할 수 있습니다.

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

이 체인을 **메서드 결정 순서 (Method Resolution Order)**라고 부릅니다. 속성을 찾기 위해 Python 은 MRO 를 순서대로 탐색합니다. 첫 번째 일치하는 항목이 선택됩니다.

다중 상속에서의 MRO (MRO in Multiple Inheritance)

다중 상속의 경우, 최상위 클래스까지의 경로는 하나가 아닙니다. 예시를 살펴보겠습니다.

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

속성에 접근할 때 어떤 일이 발생할까요?

e = E()
e.attr

속성 검색 프로세스가 수행되지만, 순서는 어떻게 될까요? 이것이 문제입니다.

Python 은 클래스 순서에 대한 몇 가지 규칙을 따르는 협력적 다중 상속 (cooperative multiple inheritance) 을 사용합니다.

  • 자식 클래스는 항상 부모 클래스보다 먼저 확인됩니다.
  • 부모 클래스 (여러 개인 경우) 는 항상 나열된 순서대로 확인됩니다.

MRO 는 이러한 규칙에 따라 계층 구조의 모든 클래스를 정렬하여 계산됩니다.

>>> E.__mro__
(
  <class 'E'>,
  <class 'C'>,
  <class 'A'>,
  <class 'D'>,
  <class 'B'>,
  <class 'object'>)
>>>

기본 알고리즘은 "C3 선형화 알고리즘 (C3 Linearization Algorithm)"이라고 불립니다. 집에서 화재가 발생하여 대피해야 하는 경우와 마찬가지로, 자식 클래스부터 시작하여 부모 클래스 순으로 진행한다는 점만 기억하면 세부 사항은 중요하지 않습니다.

이상한 코드 재사용 (다중 상속 관련)

완전히 관련 없는 두 객체를 생각해 봅시다.

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## Code commonality with LoudBike (below)
        return super().noise().upper()

그리고

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        ## Code commonality with LoudDog (above)
        return super().noise().upper()

LoudDog.noise()LoudBike.noise()의 구현에 코드 공통성이 있습니다. 사실, 코드는 정확히 같습니다. 당연히, 이러한 코드는 소프트웨어 엔지니어의 관심을 끌 것입니다.

"믹스인 (Mixin)" 패턴

믹스인 (Mixin) 패턴은 코드 조각을 가진 클래스입니다.

class Loud:
    def noise(self):
        return super().noise().upper()

이 클래스는 단독으로 사용할 수 없습니다. 상속을 통해 다른 클래스와 섞입니다.

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

놀랍게도, 이제 시끄러움 (loudness) 은 단 한 번 구현되었고, 완전히 관련 없는 두 클래스에서 재사용되었습니다. 이러한 트릭은 Python 에서 다중 상속을 사용하는 주요 목적 중 하나입니다.

super()를 사용해야 하는가

메서드를 재정의할 때는 항상 super()를 사용하십시오.

class Loud:
    def noise(self):
        return super().noise().upper()

super()는 MRO (Method Resolution Order, 메서드 결정 순서) 에서 다음 클래스로 위임합니다.

까다로운 부분은 그것이 무엇인지 모른다는 것입니다. 특히 다중 상속이 사용되는 경우 무엇인지 알 수 없습니다.

몇 가지 주의사항

다중 상속은 강력한 도구입니다. 힘에는 책임이 따른다는 것을 기억하십시오. 프레임워크/라이브러리는 구성 요소의 조합과 관련된 고급 기능을 위해 때때로 이를 사용합니다. 이제 그것을 봤다는 것을 잊으십시오.

4 절에서 주식 보유를 나타내는 Stock 클래스를 정의했습니다. 이 연습에서는 해당 클래스를 사용합니다. 인터프리터를 다시 시작하고 몇 가지 인스턴스를 만듭니다.

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

연습 문제 5.1: 인스턴스 표현

대화형 셸에서 생성한 두 인스턴스의 기본 딕셔너리를 검사합니다.

>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
... look at the output ...
>>>

연습 문제 5.2: 인스턴스 데이터 수정

위의 인스턴스 중 하나에 새 속성을 설정해 보십시오.

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... look at output ...
>>> ibm.__dict__
... look at output ...
>>>

위의 출력에서 goog 인스턴스에는 date 속성이 있는 반면 ibm 인스턴스에는 없음을 알 수 있습니다. Python 은 속성에 어떠한 제한도 두지 않는다는 점에 유의하는 것이 중요합니다. 예를 들어, 인스턴스의 속성은 __init__() 메서드에서 설정된 속성으로 제한되지 않습니다.

속성을 설정하는 대신, __dict__ 객체에 직접 새 값을 넣어보십시오.

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

여기서 인스턴스가 딕셔너리 위에 있는 레이어일 뿐이라는 사실을 실제로 알 수 있습니다. 참고: 딕셔너리를 직접 조작하는 것은 드물다는 점을 강조해야 합니다. 항상 (.) 구문을 사용하도록 코드를 작성해야 합니다.

연습 문제 5.3: 클래스의 역할

클래스 정의를 구성하는 정의는 해당 클래스의 모든 인스턴스에서 공유됩니다. 모든 인스턴스가 관련 클래스로 다시 연결되어 있음을 확인하십시오.

>>> goog.__class__
... look at output ...
>>> ibm.__class__
... look at output ...
>>>

인스턴스에서 메서드를 호출해 보십시오.

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

goog.__dict__ 또는 ibm.__dict__에 'cost'라는 이름이 정의되어 있지 않음을 확인하십시오. 대신, 클래스 딕셔너리에서 제공됩니다. 다음을 시도해 보십시오.

>>> Stock.__dict__['cost']
... look at output ...
>>>

딕셔너리를 통해 cost() 메서드를 직접 호출해 보십시오.

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

클래스 정의에서 정의된 함수를 어떻게 호출하고 self 인수가 인스턴스를 얻는지 확인하십시오.

Stock 클래스에 새 속성을 추가해 보십시오.

>>> Stock.foo = 42
>>>

이 새 속성이 이제 모든 인스턴스에 표시되는 방식을 확인하십시오.

>>> goog.foo
42
>>> ibm.foo
42
>>>

그러나 인스턴스 딕셔너리의 일부가 아님을 확인하십시오.

>>> goog.__dict__
... look at output and notice there is no 'foo' attribute ...
>>>

인스턴스 자체에서 찾을 수 없는 경우 Python 이 항상 클래스 딕셔너리를 확인하기 때문에 인스턴스에서 foo 속성에 액세스할 수 있습니다.

참고: 이 연습 부분은 클래스 변수라고 하는 것을 보여줍니다. 예를 들어, 다음과 같은 클래스가 있다고 가정해 보십시오.

class Foo(object):
     a = 13                  ## Class variable
     def __init__(self,b):
         self.b = b          ## Instance variable

이 클래스에서 클래스 자체의 본문에서 할당된 변수 a는 "클래스 변수"입니다. 생성되는 모든 인스턴스에서 공유됩니다. 예를 들어:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## Inspect the class variable (same for both instances)
13
>>> g.a
13
>>> f.b          ## Inspect the instance variable (differs)
10
>>> g.b
20
>>> Foo.a = 42   ## Change the value of the class variable
>>> f.a
42
>>> g.a
42
>>>

연습 문제 5.4: 바운드 메서드 (Bound methods)

Python 의 미묘한 특징은 메서드를 호출하는 것이 실제로 두 단계와 바운드 메서드라고 하는 것을 포함한다는 것입니다. 예를 들어:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

바운드 메서드는 실제로 메서드를 호출하는 데 필요한 모든 조각을 포함합니다. 예를 들어, 메서드를 구현하는 함수의 기록을 유지합니다.

>>> s.__func__
<function sell at 0x10049af50>
>>>

이것은 Stock 딕셔너리에서 찾은 것과 동일한 값입니다.

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

바운드 메서드는 또한 self 인수인 인스턴스를 기록합니다.

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

()를 사용하여 함수를 호출하면 모든 조각이 함께 결합됩니다. 예를 들어, s(25)를 호출하면 실제로 다음과 같은 일이 발생합니다.

>>> s.__func__(s.__self__, 25)    ## Same as s(25)
>>> goog.shares
50
>>>

연습 문제 5.5: 상속 (Inheritance)

Stock에서 상속하는 새 클래스를 만듭니다.

>>> class NewStock(Stock):
        def yow(self):
            print('Yow!')

>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

상속은 속성에 대한 검색 프로세스를 확장하여 구현됩니다. __bases__ 속성은 즉시 상위 클래스의 튜플을 갖습니다.

>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>

__mro__ 속성은 속성을 검색할 순서대로 모든 상위 클래스의 튜플을 갖습니다.

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>)
>>>

다음은 위의 인스턴스 ncost() 메서드를 찾는 방법입니다.

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

요약

축하합니다! Dictionaries Revisited 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.