객체지향 프로그래밍

객체

객체란 일상 생활에서 보는 모든 물체들 TV, 휴대폰, 모니터, 사과, 고양이, 자동차 등을 컴퓨터 프로그래밍으로 모델링하여 사용하는 개념이라고 할 수 있습니다. 객체는 크게 상태(state)와 행위(behavior)로 구성될 수 있습니다. 자동차를 예를 들어봅니다. 자동차를 구성하는 것으로 차의 색상, 바퀴, 차체, 유리, 핸들, 전조등, 음향 기기, 계기판 등 다양한 부속품들이 필요할 것입니다. 또한 그러한 것들을 움직이게 하도록 하는 기능들이 필요할 것입니다. 예를 들면, 전조등 및 라디오를 켜고 끄는 동작, 와이퍼 작동, 속력을 높이고 낮추고 하는 등의 기능들이 필요합니다. 여기서 자동차를 구성하는 부속품들 즉, 색상, 바퀴, 차체 등을 속성(또는 데이터)라 하고, 그것을 움직이게 하는 필요한 기능들을 메소드라고 합니다. 클래스란 자동차의 설계도 또는 청사진이라고 생각할 수 있습니다.

객체지향 언어

일상의 물체들을 프로그래밍으로 표현하기 위해 프로그래밍 언어에 객체 개념을 도입합니다. 객체 지향언어는 다음과 같은 특성이 있습니다.

  1. 은닉(Encapsulation)

    은닉화는 내부를 보호하기 위해 볼 수 없게 만드는 것입니다. 예를 들면 TV는 리모콘을 통해서 채널, 음량을 조절할 뿐 TV 내부 회로 구성에 대해서 사용자가 볼 수 없게 숨겨놓는 것이 안전할 겁니다. 프로그래밍 언어에서는 클래스라는 것을 이용하여 객체를 만들고 내부를 숨기는 역할을 합니다.

    class Animal:
    
      def __init__(self, name, age):
        self.name = name
        self.age = age
    
      def eat(self):
        pass
    
      def speak(self):
        pass
    
  2. 상속(Inheritance)

    상위 객체의 속성이 하위 객체에 물려 받아 져서 하위 객체가 상위 객체의 속성을 모두 가지고 있는 것을 의미합니다. 동물과 식물은 생물이라는 상위 객체의 속성을 이어받고, 사람과 짐승은 동물의 속성을 가지고 있는 것과 같은 것을 표현하는 것입니다. 프로그래밍 언어에서 상위 객체를 부모(상위 또는 수퍼) 클래스라고 하고 하위 객체를 자식(하위 또는 서브) 클래스라고 부릅니다.

    class Human(Animal):
    
      def __init__(self, name, age, hobby):
        super().__init__(name, age)
        self.hobby = hobby
    
      def work(self):
        pass
    
      def laugh(self):
        pass
    
  3. 다형성(Polymorphism)

    다형성은 같은 이름의 메소드가 객체에 따라 다르게 동작하도록 구현하는 것을 말합니다. 동물의 소리내기라는 메소드는 강아지, 고양이, 돼지 등의 객체에서 서로 다른 소리가 나는 것과 같습니다. 매소드 오버라이딩(method overriding)으로 구현됩니다.

클래스와 인스턴스

클래스

클래스는 데이터(속성 또는 필드)와 기능(함수 또는 메소드)을 함께 묶는 방법을 제공합니다. 새로운 클래스를 정의하는 것은 새로운 데이터 형을 만든다는 것입니다.

인스턴스

클래스를 이용해서 인스턴스 instance 들을 만들어 낼 수 있습니다. 인스턴스는 인스턴스의 상태를 나타내는 속성 attribute (또는 필드 field)들과 인스턴스의 상태를 변경할 수 있는 메소드 method 들을 가질 수 있습니다. 클래스는 붕어빵을 만들어내는 붕어빵 기계와 같은 것이고 인스턴스란 붕어빵 기계에서 나온 붕어빵들 이라고 상상할 수 있습니다. 붕어빵을 만들 때 속재료에 해당하는 것이 인스턴스의 상태를 나타내는 속성이라고 할 수 있습니다. 속재료의 내용을 변경하려고 할 때 사용하는 동작에 해당하는 것이 메소드라고 할 수 있습니다.

