파이썬 기초 강좌: 14. 객체지향 프로그래밍 예제와 실습 (Python Basics – Object-oriented programming Examples and Exercises)

파이썬 기초 강좌 – 객체 지향 프로그래밍(OOP)은 대규모 프로그램을 효율적으로 관리하고 유지보수할 수 있게 해주는 중요한 프로그래밍 패러다임입니다. OOP는 코드를 작고 이해하기 쉽게 나누어 복잡한 프로그램을 체계적으로 관리할 수 있도록 도와줍니다. 이 글에서는 OOP의 기본 개념과 장점을 설명하며, 이를 통해 더 우아하고 효율적인 프로그램을 작성하는 방법을 소개합니다.

파이썬 기초 강좌 - 객체지향 프로그래밍 예제와 실습

1. 파이썬 기초 강좌 – 대규모 프로그램 관리의 핵심

프로그래밍을 처음 배울 때, 우리는 프로그램을 구성하는 네 가지 기본 패턴을 익혔습니다:

  • 순차적 코드
  • 조건문 (if 문)
  • 반복문 (loops)
  • 저장 및 재사용 (함수)

이후의 장에서는 단순 변수뿐만 아니라 리스트, 튜플, 딕셔너리와 같은 컬렉션 데이터 구조도 다루었습니다. 프로그램을 작성할 때는 이러한 데이터 구조를 설계하고 조작하는 코드를 작성하게 됩니다. 프로그램을 작성하는 방법에는 여러 가지가 있으며, 여러분은 이미 “그다지 우아하지 않은” 프로그램과 “좀 더 우아한” 프로그램을 작성해 본 경험이 있을 것입니다. 프로그램이 작더라도 코드 작성에는 일종의 예술성과 미적 감각이 있다는 것을 점점 더 느끼게 됩니다.

프로그램이 수백만 줄로 늘어나면, 이해하기 쉬운 코드를 작성하는 것이 점점 더 중요해집니다. 수백만 줄의 프로그램을 작성할 때는 전체 프로그램을 한 번에 모두 기억하는 것이 불가능합니다. 문제를 해결하거나 버그를 수정하거나 새로운 기능을 추가할 때, 큰 프로그램을 여러 작은 부분으로 나누어 보는 방법이 필요합니다.

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 코드를 배열하는 한 방법으로, 현재 작업 중인 50줄의 코드를 이해하면서 나머지 999,950줄의 코드는 잠시 무시할 수 있게 해줍니다. 이를 통해 대규모 프로그램을 더 효율적으로 관리하고 유지보수할 수 있습니다.

2. 객체 지향 프로그래밍 시작하기

객체 지향 프로그래밍(OOP)의 개념을 효과적으로 사용하려면 먼저 개념을 학습하는 것이 필수적입니다. 이 장에서는 OOP의 용어와 개념을 배우고 몇 가지 간단한 예제를 통해 기초를 다질 것입니다.

이 장의 주요 목표는 객체가 어떻게 구성되고 기능하는지에 대한 기본적인 이해를 얻는 것입니다. 특히, 파이썬과 파이썬 라이브러리에서 제공하는 객체의 기능을 어떻게 활용하는지 배우는 것이 가장 중요합니다.

3. 객체 사용하기

우리는 이미 객체를 계속 사용해 왔습니다. 파이썬은 많은 내장 객체를 제공합니다. 다음은 간단한 코드로, 첫 몇 줄은 매우 간단하고 자연스럽게 느껴질 것입니다.

>>> stuff = list()
>>> stuff.append('python')
>>> stuff.append('chuck')
>>> stuff.sort()
>>> print(stuff[0])
>>> print(stuff.__getitem__(0))
>>> print(list.__getitem__(stuff, 0))

