코드 추상화: 함수

추상화는 일반적으로 구체적인 사물들의 무리로부터 핵심적인 개념 또는 기능을 추출해 내는 것을 의미한다. 대표적으로 수학에서 다루는 점, 선, 면이 추상화를 통해 생성된 개념이다. 예를 들어, 삼각형은 점과 선이 각각 세 개로 구성되며, 면은 세 개의 선으로 제한된 영역을 의미하는데, 이는 임의의 삼각형이 공통적으로 갖는 성질이다.

프로그래밍 분야에서 다루는 구체적인 사물은 명령문(command)과 값(value)이다. 따라서 프로그래밍 분야에서의 추상화는 특정 명령문들의 무리를 대표하는 개념을 추출하거나, 특정 값들의 속성을 대표하는 개념을 추출하는 것이다.

특정 코드를 대상으로 하는 추상화는 보통 코드에 사용된 명령문의 구체적인 형태를 숨기면서 코드의 기능을 효율적으로 지원하는 방식으로 이루어진다. 대표적으로 함수, 모듈, 클래스가 있다.

모듈과 클래스에 대해서는 나중에 다루며, 여기서는 함수를 이용한 추상화를 예를 이용하여 설명한다.

코드와 명령문: 앞으로 코드와 명령문을 혼용해서 사용한다. 명령문은 문법적인 의미를 강조할 때 사용되며, 코드는 특정 명령문을 가리키는 의미로 사용된다. 보다 정확한 설명은 명령문, 코드, 소스코드, 프로그램, 소프트웨어 설명을 참조하기 바란다.

함수 추상화

프로그램의 소스코드가 길어질 수록 프로그램의 복잡도가 증가하며, 경우에 따라 프로그램의 실행 과정을 제대로 추적하지 못할 수도 있다. 따라서 프로그램은 최대한 간단명료하게 구현해야 한다.

함수 추상화가 프로그램의 논리적 구조를 보다 명확하게 드러나게 하며, 일반적으로 아래 두 가지 형식으로 이루어진다.

  • 특정 명령문에 이름을 주어 명령문의 재사용성을 높히며, 명령문의 전체적인 구조를 단순화시킨다.
  • 유사한 명령문을 반복적으로 작성하는 대신 일반화된 명령문을 재사용한다.

함수 추상화의 구체적인 장점은 아래와 같다.

  • 중복사용된 명령문 제거
  • 보다 쉬운 소스코드 이해
  • 보다 쉬운 소스코드 유지보수

사용 예제: 커피 원두 가격 확인 프로그램 활용

인터넷에서 정보 구하기에서 다룬 프로그램에 함수 추상화를 적용하여 업그레이드한다.

구체적인 개선사항은 다음과 같다.

  • 커피 원두 가격을 확인할 때 바로 구입할지 여부에 따라 다른 일 하기
  • 코드의 중복사용을 피하기 위해 함수 활용하기
  • 지역변수와 전역변수의 활용 및 차이점 이해하기

프로그램 업그레이드 1

아래 코드는 충성고객을 위한 커피 원두 가격 웹페이지에서 가격 정보를 4.8달러 이하로 떨어질 때까지 기다린 최종 가격을 알려준다.

In [1]:
import urllib.request
import time

price_basis = 4.8
bean_price = 5.0
price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

while bean_price > price_basis:
    time.sleep(1)
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
커피 원두 현재 가격이 4.48 달러입니다.

참고: 마지막 줄에 있는 print 명령문의 인자는 부분문자열을 지정된 값으로 대치하는 기능을 지원한다. 여기서 사용하는 기술은 파이썬 최신 버전에서 지원하는 f-문자열 기법이다.

구상하기

이제 위 코드를 아래 조건을 만족하도록 개선하고자 한다.

  • 프로그램이 실행될 때 지금 당장 커피 원두콩을 구입할지 여부를 묻는다.
  • Yes로 대답할 경우 바로 시세 정보를 알려준다.
  • 기타 경우에는 이전 처럼 특정 가격 이하로 내려갈 때까지 기다린다.