클래스 정의와 인스턴스 만들기

클래스 정의

클래스는 예약어 class 뒤에 클래스 이름을 적고 콜론 :으로 마치고, 다음 줄에 들여쓰기 하면서 클래스 몸통을 시작합니다.

class <클래스 이름>:
    <클래스 몸통>

가장 간단한 클래스를 만들어 봅니다.

class MyClass:
    pass

인스턴스 만들기

MyClass란 이름의 클래스를 만들었고 몸통은 비워두었습니다. 클래스의 인스턴스를 만들기 위해서는 클래스 이름에 이어 소괄호 ()를 열고 닫으면 됩니다. 즉 MyClass 클래스의 인스턴스는 아래와 같이 하면됩니다.

myClass = MyClass() # 인스턴스 객체 만들기
print(myClass)
<__main__.MyClass object at 0x000001FA3AEA97F0>

위에서 myClass = MyClass() 부분에서 MyClass 인스턴스 객체를 만들어 myClass이란 변수에 할당한 것입니다. myClass을 출력해보면 myClass가 참조하고 있는 인스턴스 객체의 저장위치 주소가 출력되는 것을 알 수 있습니다.

직접하기

  1. Rectangle 클래스를 만들고 인스턴스를 만들어 출력해보세요.

    rect = Rectangle()
    print(rect)
    
  2. Dog 클래스를 만들고 인스턴스를 만들어 출력해보세요.

    dog = Dog()
    print(dog)
    

메소드와 self

클래스에 기능을 추가해 봅니다. 기능을 추가하기 위해서는 클래스의 메소드 method 를 이용하면 됩니다. 메소드란 앞에서 배운 함수와 같은 역할을 하는 것으로 특별히 클래스에서는 함수라는 용어대신 메소드라고 부릅니다. 메소드 정의는 함수 정의할 때와 마찬가지로 def 예약어를 사용합니다.

다음은 Cat 클래스에 speak() 메소드를 추가한 것입니다.

In [1]: class Cat:
   ...:   def speak(self):
   ...:     print("야옹~~!")
   ...: 

여기서 주목해야할 것은 self라는 매개변수입니다. self란 인스턴스 객체 자신을 의미하는 것으로 메소드를 호출할 때 파이썬이 자동으로 첫번째 인수로 넘겨줍니다. 따라서 인스턴스 메소드를 정의할 때는 반드시 첫번째 매개변수로 self를 입력해야 합니다. self 대신 다른 이름을 사용할 수 있지만 통상적으로 사용하는 이름이므로 그대로 사용합니다. self는 인스턴스 메소드를 호출할 때 파이썬이 자동으로 넘겨주기 때문에 사용자가 인수로 입력할 필요가 없습니다.

메소드를 부르기 위해서는 먼저 인스턴스 객체를 만들어야 합니다. 인스턴스 객체는 클래스 이름 뒤에 소괄호를 이용해 만듭니다.

In [2]: cat = Cat() # 인스턴스 객체 만들기

메소드를 실행하려면 인스턴스 객체 바로 뒤에 점을 찍고 메소드 이름을 적어 소괄호를 이용해서 함수 호출하는 것과 똑같이 합니다.

In [3]: cat.speak() # 메소드 부르기
야옹~~!

직접하기

  1. Dog 클래스에 speak() 메소드를 추가하고, 메소드를 호출하여 멍멍 이라고 출력을 해보세요.

인스턴스 초기화 __init__ 메소드

__init__() 메소드는 인스턴스를 만들 때 인스턴스의 초기 상태를 설정하기 위해 사용됩니다. init 앞 뒤로 두겹 밑줄을 이용하여 만듭니다. 파이썬은 인스턴스를 만들 때 자동으로 __init__() 메소드를 실행합니다.

Cat 인스턴스를 만들 때, 고양이의 이름을 정하고 싶을 때 __init__ 메소드를 사용하면 됩니다.