위의 코드에서 각각의 라인이 수행하는 작업을 객체 지향 프로그래밍의 관점에서 살펴보겠습니다.

  1. stuff = list()는 리스트 타입의 객체를 생성합니다.
  2. stuff.append('python')stuff.append('chuck')append() 메소드를 호출합니다.
  3. stuff.sort()sort() 메소드를 호출합니다.
  4. print(stuff[0])는 0번 위치의 아이템을 가져옵니다.
  5. print(stuff.__getitem__(0))stuff 리스트의 __getitem__() 메소드를 호출하여 0번 아이템을 가져옵니다.
  6. print(list.__getitem__(stuff, 0))는 리스트 클래스의 __getitem__ 메소드를 호출하여 stuff 리스트와 0번 아이템을 매개변수로 전달하여 0번 아이템을 가져옵니다.

마지막 세 줄의 코드는 모두 동일한 작업을 수행하지만, 특정 위치의 아이템을 조회할 때는 단순히 대괄호 문법을 사용하는 것이 더 편리합니다.

객체의 기능 살펴보기

dir() 함수를 사용하여 객체의 기능을 확인할 수 있습니다:

>>> stuff = list()
>>> print(dir(stuff))

출력 결과:

['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getitem__',
'__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__sizeof__', '__str__', '__subclasshook__',
'append', 'clear', 'copy', 'count', 'extend', 'index',
'insert', 'pop', 'remove', 'reverse', 'sort']

이 장의 나머지 부분에서는 위의 용어들을 정의할 것이므로, 장을 마친 후 다시 돌아와서 위의 내용을 확인해 보세요.

4. 프로그램 작성 시작하기

프로그램은 기본적으로 입력을 받아 처리한 후 출력을 생성합니다. 다음은 엘리베이터 층수를 변환하는 매우 짧지만 완전한 프로그램입니다.

4.1 엘리베이터 층수 변환 프로그램

usf = input('Enter the US Floor Number: ')
wf = int(usf) - 1
print('Non-US Floor Number is', wf)

이 프로그램은 사용자로부터 미국식 층수를 입력받아, 이를 1층 낮춘 후, 결과를 출력합니다.

4.2 프로그램과 외부 세계

이 프로그램을 조금 더 생각해 보면, 프로그램은 외부 세계와의 상호작용 부분(입력과 출력)과, 프로그램 내부의 코드 및 데이터를 포함하고 있습니다. 객체 지향 프로그래밍에서는 프로그램을 여러 “영역”으로 분리하여 각 영역이 코드와 데이터를 포함하고 외부 세계 및 다른 영역과의 상호작용을 명확히 정의합니다.

4.3 BeautifulSoup를 이용한 링크 추출 프로그램

다음은 BeautifulSoup 라이브러리를 사용하여 링크를 추출하는 프로그램입니다.

# To run this, download the BeautifulSoup zip file
# http://www.py4e.com/code3/bs4.zip
# and unzip it in the same directory as this file

import urllib.request, urllib.parse, urllib.error
from bs4 import BeautifulSoup
import ssl

# Ignore SSL certificate errors
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

url = input('Enter - ')
html = urllib.request.urlopen(url, context=ctx).read()
soup = BeautifulSoup(html, 'html.parser')

# Retrieve all of the anchor tags
tags = soup('a')
for tag in tags:
    print(tag.get('href', None))

이 프로그램은 다음과 같은 단계로 동작합니다:

  1. URL을 입력받아 문자열로 저장합니다.
  2. urllib를 사용하여 해당 URL에서 데이터를 가져옵니다. urllib 라이브러리는 실제 네트워크 연결을 위해 socket 라이브러리를 사용합니다.
  3. urllib에서 반환된 문자열 데이터를 BeautifulSoup에 전달하여 파싱합니다.
  4. BeautifulSouphtml.parser 객체를 사용하여 파싱한 데이터를 반환합니다.
  5. 반환된 객체에서 tags() 메소드를 호출하여 앵커 태그 객체들의 딕셔너리를 반환합니다.
  6. 각 태그를 순회하며 get() 메소드를 호출하여 href 속성을 출력합니다.