구현하기

  • if ... else... 명령문을 이용한다.
  • 각각의 경우에 웹사이트에 접속해서 시세 정보를 확인하는 코드를 활용한다.

예를 들어 아래와 같이 구현할 수 있다.

  • Yes라 답을 하면 바로 가격을 알려준다.
In [2]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

answer = input("지금 살까요? ")    

if answer == 'Yes':
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        price_page = urllib.request.urlopen(price_url)
        page_text = price_page.read().decode("utf8")
        price_location = page_text.find('>$') + 2
        bean_price = float(page_text[price_location : price_location + 4])

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? Yes
커피 원두 현재 가격이 4.48 달러입니다.
  • 다르게 답하면 가격이 4.8달러 이하일 때까지 기다린 후 가격을 알려준다.
In [3]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

answer = input("지금 살까요? ")    

if answer == 'Yes':
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        price_page = urllib.request.urlopen(price_url)
        page_text = price_page.read().decode("utf8")
        price_location = page_text.find('>$') + 2
        bean_price = float(page_text[price_location : price_location + 4])

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? No
커피 원두 현재 가격이 4.48 달러입니다.

프로그램 업그레이드 2: 함수 활용 - 코드 재활용

프로그램 업그레이드 1에서 개선된 프로그램은 아래 명령문을 중복사용하는 문제를 갖고 있다.

price_page = urllib.request.urlopen(price_url)
page_text = price_page.read().decode("utf8")
price_location = page_text.find('>$') + 2
bean_price = float(page_text[price_location : price_location + 4])

이런 문제는 위 명령문에 이름을 주는 방법으로 쉽게 해결할 수 있다. 즉, 위 명령문을 함수로 정의한다. 예를 들어, 아래와 같이 getPrice 라는 함수로 정의할 수 있다.

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])

이제, 아래 코드를 실행하면서 Yes를 입력해 보자.

주의: 아래 코드를 주피터 노트북에서 실행하기 전에 커널(kernel)을 재시작해야 한다. 그렇지 않으면 코드가 의도한 대로 작동하지 않는다. 이유는 아래에서 설명.

In [1]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])
    
answer = input("지금 살까요? ")    

if answer == 'Yes':
    getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? Yes
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-bd22d9ac481a> in <module>
     22         getPrice()
     23 
---> 24 print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")

NameError: name 'bean_price' is not defined

이제, 위 코드를 실행하면서 No를 입력해 보자.

주의: Yes와 다르게 입력하면 실행이 멈추지 않고 무한루프에 빠진다. 따라서 실행하고 잠시 뒤에 실행을 강제로 멈추어야 한다.

프로그램 강제 종료 방법은 다음과 같다.

  • 주피터 노트북: 키보드에서 영어 알파벳 아이(I) 키를 두 번 연속 누를 것.
  • 기타 편집기 및 터미널: 일반적으로 Ctrl-C. 운영체제, 편집기에 따라 다를 수 있음.
In [3]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])
    
answer = input("지금 살까요? ")    

if answer == 'Yes':
    getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? No
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-3-bd22d9ac481a> in <module>
     20     while bean_price > price_basis:
     21         time.sleep(1)
---> 22         getPrice()
     23 
     24 print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")

<ipython-input-3-bd22d9ac481a> in getPrice()
      5 
      6 def getPrice():
----> 7     price_page = urllib.request.urlopen(price_url)
      8     page_text = price_page.read().decode("utf8")
      9     price_location = page_text.find('>$') + 2

/usr/lib/python3.6/urllib/request.py in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    221     else:
    222         opener = _opener
--> 223     return opener.open(url, data, timeout)
    224 
    225 def install_opener(opener):

/usr/lib/python3.6/urllib/request.py in open(self, fullurl, data, timeout)
    524             req = meth(req)
    525 
--> 526         response = self._open(req, data)
    527 
    528         # post-process response