In [4]: class Cat:
   ...:   def __init__(self, name):
   ...:     self.name = name
   ...: 

self.name 에서 self 는 인스턴스를 가리키며 .name 그 인스턴스에 속해있는 속성 이름 name을 의미합니다. 즉, 다음과 같이 Cat 인스턴스를 만들 때 인수로 이름을 입력하여 cat을 만들면 cat의 이름은 체리을 가지게 됩니다.

In [5]: cat = Cat('체리')

cat 인스턴스의 이름이 무었인지를 확인하기 위해서는 다음과 같이 인스턴스 뒤에 점을 찍고 속성 이름을 적어서 확인 할 수 있습니다.

In [6]: print(cat.name)
체리

인스턴스 속성들은 클래스 내에서 언제든지 사용할 수 있습니다.

In [7]: class Cat:
   ...:   def __init__(self, name):
   ...:     self.name = name
   ...: 
   ...:   def speak(self):
   ...:     print(f'야옹~~! 난 {self.name}야.')
   ...: 
   ...: cat = Cat('체리')
   ...: cat.speak()
   ...: 
야옹~~! 난 체리야.

직접하기

  1. Person 클래스의 __init__(self, name, age, job, gender, blood) 메소드를 구현해봅니다. Person 클래스의 속성으로 이름, 나이, 직업, 성별, 혈액형 등을 구현합니다.

  2. Dog 클래스에 __init__(self, name, age) 메소드를 구현해봅니다. Dog 클래스의 속성으로 name, age 를 구현하고 eat(self, food). sleep(self) 등의 메소드를 만들어 보세요.

  3. User 클래스를 구현해보세요. name, email, password 속성을 추가하세요. describe() 메소드를 이용해 유저 정보들을 출력하세요. greet() 메소드를 이용해 자신을 소개하는 내용을 출력해보세요. 여러 개의 인스턴스들을 만들고 각각의 메소드들을 실행해보세요.

  4. Restaurant 클래스의 __init__ 메소드를 이용해 식당 이름 restaurant_name, 식당 유형 cuisine_type 속성 들을 추가하세요. describe() 메소드를 이용해 식당 이름과 식당 유형을 출력해보세요. open() 메소드를 이용해 식당이 영업중이라고 출력해보세요. Restaurant 인스턴스를 만들어 각각의 메소드들을 실행해보세요.

속성 변경

직접하기

  1. Restaurant 클래스에 number_served 속성을 추가하고 기본값은 0으로 설정합니다. 인스턴스를 만들어 손님의 수를 바꿔보고 바뀐 값을 출력해봅니다. set_number_served(number) 메소드를 추가하여 방문한 손님의 수를 설정합니다. 메소드를 실행해보고 방문한 손님의 수를 출력해봅니다. increment_number_served(number) 메소드를 이용하여 손님의 수를 증가시켜보세요.

  2. User 클래스에 login_attepmts 속성을 추가하고 increment_login_attempts() 메소드를 이용하여 로그인 횟수를 하나씩 증가해보세요. reset_login_attempts() 메소드를 이용하여 로그인 회수 login_attepmts 를 0로 만들어 보세요.

Note

클래스의 메소드는 인스턴스 메소드 instance method, 클래스 메소드 class method, 정적 메소드 static method 로 나눌 수 있습니다. 인스턴스 메소드는 메소드 정의시 반드시 self 매개변수가 맨 먼저 와야하며 인스턴스의 상태를 변경하거나 반영할 수 있습니다. 클래스 메소드는 클래스의 상태와 관련된 메소드로 메소드 정의시 @classmethod 장식자(decorator)를 붙이고 cls라는 이름의 매개변수가 맨 먼저 와야 하며 cls는 클래스 객체를 가리키는 변수입니다. 여기서 cls는 변수 이름이며 다른 이름을 사용해도 됩니다. 정적 메소드는 클래스와 상관없이 정의되는 것으로 클래스의 문맥상 클래스 안에 정의할 뿐입니다. @staticmethod 장식자(decorator)를 함수 정의하기 전에 사용합니다. 정적 메소드는 모듈의 함수와 비슷한 역할을 한다고 볼 수 있습니다.