이 프로그램의 핵심은 여러 객체들이 협력하여 작업을 수행하는 방식을 보여주는 것입니다. 각 객체는 자신만의 역할을 가지고 있으며, 이 객체들 간의 데이터 이동을 조율하여 프로그램을 구성합니다. 이는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다. 처음 이 프로그램을 보았을 때, 이러한 객체 간의 데이터 이동을 인식하지 못했더라도, 코드는 작업을 수행하는 데 필요한 모든 것을 포함하고 있었습니다.

5. 문제를 세분화하기

객체 지향 프로그래밍(OOP)의 큰 장점 중 하나는 복잡성을 숨길 수 있다는 점입니다. 예를 들어, 우리가 urllibBeautifulSoup 코드를 사용할 때, 이러한 라이브러리가 내부적으로 어떻게 작동하는지 알 필요는 없습니다. 이는 우리가 해결해야 하는 문제의 부분에 집중할 수 있도록 해주고, 프로그램의 다른 부분은 무시할 수 있게 합니다.

이러한 집중력은 우리가 관심 있는 프로그램의 일부에만 집중할 수 있게 해줌으로써 매우 유용합니다. 또한, 우리가 사용하는 객체의 개발자들에게도 도움이 됩니다. 예를 들어, BeautifulSoup을 개발한 프로그래머들은 우리가 HTML 페이지를 어떻게 가져오는지, 어떤 부분을 읽고 싶은지, 웹 페이지에서 추출한 데이터를 어떻게 사용할 계획인지 알 필요가 없습니다.

객체 지향 프로그래밍을 통해 문제를 세분화하고, 각 부분에 대한 복잡성을 숨김으로써 더 효율적이고 관리하기 쉬운 프로그램을 작성할 수 있습니다. 이를 통해 개발자는 자신의 작업에 더 집중할 수 있고, 전체적인 프로그램의 품질을 향상시킬 수 있습니다.

6. 우리의 첫 번째 파이썬 객체

기본적으로 객체는 전체 프로그램보다 작은 코드와 데이터 구조입니다. 함수를 정의하면 코드를 저장하고 이름을 부여한 다음, 그 이름을 사용하여 나중에 코드를 호출할 수 있습니다.

객체는 여러 함수를 포함할 수 있으며(이를 메서드라고 부릅니다), 이러한 함수가 사용하는 데이터를 포함할 수 있습니다. 객체의 일부인 데이터 항목을 속성이라고 부릅니다. 우리는 class 키워드를 사용하여 객체를 구성할 데이터와 코드를 정의합니다. class 키워드는 클래스의 이름을 포함하고, 속성(데이터)과 메서드(코드)를 포함하는 들여쓰기된 코드 블록을 시작합니다.

class PartyAnimal:
    def __init__(self):
        self.x = 0

    def party(self):
        self.x = self.x + 1
        print("So far", self.x)

an = PartyAnimal()
an.party()
an.party()
an.party()

코드 설명

각 메서드는 def 키워드로 시작하고 들여쓰기된 코드 블록으로 구성됩니다. 첫 번째 메서드는 특별히 명명된 __init__ 메서드로, 객체에 저장하려는 데이터를 초기화하는 데 사용됩니다. 이 클래스에서 우리는 self.x 속성을 점 표기법을 사용하여 할당하고 0으로 초기화합니다.

self.x = 0

다른 메서드인 party는 첫 번째 매개변수로 self를 사용하여 객체 인스턴스에 접근할 수 있도록 합니다. 이 매개변수를 통해 속성을 설정하고 점 표기법을 사용하여 메서드를 호출할 수 있습니다.

class 키워드는 객체를 생성하지 않으며, 대신 PartyAnimal 유형의 각 객체에 포함될 데이터와 코드를 정의하는 템플릿을 정의합니다. 클래스는 쿠키 커터와 같고, 클래스 사용하여 생성된 객체는 쿠키입니다.

다음 코드 라인은 실행 가능한 첫 번째 코드입니다:

an = PartyAnimal()