/usr/lib/python3.6/urllib/request.py in _open(self, req, data)
    542         protocol = req.type
    543         result = self._call_chain(self.handle_open, protocol, protocol +
--> 544                                   '_open', req)
    545         if result:
    546             return result

/usr/lib/python3.6/urllib/request.py in _call_chain(self, chain, kind, meth_name, *args)
    502         for handler in handlers:
    503             func = getattr(handler, meth_name)
--> 504             result = func(*args)
    505             if result is not None:
    506                 return result

/usr/lib/python3.6/urllib/request.py in http_open(self, req)
   1344 
   1345     def http_open(self, req):
-> 1346         return self.do_open(http.client.HTTPConnection, req)
   1347 
   1348     http_request = AbstractHTTPHandler.do_request_

/usr/lib/python3.6/urllib/request.py in do_open(self, http_class, req, **http_conn_args)
   1319             except OSError as err: # timeout error
   1320                 raise URLError(err)
-> 1321             r = h.getresponse()
   1322         except:
   1323             h.close()

/usr/lib/python3.6/http/client.py in getresponse(self)
   1344         try:
   1345             try:
-> 1346                 response.begin()
   1347             except ConnectionError:
   1348                 self.close()

/usr/lib/python3.6/http/client.py in begin(self)
    305         # read until we get a non-100 response
    306         while True:
--> 307             version, status, reason = self._read_status()
    308             if status != CONTINUE:
    309                 break

/usr/lib/python3.6/http/client.py in _read_status(self)
    266 
    267     def _read_status(self):
--> 268         line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    269         if len(line) > _MAXLINE:
    270             raise LineTooLong("status line")

/usr/lib/python3.6/socket.py in readinto(self, b)
    584         while True:
    585             try:
--> 586                 return self._sock.recv_into(b)
    587             except timeout:
    588                 self._timeout_occurred = True

KeyboardInterrupt: 

문제 원인 설명

발생한 문제 정리

  • Yes 입력: NameError가 발생하며, bean_price가 정의되어 있지 않다고 한다.
  • No 입력: 무한루프에 빠진다. 무한루프가 발생했다라는 것을 확인하려면 get_price 함수를 호출하기 이전에 예를들어, print('확인중') 명령문을 삽입하면 도움이 된다.

bean_price 가 정의되어 있지 않다는 설명은 좀 이상하다. 왜냐하면 getPrice 함수의 본체에 bean_price 변수가 커피 원두 가격을 가리키고 있기 때문이다.

No라고 입력할 때 무한루프에 빠지는 이유도 마찬가지로 수상하다. 무엇보다도 이전에 잘 작동하던 코드인데 특정 명령문 대신에 getPrice 함수를 이름으로 지정하고 사용한 후에 문제가 발생했다.

이유가 무엇일까?

두 가지 경우의 문제가 겉으론 달라 보이지만 원인은 동일하다. 문제의 원인은 bean_price 변수가 getPrice 함수 본체에서 선언되어 있어서 함수 밖에서는 사용될 수 없다는 데에 있다. 즉, 함수 본체에서 선언된 변수는 함수 밖에서는 사용될 수 없다.

전역변수와 지역변수

주의: 먼저 PythonTuror: bean price에 있는 코드를 실행하면서 bean_price의 행동을 유심히 살펴본 후 아래 내용을 읽으면 보다 잘 이해될 것이다.

PythonTuror: bean price에서 커피 원두 가격을 알아보는 프로그램을 단순화한 코드이며, 아래와 같다. 아래 코드는 urllib, time 등의 모듈을 사용하지 않지만 프로그램 작동 과정은 기본적으로 동일하다.


def getPrice():
    bean_price = 4.6

answer = input("Yes 또는 No를 입력하세요: ")    

if answer == 'Yes':
    getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0
    while bean_price > price_basis:
        getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")

