추상화는 일반적으로 구체적인 사물들의 무리로부터 핵심적인 개념 또는 기능을 추출해 내는 것을 의미한다. 대표적으로 수학에서 다루는 점, 선, 면이 추상화를 통해 생성된 개념이다. 예를 들어, 삼각형은 점과 선이 각각 세 개로 구성되며, 면은 세 개의 선으로 제한된 영역을 의미하는데, 이는 임의의 삼각형이 공통적으로 갖는 성질이다.
프로그래밍 분야에서 다루는 구체적인 사물은 명령문(command)과 값(value)이다. 따라서 프로그래밍 분야에서의 추상화는 특정 명령문들의 무리를 대표하는 개념을 추출하거나, 특정 값들의 속성을 대표하는 개념을 추출하는 것이다.
특정 코드를 대상으로 하는 추상화는 보통 코드에 사용된 명령문의 구체적인 형태를 숨기면서 코드의 기능을 효율적으로 지원하는 방식으로 이루어진다. 대표적으로 함수, 모듈, 클래스가 있다.
모듈과 클래스에 대해서는 나중에 다루며, 여기서는 함수를 이용한 추상화를 예를 이용하여 설명한다.
코드와 명령문: 앞으로 코드와 명령문을 혼용해서 사용한다. 명령문은 문법적인 의미를 강조할 때 사용되며, 코드는 특정 명령문을 가리키는 의미로 사용된다. 보다 정확한 설명은 명령문, 코드, 소스코드, 프로그램, 소프트웨어 설명을 참조하기 바란다.
프로그램의 소스코드가 길어질 수록 프로그램의 복잡도가 증가하며, 경우에 따라 프로그램의 실행 과정을 제대로 추적하지 못할 수도 있다. 따라서 프로그램은 최대한 간단명료하게 구현해야 한다.
함수 추상화가 프로그램의 논리적 구조를 보다 명확하게 드러나게 하며, 일반적으로 아래 두 가지 형식으로 이루어진다.
함수 추상화의 구체적인 장점은 아래와 같다.
인터넷에서 정보 구하기에서 다룬 프로그램에 함수 추상화를 적용하여 업그레이드한다.
구체적인 개선사항은 다음과 같다.
아래 코드는 충성고객을 위한 커피 원두 가격 웹페이지에서 가격 정보를 4.8달러 이하로 떨어질 때까지 기다린 최종 가격을 알려준다.
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} 달러입니다.")
참고: 마지막 줄에 있는 print
명령문의 인자는 부분문자열을 지정된 값으로 대치하는 기능을 지원한다.
여기서 사용하는 기술은 파이썬 최신 버전에서 지원하는
f-문자열
기법이다.
이제 위 코드를 아래 조건을 만족하도록 개선하고자 한다.
Yes
로 대답할 경우 바로 시세 정보를 알려준다.if ... else...
명령문을 이용한다.예를 들어 아래와 같이 구현할 수 있다.
Yes
라 답을 하면 바로 가격을 알려준다.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} 달러입니다.")
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} 달러입니다.")
프로그램 업그레이드 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)을 재시작해야 한다. 그렇지 않으면 코드가 의도한 대로 작동하지 않는다. 이유는 아래에서 설명.
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를 입력해 보자.
주의: Yes와 다르게 입력하면 실행이 멈추지 않고 무한루프에 빠진다. 따라서 실행하고 잠시 뒤에 실행을 강제로 멈추어야 한다.
프로그램 강제 종료 방법은 다음과 같다.
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} 달러입니다.")
발생한 문제 정리
NameError
가 발생하며, bean_price
가 정의되어 있지 않다고 한다.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
변수에 할당하면
원하는 대로 작동한다.
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} 달러입니다.")
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} 달러입니다.")
앞서 리턴값으로 지정된 값은 함수 내부에서 선언된 bean_price
변수가 가리키는 값이며,
그 값을 전역변수 bean_price
에 할당한다.
그런데 이렇게 지역변수와 전역변수를 동일한 이름으로 사용하면 소스코드를 이해하는 데에 어려움을 초래할 수 있다. 따라서 프로그램을 구현할 때 지역변수와 전역변수는 가급적 구분해서 사용하는 것을 권장한다.
예를 들어 아래와 같이 getPrice
함수 내부에서 선언된 bean_price
변수이름을
price
라고 변경하기만 하면 된다.
전역변수 bean_price
대신에 지역변수를 변경하는 게 편리하다.
이유는 지역변수는 리턴값으로 내주면서 자신의 역할을 다하기 때문이다.
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} 달러입니다.")
global
지정자¶함수 본체에서 선언된 지역변수를 함수 외부에서도 사용하기 위한 보다 직접적인 방법이 있다.
지역변수를 선언할 때 global
지정자를 사용하면 된다.
그러면 리턴값을 내주지 않아도 bean_price
지역변수를 사용할 수 있다.
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} 달러입니다.")
global
지정자의 사용은 권장되지 않는다.
이유는 기능적으로 구분되는 전역변수와 지역변수를 강제로 연동하면
대처하기 어려운 다룬 문제를 유발할 가능성이 높아진다.
예를 들어, PythonTutor: global 변수 사용에
있는 코드를 실행해서 global
지정자를 사용할 경우 발생하는 문제를 살펴보자.
코드는 아래와 같다.
def fun_A():
global price
price = 1.74
def fun_B():
global price
price = 2
price = 0
fun_A()
fun_B()
print(price)
위 코드에서 fun_A
와 fun_B
두 함수 모두 global
키워드를 사용하여 함수 밖에서
선언된 price
전역변수를 사용하도록 선언하였다.
그리고 fun_A
함수와 fun_B
함수를 연달아 호출한 후, price
변수에
할당된 값을 최종적으로 확인한다.
확인 결과로 fun_B
함수에 의해 결정된 값인 2가 할당됨을 알게 된다.
이렇듯, global
키워드를 여러 곳에서 사용하면 여러 함수가 하나의 전역변수를 건드리는 결과가 발생할 수 있기 때문에
코드가 길어지고 복잡해지면 프로그램이 실행될 때 변수에 할당된 값이 어떻게 변경되는가를 추적하는 일이 매우 어렵거나
심지어 불가능해지는 일이 발생할 수 있다.
프로그램 업그레이드 2에서 return
지정자를 활용하여
선언한 getPrice
함수는 매우 편리하다.
getPrice()
형식의 명령문으로 실행하면
충성고객을 위한 커피 원두 가격 웹페이지에서
가격정보를 자동으로 확인하여 알려준다.
그런데 한 가지 부족한 점이 있다. 충성고객만을 위한 가격만 확인할 뿐 일반인을 위한 가격은 알려주지 않는다.
물론 price_url
변수에 할당된 주소를 수정하면 되지만,
매번 그런식으로 코드를 수정하는 일은 번거롭다.
또한 충성고객용 가격과 일반고객용 가격을 비교하는 경우에는
아래와 같이 getPrice
함수를 복제해서 하나는 충성고객용, 다른 하나는 일반고객용으로 사용해야 하는데,
이것 또한 코드의 중복사용 문제에 해당한다.
예를 들어, 아래 코드는 getPrice
함수를 복제하여 충성고객용 getPrice_loyalty
와
일반고객용 getPrice_general
을 정의한다.
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} 달러 저렴합니다.")
이에 대한 해결책은 함수의 인자를 활용하는 것이다.
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
매개변수에 사용되는 인자에 따라 방문하는 웹페이지를 달리하여
가격정보를 불러오게 만들었다.
이제 가격정보를 확인하는 코드를 아래와 같이 작성할 수 있다. 코드가 훨씬 간단해졌음을 확인할 수 있다.
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} 달러 저렴합니다.")
참고: 부동소수점의 소수점 이하 길이를 제한하려면 f-문자열 서식 지정자를 사용한다.