클래스 객체 대 인스턴스 객체

클래스/인스턴스 객체 만들기

파이썬 클래스와 관련된 객체는 클래스 객체인스턴스 객체로 나눌 수 있습니다. 클래스 객체는 클래스 정의가 되자마자 만들어집니다. 즉, 파이썬 인터프리터가 소스 파일로부터 클래스 정의 부분을 읽어들일 때 만들어집니다.

In [8]: class Rectangle:
   ...:     name = "직사각형"
   ...: 
   ...:     def __init__(self, width, height):
   ...:         self.width = width
   ...:         self.height = height
   ...: 
   ...:     def info(self):
   ...:         print('직사각형 클래스입니다')
   ...: 
   ...:     def area(self):
   ...:         return self.width * self.height
   ...: 
   ...:     def perimeter(self):
   ...:         return 2 * (self.width + self.height)
   ...: 

클래스 객체는 앞에서 봤던 인스턴스 객체와 다르게 소괄호를 통해서 만들어질 필요가 없습니다. 클래스 객체는 클래스 이름으로 접근하면 됩니다. 그리고 클래스 객체를 이용해서 클래스변수와 메소드들을 접근할 수 있습니다.

In [9]: print(Rectangle.name)
직사각형

메소드와 함수

클래스에 정의된 메소드를 클래스 객체를 이용해서 접근할 수도 있습니다. 이때는 인스턴스와는 다르게 첫번째 인자로 인스턴스 객체를 넘겨 주어야 합니다. 클래스 객체를 이용해서 접근하는 것은 일반적인 함수 객체를 접근하는 것과 같습니다.

In [10]: Rectangle.area() # 에러 발생. 첫번째 인수가 필요
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-479b64445857> in <module>
----> 1 Rectangle.area() # 에러 발생. 첫번째 인수가 필요

TypeError: area() missing 1 required positional argument: 'self'

하지만 다음과 같이 첫번째 인수로 인스턴스 객체를 넘겨주면 작동합니다.

In [11]: rect = Rectangle(2, 3)
   ....: Rectangle.area(rect)    # 첫번째 인수로 인스턴스 객체 넘겨줌
   ....: 
Out[11]: 6

__init__() 메소드 차이

인스턴스 객체는 위에서 본 바와 같이 <클래스 이름>()을 통하여 만들어 지고 인스턴스 객체가 생성되면서 자동적으로 __init__메소드가 실행이 됩니다. 하지만 클래스 객체는 __init__ 메소드를 실행하지 않습니다.

In [12]: class MyClass:
   ....:     cvar = "클래스 변수"
   ....: 
   ....:     def __init__(self, var1):
   ....:         self.var1 = var1
   ....:         print("__init__메소드가 실행되었습니다. self.var1은 {}입니다.".format(self.var1))
   ....: 
   ....:     def method1(self):
   ....:         print("메소드1 입니다.")
   ....: 
   ....:     def method2(self, var2):
   ....:         print("method2의 인자는 {}입니다.".format(var2))
   ....: 

클래스 객체를 통해서 실행해봅니다.

In [13]: MyClass.method1("Don't Care!")
메소드1 입니다.

보는바와 같이 __init__ 메소드가 실행이 되지 않습니다. 하지만 인스턴스를 만들어서 실행하면 다음과 같이 됩니다.

In [14]: my = MyClass("사과")
__init__메소드가 실행되었습니다. self.var1은 사과입니다.

인스턴스 객체가 만들어지면 언제든지 인스턴스.변수 또는 인스턴스.메소드()를 통하여 자신의 값을 접근하고 변경할 수 있습니다.

In [15]: print(my.var1)
사과
In [16]: my.var1 = "배"
   ....: print(my.var1)
   ....: 

클래스 변수, 인스턴스 변수

클래스 변수란 클래스 안에, 메소드 밖에서 정의된 변수를 말합니다. 위에서 cvar이 클래스변수입니다. 클래스 변수는 모든 인스턴스가 공유하는 속성과 메소드를 의미합니다. 클래스 변수 접근은 클래스이름.변수를 이용합니다.