위 코드를 PythonTutor에서 실행하다 보면 getPrice 함수의 본체에 정의된 bean_price의 역할을 확인할 수 있다. bean_price처럼 함수 본체에서 선언된 변수를 지역변수(local variable)라 부른다. 지역변수가 아닌 변수들은 전역변수(global variable)이다.

주의: 위 코드에서 bean_price가 전역변수로도 선언되었다. 결론적으로 말하면, 동일한 이름의 변수라 하더라도 지역변수와 전역변수는 서로 아무 상관이 없다. 바로 이 점 때문에 위 코드가 기대한 것과 다르게 작동한다. 이유는 다음과 같다.

  • Yes 입력: if의 조건식 answer == 'Yes'가 참이되어, getPrice()가 실행된다. 그러면서 함수 실행과정에 함수 본체 명령문인

    bean_price = 4.6
    

    이 실행된다. 하지만 이후에 곧바로 getPrice()의 실행이 멈추는데, 그러면서 getPrice()의 실행과정에서 생성된 모든 지역변수가 사라진다. 따라서 마지막 명령문에서 print 함수가 실행될 때 bean_price를 모른다고 오류가 발생하는 것이다.

  • No 입력: Yes 입력의 경우와는 달리 else의 본체 명령문이 실행할 때 price_basis, bean_price전역변수가 선언된다.

    price_basis = 4.8
      bean_price = 5.0
    

    전역변수는 프로그램 전체가 종료될 때까지 사라지지 않는다. 이후 while 반복문이 실행될 때 getPrice()가 호출되어 실행되는 과정에 bean_price라는 지역변수가 또 생성된다. 하지만 전역변수와 지역변수는 서로 다른 변수이며 상호 영향을 주지 않는다. 또한 getPrice()의 실행이 종료되면서 지역변수로 선언된 bean_price 또한 사라진다. 따라서 bean_price가 가리키는 값은 5달러로 고정되면 전혀 변하지 않는다. 따라서 while 반복문이 무한루프레 빠지게 된다.

추천 해결책: return 지정자 활용

앞서 살펴본 문제는 함수 내부에서 선언된 지역변수를 함수 외부에서 사용할 수 있도록 하면 해결된다.

함수를 실행하는 과정에서 생성된 값을 함수 외부로 보내는 방법은 return 지정자를 이용하여 함수의 실행을 종료하면서 특정 값을 내주는 것이다. 함수의 본질은 사실 특정 값을 전달하여 계속해서 사용될 수 있도록 하는 것이다. 즉, 함수는 특정 인자들과 함께 실행되어 함수 본체에서 지정된 명령문을 실행하여 특정 값을 생성한 후 리턴값으로 내주는 것이 본연의 임무이다.

앞서 선언된 getPrice 함수는 그런데 return 지정자를 사용하지 않는다. 이런 함수는 리턴값으로 None 이란 값을 내준다. None은 아무런 의미도 없는 값이며, 따라서 어디에도 사용할 수 없다.

따라서 getPrice 함수가 아래와 같이 bean_price에 할당된 커피 원두의 가격을 리턴값으로 내주도록 하면 문제를 해결할 수 있다.

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])

    return bean_price

함수가 내주는 값은 변수에 할당하거나 다른 함수의 인자로 사용될 수 있다. 즉, 제1종 객체가 된다.

그래서 getPrice()가 내주는 값을 bean_price 변수에 할당하면 원하는 대로 작동한다.

In [4]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])

    return bean_price

answer = input("지금 살까요? ")    

if answer == 'Yes':
    bean_price = getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        bean_price = getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? Yes
커피 원두 현재 가격이 4.34 달러입니다.
In [5]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])
    return bean_price

answer = input("지금 살까요? ")    

if answer == 'Yes':
    bean_price = getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        bean_price = getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? No
커피 원두 현재 가격이 4.34 달러입니다.

지역변수와 전역변수 구분

앞서 리턴값으로 지정된 값은 함수 내부에서 선언된 bean_price 변수가 가리키는 값이며, 그 값을 전역변수 bean_price에 할당한다.