이 코드는 PartyAnimal 클래스의 객체 또는 인스턴스를 생성하도록 파이썬에 지시합니다. 이는 클래스 자체에 대한 함수 호출처럼 보입니다. 파이썬은 적절한 데이터와 메서드를 사용하여 객체를 생성하고, 해당 객체를 변수 an에 할당합니다. 이는 우리가 익숙하게 사용해 온 다음 코드와 유사합니다:

counts = dict()

PartyAnimal 클래스를 사용하여 객체를 생성하면 변수 an은 해당 객체를 가리킵니다. 우리는 an을 사용하여 PartyAnimal 클래스의 특정 인스턴스에 대한 코드와 데이터에 접근할 수 있습니다. 각 PartyAnimal 객체/인스턴스는 변수 xparty라는 메서드를 포함합니다. 우리는 다음과 같이 party 메서드를 호출합니다:

an.party()

party 메서드가 호출될 때, 첫 번째 매개변수(관례적으로 self라 부름)는 PartyAnimal 객체의 특정 인스턴스를 가리킵니다. party 메서드 내에서 우리는 다음 줄을 봅니다:

self.x = self.x + 1

이 점 표기법을 사용하는 구문은 ‘self 내의 x’를 의미합니다. party()가 호출될 때마다 내부 x 값은 1씩 증가하고 값이 출력됩니다. 다음 줄은 an 객체 내에서 party 메서드를 호출하는 또 다른 방법입니다:

PartyAnimal.party(an)

이 변형에서는 클래스 내의 코드를 접근하고 객체 포인터 an을 첫 번째 매개변수(메서드 내의 self)로 명시적으로 전달합니다. an.party()는 위의 줄의 축약형이라고 생각할 수 있습니다.

프로그램이 실행되면 다음과 같은 출력이 생성됩니다:

So far 1
So far 2
So far 3
So far 4

객체가 생성되고 party 메서드가 네 번 호출되며, an 객체 내의 x 값을 증가시키고 출력합니다.

7. 클래스를 타입으로 사용하기

파이썬에서 모든 변수는 타입을 가지고 있습니다. 내장 함수 dir를 사용하여 변수의 기능을 살펴볼 수 있습니다. 우리가 생성한 클래스와 함께 typedir을 사용할 수도 있습니다.

다음은 예제 코드입니다:

class PartyAnimal:
    def __init__(self):
        self.x = 0

    def party(self):
        self.x = self.x + 1
        print("So far", self.x)

an = PartyAnimal()
print("Type", type(an))
print("Dir ", dir(an))
print("Type", type(an.x))
print("Type", type(an.party))

이 프로그램을 실행하면 다음과 같은 출력이 생성됩니다:

Type <class '__main__.PartyAnimal'>
Dir ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'party', 'x']
Type <class 'int'>
Type <class 'method'>

코드 설명

이 코드를 통해 우리는 클래스 키워드를 사용하여 새로운 타입을 생성했습니다. dir 함수의 출력에서, x 정수 속성과 party 메서드가 객체 내에서 사용할 수 있음을 알 수 있습니다.

  1. an = PartyAnimal()PartyAnimal 클래스의 새로운 인스턴스를 생성합니다.
  2. print("Type", type(an))an 객체의 타입을 출력합니다.
  3. print("Dir ", dir(an))an 객체의 속성과 메서드를 출력합니다.
  4. print("Type", type(an.x))x 속성의 타입을 출력합니다.
  5. print("Type", type(an.party))party 메서드의 타입을 출력합니다.

이 출력을 통해, 우리는 PartyAnimal 클래스가 새로운 타입을 생성하며, 이 클래스의 인스턴스가 해당 타입을 가지게 됨을 알 수 있습니다. 또한 dir 함수를 통해 객체의 속성과 메서드를 확인할 수 있습니다.

8. 객체의 생명 주기