In [17]: class 클래스:
   ....:   변수 = '클래스 변수'
   ....: 
   ....:   def __init__(self, 이름):
   ....:     self.이름 = 이름 # self.이름은 인스턴스 변수
   ....: 
   ....: print(클래스.변수)
   ....: 
클래스 변수

인스턴스 변수는 인스턴스에 종속되어 변하는 데이터에 연결된 변수를 의미하고 인스턴스 변수 접근은 인스턴스객체변수.변수와 같이 합니다.

In [18]:  = 클래스("객체이름")
   ....: print(.이름)
   ....: 
객체이름

식별자 유효범위(scope)

클래스의 메소드에서 사용되는 식별자는 그 식별자가 인스턴스에 속한 것인지 클래스에 속한 것인지 아니면 지역에 속한 것인지 전역에 속한 것인지를 명확하게 구분해줘야 합니다.

인스턴스 객체의 메소드 식별자 검색 순서는 먼저 지역 변수인지 확인하고 다음으로 인스턴스 객체 변수인지 확인하고 다음은 클래스 객체 변수인지 확인하고 마지막으로 전역변수인지를 확인해서 없으면 NameError가 발생합니다.

In [19]: 변수 = "전역 변수"
   ....: 
   ....: class 클래스:
   ....:     변수 = '클래스 변수'
   ....: 
   ....:     def __init__(self, 이름):
   ....:         self.이름 = 이름 # self.이름은 인스턴스 변수
   ....: 
   ....:     def 메소드(self):
   ....:         변수 = "어디에 속할까?"
   ....:         self.변수 = "인스턴스 변수"
   ....: 
   ....:     def 상태(self):
   ....:         print("변수는:", 변수)
   ....:         print("self.변수는:", self.변수)
   ....: 
   ....: print(변수)
   ....:  = 클래스("클래스 이름")
   ....: .상태()
   ....: 
전역 변수
변수는: 전역 변수
self.변수는: 클래스 변수

위에서 self.변수의 값이 클래스 변수로 나온 것을 볼 수 있습니다. 이것은 self.변수 식별자를 지역, 인스턴스, 클래스, 전역 순서로 찾기때문입니다. 지역에도 없고 인스턴스에도 정의가 되어 있지 않으므로 클래스 객체의 값을 참조하게되는 것입니다.

메소드에서 클래스 변수의 값을 변경하기 위해서는 인스턴스객체.__class__를 이용합니다. 인스턴스객체.__class__는 인스턴스 객체가 속한 클래스 객체의 참조입니다.

In [20]: 변수 = "전역 변수"
   ....: 
   ....: class 클래스:
   ....:     변수 = '클래스 변수'
   ....: 
   ....:     def __init__(self, 이름):
   ....:         self.이름 = 이름 # self.이름은 인스턴스 변수
   ....: 
   ....:     def 메소드(self):
   ....:         변수 = "어디에 속할까?"
   ....:         self.변수 = "인스턴스 변수"
   ....: 
   ....:     def 클래스변수값변경(self):
   ....:         self.__class__.변수 = "클래스 변수값이 변경되었습니다."
   ....: 
   ....:     def 상태(self):
   ....:         print("변수는:", 변수)
   ....:         print("클래스 변수는:", self.__class__.변수)
   ....: 
   ....:  = 클래스("클래스 이름")
   ....: .클래스변수값변경()
   ....: .상태()
   ....: 
변수는: 전역 변수
클래스 변수는: 클래스 변수값이 변경되었습니다.

메소드 안에서 다른 메소드를 부를 때도 인스턴스의 메소드인지 클래스의 메소드인지를 명확하게 해주어야 합니다.