그런데 이렇게 지역변수와 전역변수를 동일한 이름으로 사용하면 소스코드를 이해하는 데에 어려움을 초래할 수 있다. 따라서 프로그램을 구현할 때 지역변수와 전역변수는 가급적 구분해서 사용하는 것을 권장한다.

예를 들어 아래와 같이 getPrice 함수 내부에서 선언된 bean_price 변수이름을 price라고 변경하기만 하면 된다. 전역변수 bean_price 대신에 지역변수를 변경하는 게 편리하다. 이유는 지역변수는 리턴값으로 내주면서 자신의 역할을 다하기 때문이다.

In [1]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    # 지역변수 이름을 price로 변경
    price = float(page_text[price_location : price_location + 4])
    return price

answer = input("지금 살까요? ")    

if answer == 'Yes':
    bean_price = getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        bean_price = getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? Yes
커피 원두 현재 가격이 4.34 달러입니다.

global 지정자

함수 본체에서 선언된 지역변수를 함수 외부에서도 사용하기 위한 보다 직접적인 방법이 있다. 지역변수를 선언할 때 global 지정자를 사용하면 된다. 그러면 리턴값을 내주지 않아도 bean_price 지역변수를 사용할 수 있다.

In [2]:
import urllib.request
import time

price_url = "http://beans.itcarlow.ie/prices-loyalty.html"

def getPrice():
    global bean_price           # 전역변수로 선언
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    bean_price = float(page_text[price_location : price_location + 4])

answer = input("지금 살까요? ")    

if answer == 'Yes':
    getPrice()
else:     
    price_basis = 4.8
    bean_price = 5.0

    while bean_price > price_basis:
        time.sleep(1)
        getPrice()

print(f"커피 원두 현재 가격이 {bean_price} 달러입니다.")
지금 살까요? Yes
커피 원두 현재 가격이 4.34 달러입니다.

'global' 지정자 사용의 문제점

global 지정자의 사용은 권장되지 않는다. 이유는 기능적으로 구분되는 전역변수와 지역변수를 강제로 연동하면 대처하기 어려운 다룬 문제를 유발할 가능성이 높아진다.

예를 들어, PythonTutor: global 변수 사용에 있는 코드를 실행해서 global 지정자를 사용할 경우 발생하는 문제를 살펴보자.

코드는 아래와 같다.

In [3]:
def fun_A():
    global price
    price = 1.74
    
def fun_B():
    global price
    price = 2
    
price = 0

fun_A()
fun_B()

print(price)
2

위 코드에서 fun_Afun_B 두 함수 모두 global 키워드를 사용하여 함수 밖에서 선언된 price 전역변수를 사용하도록 선언하였다.

그리고 fun_A 함수와 fun_B 함수를 연달아 호출한 후, price 변수에 할당된 값을 최종적으로 확인한다. 확인 결과로 fun_B 함수에 의해 결정된 값인 2가 할당됨을 알게 된다. 이렇듯, global 키워드를 여러 곳에서 사용하면 여러 함수가 하나의 전역변수를 건드리는 결과가 발생할 수 있기 때문에 코드가 길어지고 복잡해지면 프로그램이 실행될 때 변수에 할당된 값이 어떻게 변경되는가를 추적하는 일이 매우 어렵거나 심지어 불가능해지는 일이 발생할 수 있다.

프로그램 업그레이드 3: 함수 활용 - 코드 일반화

프로그램 업그레이드 2에서 return 지정자를 활용하여 선언한 getPrice 함수는 매우 편리하다. getPrice() 형식의 명령문으로 실행하면 충성고객을 위한 커피 원두 가격 웹페이지에서 가격정보를 자동으로 확인하여 알려준다.