이전 예제들에서, 우리는 클래스를 정의하고, 그 클래스를 사용하여 인스턴스(객체)를 생성한 후, 인스턴스를 사용했습니다. 프로그램이 끝나면 모든 변수는 버려집니다. 일반적으로 변수의 생성과 소멸에 대해 많이 생각하지 않지만, 객체가 더 복잡해지면 객체가 생성될 때 설정 작업을 수행하고 객체가 버려질 때 정리 작업을 수행해야 할 필요가 있습니다.

객체가 이러한 생성 및 소멸 시점을 인식하도록 하려면, 특별히 명명된 메서드를 객체에 추가합니다:

class PartyAnimal:
    def __init__(self):
        self.x = 0
        print('I am constructed')

    def party(self):
        self.x = self.x + 1
        print('So far', self.x)

    def __del__(self):
        print('I am destructed', self.x)

an = PartyAnimal()
an.party()
an.party()
an = 42
print('an contains', an)

코드 설명

이 프로그램을 실행하면 다음과 같은 출력이 생성됩니다:

I am constructed
So far 1
So far 2
I am destructed 2
an contains 42

파이썬이 객체를 생성할 때, __init__ 메서드를 호출하여 객체의 초기 값을 설정할 기회를 제공합니다.

def __init__(self):
    self.x = 0
    print('I am constructed')

an = 42 라인을 만나면, 파이썬은 실제로 객체를 “버리고”, an 변수를 재사용하여 값 42를 저장할 수 있습니다. 객체가 “소멸”되는 순간, 소멸자 코드 __del__이 호출됩니다.

def __del__(self):
    print('I am destructed', self.x)

변수가 소멸되는 것을 막을 수는 없지만, 객체가 더 이상 존재하지 않기 직전에 필요한 정리 작업을 수행할 수 있습니다. 객체를 개발할 때, 초기 값을 설정하기 위해 생성자를 추가하는 것은 매우 일반적입니다. 객체에 소멸자가 필요한 경우는 상대적으로 드뭅니다.

9. 여러 인스턴스 생성하기

지금까지 우리는 클래스를 정의하고, 단일 객체를 생성하여 사용한 후 버리는 과정을 다루었습니다. 그러나 객체 지향 프로그래밍의 진정한 힘은 클래스로부터 여러 인스턴스를 생성할 때 나타납니다. 여러 객체를 생성할 때 각 객체에 대해 다른 초기 값을 설정하고 싶을 수 있습니다. 생성자에 데이터를 전달하여 각 객체에 다른 초기 값을 줄 수 있습니다.

class PartyAnimal:
    def __init__(self, nam):
        self.x = 0
        self.name = nam
        print(self.name, 'constructed')

    def party(self):
        self.x = self.x + 1
        print(self.name, 'party count', self.x)

s = PartyAnimal('Sally')
s.party()
j = PartyAnimal('Jim')
j.party()
s.party()

코드 설명

이 프로그램에서 생성자는 self 매개변수와 객체가 생성될 때 전달되는 추가 매개변수를 가지고 있습니다:

s = PartyAnimal('Sally')

생성자 내부에서 두 번째 라인은 객체 인스턴스 내의 name 속성에 전달된 매개변수(nam)를 복사합니다:

self.name = nam

프로그램의 출력은 각 객체(sj)가 xnam의 독립적인 복사본을 포함하고 있음을 보여줍니다:

Sally constructed
Jim constructed
Sally party count 1
Jim party count 1
Sally party count 2

각 객체는 자체 속성과 메서드를 가지며, 서로 독립적으로 작동합니다. 이를 통해 객체 지향 프로그래밍의 강력한 기능을 활용하여 더 복잡하고 유연한 프로그램을 작성할 수 있습니다.

10. 상속

객체 지향 프로그래밍의 또 다른 강력한 기능은 기존 클래스를 확장하여 새로운 클래스를 만드는 능력입니다. 클래스를 확장할 때, 원래 클래스를 부모 클래스, 새로운 클래스를 자식 클래스라고 부릅니다.