In [21]: 변수 = "전역 변수"
   ....: 
   ....: class 클래스:
   ....:     변수 = '클래스 변수'
   ....: 
   ....:     def __init__(self, 이름):
   ....:         self.이름 = 이름 # self.이름은 인스턴스 변수
   ....: 
   ....:     def 메소드(self):
   ....:         변수 = "어디에 속할까?"
   ....:         self.변수 = "인스턴스 변수"
   ....: 
   ....:     def 클래스변수값변경(self):
   ....:         메소드()
   ....:         self.__class__.변수 = "클래스 변수값이 변경되었습니다."
   ....: 
   ....:     def 상태(self):
   ....:         print("변수는:", 변수)
   ....:         print("클래스 변수는:", self.__class__.변수)
   ....: 
   ....:  = 클래스("클래스 이름")
   ....: .클래스변수값변경()
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-21-0401b0c31ab2> in <module>
     20 
     21  = 클래스("클래스 이름")
---> 22 .클래스변수값변경()

<ipython-input-21-0401b0c31ab2> in 클래스변수값변경(self)
     12 
     13     def 클래스변수값변경(self):
---> 14         메소드()
     15         self.__class__.변수 = "클래스 변수값이 변경되었습니다."
     16 

NameError: name '메소드' is not defined

14줄에서 메소드()라는 이름을 찾을 수 없다는 에러가 발생하는 것을 볼 수 있습니다. 이 메소드를 전역에서 찾기때문입니다. 인스턴스의 메소드로 사용하기 위해서는 self.메소드()와 같이 메소드가 어디에 속했는지를 밝혀서 사용해야 합니다.

가변/불변 객체(Object)

모든 객체는 고유성 identity를 가지고 있습니다. 고유성은 형 type과 값 value로 구성됩니다. 값이 변할 수 있는 객체를 가변 객체 mutable object라고 하고 값이 변하지 않는 객체를 불변 객체 immutable object라고 합니다. 불변 객체로는 숫자형(int, float, complex), 문자열(str), 튜플(tuple)등이 있습니다. 객체의 고유성은 객체가 일단 한 번 생성되면 절대 변하지 않습니다. 고유성은 객체의 메모리 주소라고 생각할 수 있습니다. id() 함수를 이용해 고유성을 나타내는 정수를 확인할 수 있습니다.

is 연산자를 이용하여 두 객체가 같은 고유성을 갖는지를 알 수 있습니다. is는 두 객체의 id() 값이 같으면 True를 다르면 False를 반환합니다. is not은 그 반대로 작동합니다.

불변 객체라고 해도 불변 객체를 이루고 있는 성분이 가변 객체라면 가변 객체의 내용이 변함에 따라 불변 객체에 대응되는 가변 객체 성분이 변할 수 있습니다. 예를 들어 봅니다.

In [22]: clist = [1, 2, 3]
   ....: x = (1, "a", clist)
   ....: print("변경 전 clist:", clist)
   ....: print("clist 변경 전 x:", x)
   ....: 
   ....: print('clist 변경 전 id(x):',id(x))
   ....: 
   ....: clist[0] = 100
   ....: print("\n변경된 clist:", clist)
   ....: 
   ....: print("clist 변경 후 x:", x)
   ....: print('clist 변경 후 id(x):',id(x))
   ....: 
변경 전 clist: [1, 2, 3]
clist 변경 전 x: (1, 'a', [1, 2, 3])
clist 변경 전 id(x): 2226057532864

변경된 clist: [100, 2, 3]
clist 변경 후 x: (1, 'a', [100, 2, 3])
clist 변경 후 id(x): 2226057532864

불변 객체 튜플 x = (1, "a", clist)의 3번째 성분이 가변 객체 리스트 clist입니다. clist[0] = 100으로 변경하면 x의 3번째 성분도 함께 변경됩니다. 하지만 xid는 변하지 않습니다.

특별한 메소드들

파이썬 클래스는 특별한 메소드들을 제공합니다. special method 또는 magic method 또는 double under method(줄여서 dunder method라고도 불립니다.)라는 메소드들이 있습니다. 앞에서 봤던 __init__ 메소드가 예입니다. 그외에도 여러 가지 특별한 메소드들이 있습니다.

수학 기호와 관련된 특별한 메소드들

__add__는 덧셈 기호 +를 사용할 수 있는 연산을 정의합니다. __sub__- 기호에 대한 정의를 합니다. 자세한 것은 이곳을 참조하세요.

메소드