그런데 한 가지 부족한 점이 있다. 충성고객만을 위한 가격만 확인할 뿐 일반인을 위한 가격은 알려주지 않는다. 물론 price_url 변수에 할당된 주소를 수정하면 되지만, 매번 그런식으로 코드를 수정하는 일은 번거롭다. 또한 충성고객용 가격과 일반고객용 가격을 비교하는 경우에는 아래와 같이 getPrice 함수를 복제해서 하나는 충성고객용, 다른 하나는 일반고객용으로 사용해야 하는데, 이것 또한 코드의 중복사용 문제에 해당한다.

예를 들어, 아래 코드는 getPrice 함수를 복제하여 충성고객용 getPrice_loyalty와 일반고객용 getPrice_general을 정의한다.

In [1]:
import urllib.request
import time

price_url_loyal = "http://beans.itcarlow.ie/prices-loyalty.html"
price_url_general = "http://beans.itcarlow.ie/prices.html"

def getPrice_loyal():
    price_page = urllib.request.urlopen(price_url_loyal)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    price = float(page_text[price_location : price_location + 4])

    return price

def getPrice_general():
    price_page = urllib.request.urlopen(price_url_general)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    price = float(page_text[price_location : price_location + 4])

    return price

print("지금 가격을 비교합니다.")    

bean_price_loyal = getPrice_loyal()
bean_price_general = getPrice_general()
price_difference = abs(bean_price_general - bean_price_loyal)

print(f"충성고객용 가격이 일반고객용 가격보다 {price_difference:.2f} 달러 저렴합니다.")
지금 가격을 비교합니다.
충성고객용 가격이 일반고객용 가격보다 2.10 달러 저렴합니다.

이에 대한 해결책은 함수의 인자를 활용하는 것이다. getPrice 함수는 입력값을 받지 않는다. 따라서 실행할 때 매번 동일한 명령문을 수행한다. 즉, 동일한 웹페이지만 방문한다.

반면에 함수가 들어온 입력값에 따라 다르게 행동하게 만들 수 있다. 예를 들어, 지정된 웹페이지에 따라 다른 웹페이지를 방문하게 만들 수 있다.

이렇게 함수가 입력값을 받아, 입력값에 따라 다른 일을 하도록 하려면 다음과 같이 매개변수를 이용하여 함수를 선언해야 한다.

예를 들어, getPrice 함수를 아래와 같이 정의한다.

def getPrice(price_url):
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    price = float(page_text[price_location : price_location + 4])

    return price

위 함수는 price_url을 매개변수로 선언하였다. 즉, price_url 매개변수에 사용되는 인자에 따라 방문하는 웹페이지를 달리하여 가격정보를 불러오게 만들었다.

이제 가격정보를 확인하는 코드를 아래와 같이 작성할 수 있다. 코드가 훨씬 간단해졌음을 확인할 수 있다.

In [3]:
import urllib.request
import time

price_url_loyal = "http://beans.itcarlow.ie/prices-loyalty.html"
price_url_general = "http://beans.itcarlow.ie/prices.html"

# price_url 매개변수 활용
def getPrice(price_url):
    price_page = urllib.request.urlopen(price_url)
    page_text = price_page.read().decode("utf8")
    price_location = page_text.find('>$') + 2
    price = float(page_text[price_location : price_location + 4])

    return price

print("지금 가격을 비교합니다.")    

# getPrice 함수를 인자를 달리하여 두 번 실행
bean_price_loyal = getPrice(price_url_loyal)
bean_price_general = getPrice(price_url_general)
# abs 함수는 절댓값을 계산하는 함수. 가격의 차이를 나타내기 위해 사용
price_difference = abs(bean_price_general - bean_price_loyal)

print(f"충성고객용 가격이 일반고객용 가격보다 {price_difference:.2f} 달러 저렴합니다.")
지금 가격을 비교합니다.
충성고객용 가격이 일반고객용 가격보다 1.42 달러 저렴합니다.

참고: 부동소수점의 소수점 이하 길이를 제한하려면 f-문자열 서식 지정자를 사용한다.

연습문제

  1. PythonTutor:loyalVSgenral에 있는 코드를 함수 일반화를 이용하여 수정하여 Python Tutor에서 직접 실행하라.