이 예제에서는 PartyAnimal 클래스를 별도의 파일로 이동한 다음, 새 파일에서 PartyAnimal 클래스를 확장하여 사용합니다:

from party import PartyAnimal

class CricketFan(PartyAnimal):
    def __init__(self, nam):
        super().__init__(nam)
        self.points = 0

    def six(self):
        self.points = self.points + 6
        self.party()
        print(self.name, "points", self.points)

s = PartyAnimal("Sally")
s.party()
j = CricketFan("Jim")
j.party()
j.six()
print(dir(j))

코드 설명

CricketFan 클래스를 정의할 때, 우리는 PartyAnimal 클래스를 확장한다고 명시합니다. 이는 PartyAnimal 클래스의 모든 변수(x)와 메서드(party)가 CricketFan 클래스에 상속된다는 것을 의미합니다. 예를 들어, CricketFan 클래스의 six 메서드 내에서 우리는 PartyAnimal 클래스의 party 메서드를 호출합니다.

CricketFan 클래스의 __init__() 메서드에서는 특별한 문법을 사용하여 PartyAnimal 클래스의 __init__ 메서드를 호출하여 PartyAnimal이 필요한 초기 설정을 수행하도록 합니다:

def __init__(self, nam):
    super().__init__(nam)
    self.points = 0

super() 문법은 파이썬에게 우리가 확장하고 있는 클래스(PartyAnimal__init__ 메서드)를 호출하라고 지시합니다. PartyAnimal은 부모(또는 상위) 클래스이고, CricketFan은 자식(또는 하위) 클래스입니다.

프로그램이 실행되면, sjPartyAnimalCricketFan의 독립된 인스턴스로 생성합니다. j 객체는 s 객체보다 추가적인 기능을 가지고 있습니다.

Sally constructed
Sally party count 1
Jim constructed
Jim party count 1
Jim party count 2
Jim points 6
['__class__', '__delattr__', ... '__weakref__', 'name', 'party', 'points', 'six', 'x']

j 객체(CricketFan 클래스의 인스턴스)의 dir 출력에서, j 객체는 부모 클래스의 속성과 메서드뿐만 아니라 CricketFan 클래스를 확장할 때 추가된 속성과 메서드도 가지고 있음을 확인할 수 있습니다.

11. 마무리

객체 지향 프로그래밍 요약

객체 지향 프로그래밍(OOP)은 복잡한 프로그램을 관리하고 확장하는 데 매우 유용한 패러다임입니다. 우리는 클래스와 객체의 개념을 이해하고, 객체의 생명 주기와 상속을 통해 코드의 재사용성과 유지 보수성을 높일 수 있었습니다. 클래스를 정의하고, 인스턴스를 생성하며, 각 객체에 독립적인 속성과 메서드를 할당하는 방법을 배웠습니다. 또한, 객체 간의 상속 관계를 설정하여 부모 클래스의 기능을 자식 클래스에 상속하고, 이를 확장하는 방법도 살펴보았습니다.

예제 코드를 통해 객체의 생성, 사용, 소멸을 경험하였고, 상속을 통해 기존 클래스를 확장하여 새로운 기능을 추가하는 방법도 배웠습니다. 객체 지향 프로그래밍을 통해 코드를 더 체계적이고 이해하기 쉽게 작성할 수 있으며, 이를 통해 복잡한 문제를 효율적으로 해결할 수 있습니다.

이제 여러분은 OOP의 기본 개념과 실습을 통해 더 나은 프로그램을 작성할 수 있는 기초를 마련하였습니다. 이를 바탕으로 더 복잡한 프로그램을 작성하고, 다양한 라이브러리와 프레임워크를 활용하여 프로젝트를 진행해 보세요. 객체 지향 프로그래밍의 강력한 기능을 통해 더 우아하고 효율적인 코드를 작성할 수 있을 것입니다.

파이썬 기초 강좌: 13. 웹서비스 사용하기 예제와 실습 (Python Basics – Using Web Services Examples and Exercises)

파이썬 기초 강좌: 14.

Leave a Comment