기호

설명

__add__(self, other)

+

덧셈

__sub__(self, other)

-

뺄셈

__mul__(self, other)

*

곱셈

__matmul__(self, other)

@

행렬 곱

__truediv__(self, other)

/

나눗셈

__floordiv__(self, other)

//

__mod__(self, other)

%

나머지

__divmod__(self, other)

divmod()

몫과 나머지 순서쌍

__pow__(self, other)

**

거듭제곱

__lshift__(self, other)

<<

왼쪽 시프트

__rshift__(self, other)

>>

오른쪽 시프트

__and__(self, other)

&

논리곱

__xor__(self, other)

^

배타적 합

__or__(self, other)

|

논리합

In [23]: class Vector:
   ....:   def __init__(self, x, y):
   ....:     self.x = x
   ....:     self.y = y
   ....: 
   ....:   def __add__(self, other):
   ....:     x = self.x + other.x
   ....:     y = self.y + other.y
   ....:     return x, y
   ....: 
   ....: v1 = Vector(1, 1)
   ....: v2 = Vector(2, 3)
   ....: print(f"v1 + v2 = {v1 + v2}")
   ....: 
v1 + v2 = (3, 4)

__call__ 메소드

__call__ 메소드는 클래스 객체를 함수 호출하듯이 부를 수 있습니다.

In [24]: class MyFun:
   ....:   def __init__(self, a, b):
   ....:     self.a = a
   ....:     self.b = b
   ....: 
   ....:   def value(self, x):
   ....:     return self.a * x + self.b
   ....: 
   ....:   def __call__(self, x):
   ....:     return self.a * x + self.b
   ....: 

함수의 객체를 만듭니다.

In [25]: f1 = MyFun(1, 1)
   ....: f1.value(10)
   ....: 
Out[25]: 11

객체를 함수 호출하듯이 사용할 수 있습니다.

In [26]: f1(10)
Out[26]: 11

수치 미분에 응용

함수 f(x)에 대한 수치 미분은 다음과 같이 구할 수 있습니다.

\[f'(x) \approx \frac{f(x+h) - f(x)}{h}\]

이러한 미분을 클래스로 정의하여 모든 함수에 대해서 미분값을 근사할 수 있습니다.

다음은 수치 미분을 Derivative란 이름의 클래스로 구현한 것입니다. 인스턴스를 만들 때 인수로 함수 f를 입력받고 __call__ 메소드를 호출하여 그 함수의 미분값을 반환합니다.

In [27]: class Derivative:
   ....:   def __init__(self, f, h=1E-5):
   ....:     self.f = f
   ....:     self.h = h
   ....: 
   ....:   def __call__(self, x):
   ....:     f, h = self.f, self.h # 간단한 이름으로 변경
   ....:     return (f(x+h) - f(x)) / h
   ....: 

다음은 일차함수 \(y = 3x +1\) 을 수치미분한 것입니다.

In [28]: f = lambda x: 3 * x + 1
   ....: df = Derivative(f)
   ....: df(2)
   ....: 
Out[28]: 3.000000000064062

직접하기

  1. Derivative 클래스를 이용하여 함수 f(x) = sin(x)의 수치미분을 x=1 에서 구해보세요.

반복 가능 객체 iterable

반복가능 객체란 한번에 하나씩 값을 반환하는 객체입니다. 반복가능한 객체로 list, str, tuple 과 같은 열거형 sequece type 이 있고, 비열거형 non-sequence type 객체로는 dict와 file 객체가 있습니다. 그리고 __iter__() 또는 __getitem__() 메소드를 가지고 있는 클래스들이 반복가능 객체가 됩니다. 내장함수 iter() 에 의해서 반복가능 객체는 __iter__() 메소드로부터 반복자 iterator 를 반환합니다.

반복자는 __next__() 메소드를 가지고 있어야 하면 __next__() 메소드는 내장함수 next() 에 의해 값을 하나씩 반환하도록 하든지, 아니면 StopIteration 을 발생시켜야 합니다.

In [29]: class IterClass:
   ....: 
   ....:   def __init__(self):
   ....:     self.x = 0
   ....: 
   ....:   def __iter__(self):
   ....:     return self
   ....: 
   ....:   def __next__(self):
   ....:     if self.x < 5:
   ....:       self.x += 1
   ....:       return self.x
   ....:     else:
   ....:       raise StopIteration
   ....: 
   ....: itc = IterClass()
   ....: it = iter(itc)
   ....: for i in it:
   ....:   print(i)
   ....: 
1
2
3
4
5

참조

파이썬 객체는 내부적으로 객체를 참조하는 갯수를 세고 있다가 참조수(reference count)가 0이 되면 객체를 삭제합니다. __del__() 메소드를 이용하여 객체가 삭제될 때 작업을 할 수 있습니다.

 1  class A:
 2
 3      def __init__(self, name):
 4          self.name = name
 5          print("{}가 만들어졌습니다.".format(self.name))
 6
 7      def __del__(self):
 8          print("{}가 삭제되었습니다.".format(self.name))
 9
10  a1 = A("객체")
11  del a1
객체가 만들어졌습니다.
객체가 삭제되었습니다.
  • 10줄: 객체가 만들어져서 a1이라는 변수가 객체를 참조하고 있습니다.

  • 11줄: 참조를 삭제하면 객체가 삭제되었습니다라는 메시지를 볼 수 있습니다.

a1 = A("객체1")
a2 = A("객체2")
del a1, a2
객체1가 만들어졌습니다.
객체2가 만들어졌습니다.
객체1가 삭제되었습니다.
객체2가 삭제되었습니다.

아래의 경우는 a, b 변수가 동시에 하나의 객체를 참조하고 있습니다.

1  a = A("객체")
2  b = a
3  del a
객체가 만들어졌습니다.
  • 3줄: a를 제거하고 있지만 b라는 변수가 여전히 객체를 참조하고 있기 때문에 삭제되지 않습니다.

1  a = A("객체")
2  b = a
3  del a
4  del b
객체가 만들어졌습니다.
객체가 삭제되었습니다.
객체가 삭제되었습니다.
  • 4줄: ab를 삭제해야만 객체가 제거되는 것을 볼 수 있습니다.

순환참조(cycle reference)

다음과 같이 순환참조를 할 경우 객체 참조를 제거해도 객체는 계속 남아있게 됩니다.

 1  class A:
 2
 3      def __init__(self, name):
 4          self.name = name
 5          print("{}가 만들어졌습니다.".format(self.name))
 6
 7      def __del__(self):
 8          print("{}가 삭제되었습니다.".format(self.name))
 9
10  a1 = A("객체1")
11  a2 = A("객체2")
12
13  a1.ref = a2
14  a2.ref = a1
15
16  del a1, a2
객체1가 만들어졌습니다.
객체2가 만들어졌습니다.
  • 13, 14줄: a1.ref = a2를 통해서 객체1 안의 변수 ref객체2를 참조하고 반대로 a2.ref = a1을 통해 객체2 안의 변수 ref객체1을 참조하고 있습니다.

  • 16줄: a1, a2 참조를 제거해도 a1.ref, a2.ref 참조가 각각의 객체를 참조하고 있기 때문에 객체1, 객체2는 제거되지 않습니다.

아래 그림을 참조 합니다.

만일 객체2를 제거하기 원합다면 먼저 객체2의 참조들을 삭제해야 합니다.

1  a1 = A("객체1")
2  a2 = A("객체2")
3
4  a1.ref = a2
5  a2.ref = a1
6
7  del a2, a1.ref
8  del a1
객체1가 만들어졌습니다.
객체2가 만들어졌습니다.
객체2가 삭제되었습니다.
객체1가 삭제되었습니다.
  • 7줄: a2 참조와, a1.ref 참조를 차례로 제거하면 더이상 객체2에 대한 참조는 존재하지 않으므로 객체2가 삭제가 됩니다.

  • 그러면 a2.ref 참조도 자동적으로 삭제가 됩니다.

  • 8줄: a1을 삭제하면 객체1 참조들도 모두 사라져서 객체1이 제거됩니다.

참조 사이트