재고관리 프로그램 파이썬 - jaegogwanli peulogeulaem paisseon

초보자를 위한 파이썬 300제 00. 파이썬 문법 리뷰 01) 유튜브 01. 파이썬 시작하기 001 ~ 010 02. 파이썬 변수 011 ~ 020 03. 파이썬 문자열 021 ~ 030 031 ~ 040 041 ~ 050 04. 파이썬 리스트 051 ~ 060 061 ~ 070 05. 파이썬 튜플 071 ~ 080 06. 파이썬 딕셔너리 081 ~ 090 091 ~ 100 07. 파이썬 분기문 101 ~ 110 111 ~ 120 121 ~ 130 08. 파이썬 반복문 131 ~ 140 141 ~ 150 151 ~ 160 161 ~ 170 171 ~ 180 181 ~ 190 191 ~ 200 09. 파이썬 함수 201 ~ 210 211~ 220 221 ~ 230 231 ~ 240 10. 파이썬 모듈 241 ~ 250 11. 파이썬 클래스 251 ~ 260 261 ~ 270 271 ~ 280 281 ~ 290 12. 파일 입출력과 예외처리 291 ~ 300

안녕하세요 시벅이 입니다!

요즘 위탁 대량 등록이라는 재미있는 프로젝트를 하고 있습니다! 말 그대로 대량 등록이라 수만 가지 상품을 판매하고 있는데요! 여기서 가장 중요한 것은 판매하는 상품의 정보를 정확히 관리하고 최신 정보로 업데이트해주는 것이 매우 매우 중요합니다! (재고나 가격 업데이트가 안되면 추후 이거로 인한 손해/CS 가 몰려오기 때문입니다...)

수만 가지 상품의 제고를 파이썬으로 관리하다 보니 for문을 통해 위에서부터 하나하나 확인하고 있습니다. 이런 방식은 상품이 많아질수록 엄청난 시간 소요한다는 단점을 갖고 있습니다.

그래서! 이것을 해결하기 위한 파이썬의 어마어마한 무기 threading! 

재고관리 프로그램 파이썬 - jaegogwanli peulogeulaem paisseon
파이썬 threading 모듈

threading을 사람으로 비유해서 이야기하면 일처리를 한 명이 하고 있던 것을 두 명 세명 나눠서 한다고 생각하면 됩니다!

하지만 이 모듈을 사용할 때 주의할 점은 각 thread끼리 전역 변수를 공유하니 둘이 꼬이면 값이 다르게 나오는 경우가 안되게 프로그램을 잘 구성하셔야 합니다!

아래는 재고 관리 프로그램을 간단히 나타낸 스크립트입니다.

import threading

def firstStockPrice():
	print("1-10위탁 도매 사이트 재고관리")
def secondStockPrice():
	print("10-20위탁 도매 사이트 재고관리")
    
def main():
	threading.thread(target=firstStockPrice).start()
    threading.thread(target=secondStockPrice).start()
    
if __name__ == "__main__":
	main()

첫 firstStockPrice 는 위탁 도매사이트 10개의 상품 재고와 가격 변동을 파악하는 함수입니다. 각 위탁 도매사이트 아이템이 여러 개다 보니 하나 돌리는데도 시간이 2시간 정도 걸리더라고요!

secondStockPrice는 나머지 10개 위탁 도매사이트 상품 재고 가격 변동을 파악하는 함수입니다.

원래 두개를 한 번에 돌릴 땐 3시간 넘게 걸렸는데 두 개를 나눠놓으니 1시간 30분 정도 걸리네요! 이렇게 시간을 줄여 더 효율적인 것에 투자하니 기분이 아주 좋습니다 ㅎㅎ

프로그래밍이란 게 참 매력적이고 재미있는 것 같습니다. 코드 몇 개만 바꿔서 사람의 시간을 1시간 30분이나 벌어주다니...

이상 재고 프로그램을 효율적으로 바꿔주는 threading에 대한 리뷰 마치겠습니다!

글 내용이 도움되셨다면 구독 또는 좋아요 부탁드립니다~

더 많은 정보와 리뷰를 원하시면 여기로!

모바일은 여기로!

프로젝트 개요 

  •  데이터 관리 프로그램 팀 프로젝트
  • 2020년도 2학기 / 2020.10.25-2020.12.04
  • 자료구조/  Heap 구조 활용 
  • 파이썬 기반
  • 코딩, 발표 , 발표 자료 제작 
What's in my Refrigerator는 냉장고 속 식재료 데이터를 관리하는 프로그램입니다. Heap 자료구조와 파이썬을 이용하여 구현하였습니다.

2020-2 데이터사이언스와 컴퓨팅2 (안용학 교수님)

2020년도 2학기에 안용학 교수님의 데이터사이언스와 컴퓨팅2 (알고리즘) 수업을 들으면서 한학기 동안 수행한 팀 프로젝트입니다. 

한학기 동안 배운 자료구조를 하나 선정하여 데이터 관리 프로그램을 제작하는 프로젝트입니다.


What's in my Refrigerator

What's in my Refrigerator 는 냉장고 속 식재료 데이터를 관리하는 프로그램입니다.
Python을 기반으로 개발하였고 Heap 자료 구조를 사용했습니다. 

What's in my Refrigerator의 기능

  • 냉장고에 있는 식재료 데이터를 유통기한 순으로 정렬하여 관리합니다.
  • 식재료 데이터의 유통기한이 임박하면 알려줍니다.
  • 냉장고 속 식재료를 이용하여 만들 수 있는 요리 레시피를 알려줍니다.

Heap을 자료구조로 사용한 이유

  • 제자리 정렬이 가능하여 메모리 측면에서 유리합니다.
  • 처음 프로그램 시작 시, 데이터 파일의 텍스트 등 많은 자료를 불러올 때 상향식 힙 생성으로 O(n)시간만을 소요하게 됩니다.
  • 아이템을 추가할 때 O(log n)시간 만이 소요됩니다.
  • 정렬 시, 힙 정렬 2기만 수행하면 돼서 간단합니다.
  • 아이템 업데이트
    • 업데이트한 자료를 기준으로 upheap 또는 downheap을 수행하여 정렬합니다.
  • 아이템 제거 
    • 아이템 제거시 힙 속성을 유지하기 위해 루트 노드를 제거할 때와 유사한 방식으로 수행합니다. 
      1. 수정할 자료와 마지막 자료의 위치를 변경합니다 
      2. 마지막에 위치한 자료를 삭제한 것으로 간주합니다. 
      3. 수정할 자료강 있던 자리에서 수정된 값에 따라 upheap 또는 downheap을 수행합니다. 

class 다이어그램 

재고관리 프로그램 파이썬 - jaegogwanli peulogeulaem paisseon

주요 기능

  • Create
    • 새로운 식재료를 냉장고에 넣을 때, 식재료 명과 유통기한을 함께 입력해서 리스트에 삽입합니다. 
    • 데이터 삽입 후 유통기한을 기준으로 오름차순 정렬을 수행하여 정렬된 상태를 유지합니다. 
  • Read
    • 사용자가 식재료 명을 입력했을 때 해당 식재료의 유통기한을 알려줍니다.
    • 유통기한이 가장 임박한 재료를 알려줍니다.
    • 냉장고 속 식재료의 총 개수를 알려줍니다.
  • Update
    • 식재료 명을 수정하고 수정된 정보를 보여줍니다.
    • 식재료 유통기한을 수정하고 수정된 정보를 보여줍니다.
  • Delete
    • 식재료를 삭제하고 삭제한 데이터를 알려줍니다. 
  • Print
    • 냉장고 속 모든 식재료의 이름과 유통기한을 보여줍니다. 
  • 추가 기능
    • 냉장고 속 재료로 만들 수 있는 요리를 알려줍니다. 

보다 자세한 사항은 링크에 첨부된 파일들(제안서, 발표 자료, 소스코드 등)을 참고해주시면 감사하겠습니다 :) 


class Refrigerator 

재고관리 프로그램 파이썬 - jaegogwanli peulogeulaem paisseon

source code

  • Data_structure.py

데이터 관리에 사용한 자료 구조인 힙을 구현한 파일입니다.

소스 코드 보기:

더보기

import copy

class Heap:
    #######################
    ##### Heap Basics #####
    #######################

    def __init__(self):
        self.data = [None]
        self.last = 0

    def left_child(self, i):
        if 2 * i > self.last: # 왼쪽 자식이 없다면,
            return None # None
        return 2 * i

    def right_child(self, i):
        if 2 * i + 1 > self.last:  # 오른쪽 자식이 없다면,
            return None
        return 2 * i + 1

    def parent(self, i):
        if i == 1: # 부모가 없다면, (루트라면,)
            return None
        return int(i / 2)

    def swap_element(self, i, j):
        self.data[i], self.data[j] = self.data[j], self.data[i]
        return

    # def downHeap(self, i):
    #     if not self.left_child(i):
    #         return
    #     greater = self.left_child(i) # 왼쪽 자식이 있다면, 우선 greater = left
    #
    #     if self.right_child(i): # 오른쪽 자식이 있다면,
    #         if self.data[self.right_child(i)] > self.data[greater]: # 오른쪽 자식과 왼쪽 자식의 key 비교, 오른쪽이 더 크다면,
    #             greater = self.right_child(i) # greater = right
    #
    #     if self.data[i] >= self.data[greater]: # downHeap의 대상과 자식 중 더 큰것을 비교, 대상이 더 크거나 같다면,
    #         return # Heap 속성을 만족하므로 종료.
    #
    #     self.swap_element(i, greater) # 자식이 더 크다면, 서로 위치를 바꾸고,
    #     self.downHeap(greater) # 바꾼 위치에서 다시 downHeap 수행.

    def downHeap_by_date(self, i):
        if not self.left_child(i):
            return
        greater = self.left_child(i) # 왼쪽 자식이 있다면, 우선 greater = left

        if self.right_child(i): # 오른쪽 자식이 있다면,
            if self.data[self.right_child(i)].get_date() > self.data[greater].get_date(): # 오른쪽 자식과 왼쪽 자식의 key 비교, 오른쪽이 더 크다면,
                greater = self.right_child(i) # greater = right

        if self.data[i].get_date() >= self.data[greater].get_date(): # downHeap의 대상과 자식 중 더 큰것을 비교, 대상이 더 크거나 같다면,
            return # Heap 속성을 만족하므로 종료.

        self.swap_element(i, greater) # 자식이 더 크다면, 서로 위치를 바꾸고,
        self.downHeap_by_date(greater) # 바꾼 위치에서 다시 downHeap 수행.

    # def downHeap_by_name(self, i):
    #     if not self.left_child(i):
    #         return
    #     greater = self.left_child(i) # 왼쪽 자식이 있다면, 우선 greater = left
    #
    #     if self.right_child(i): # 오른쪽 자식이 있다면,
    #         if self.data[self.right_child(i)].get_name > self.data[greater].get_name: # 오른쪽 자식과 왼쪽 자식의 key 비교, 오른쪽이 더 크다면,
    #             greater = self.right_child(i) # greater = right
    #
    #     if self.data[i].get_name >= self.data[greater].get_name: # downHeap의 대상과 자식 중 더 큰것을 비교, 대상이 더 크거나 같다면,
    #         return # Heap 속성을 만족하므로 종료.
    #
    #     self.swap_element(i, greater) # 자식이 더 크다면, 서로 위치를 바꾸고,
    #     self.downHeap_by_name(greater) # 바꾼 위치에서 다시 downHeap 수행.

    # def upHeap(self, i):
    #     if not self.parent(i): # i가 루트라면,
    #         return
    #
    #     if self.data[i] <= self.data[self.parent(i)]: # Heap 속성을 만족한다면,
    #         return
    #
    #     self.swap_element(i, self.parent(i)) # i가 root가 아니고 Heap 속성도 만족하지 않는다면, swap
    #     self.upHeap(self.parent(i)) # parent에서 upHeap 수행.

    def upHeap_by_date(self, i):
        if not self.parent(i): # i가 루트라면,
            return

        if self.data[i].get_date() <= self.data[self.parent(i)].get_date(): # Heap 속성을 만족한다면,
            return

        self.swap_element(i, self.parent(i)) # i가 root가 아니고 Heap 속성도 만족하지 않는다면, swap
        self.upHeap_by_date(self.parent(i)) # parent에서 upHeap 수행.

    # def upHeap_by_name(self, i):
    #     if not self.parent(i): # i가 루트라면,
    #         return
    #
    #     if self.data[i].get_name() <= self.data[self.parent(i)].get_name(): # Heap 속성을 만족한다면,
    #         return
    #
    #     self.swap_element(i, self.parent(i)) # i가 root가 아니고 Heap 속성도 만족하지 않는다면, swap
    #     self.upHeap_by_name(self.parent(i)) # parent에서 upHeap 수행.

    ################
    ##### CRUD #####
    ################

    # def insert_item(self, item):
    #     self.last += 1
    #     self.data.append(item)
    #     self.upHeap(self.last)

    def insert_item_by_date(self, item):
        self.last += 1
        self.data.append(item)
        self.upHeap_by_date(self.last)

    # def remove_max(self):
    #     if self.last == 0:
    #         return None
    #
    #     self.swap_element(1, self.last) # root와 last swap
    #     max = self.data.pop() # last 제거, max에 값 저장.
    #     self.last -= 1
    #
    #     self.downHeap(1) # Heap 속성 복구
    #     return max

    def remove_max_by_date(self):
        if self.last == 0:
            return None

        self.swap_element(1, self.last) # root와 last swap
        max = self.data.pop() # last 제거, max에 값 저장.
        self.last -= 1

        self.downHeap_by_date(1) # Heap 속성 복구
        return max

    # def remove_idx(self, i):
    #     if i > self.last:
    #         return None # index 범위 초과
    #
    #     self.swap_element(i, self.last)  # i와 last swap
    #     removed = self.data.pop()  # last 제거, removed에 값 저장.
    #     self.last -= 1
    #
    #     self.upHeap(i) # Heap 속성 복구
    #     self.downHeap(i)  # Heap 속성 복구
    #     return removed

    def remove_idx_by_date(self, i):
        if i > self.last:
            return None # index 범위 초과

        self.swap_element(i, self.last)  # i와 last swap
        removed = self.data.pop()  # last 제거, removed에 값 저장.
        self.last -= 1

        self.upHeap_by_date(i) # Heap 속성 복구
        self.downHeap_by_date(i) # Heap 속성 복구
        return removed


    ######################
    ##### Heap inout #####
    ######################

    # self.data 정렬된 상태로 바꿈.
    # def in_place_heap_sort(self):
    #     for i in range(self.last, 1, -1): # self.last 부터 2까지
    #         self.swap_element(1, i)
    #         self.last -= 1
    #         self.downHeap_by_date(1)
    #     return

    # 정렬된 새로운 list return
    # def get_sorted(self):
    #     result = copy.deepcopy(self)
    #     for i in range(result.last, 1, -1): # self.last 부터 2까지
    #         result.swap_element(1, i)
    #         result.last -= 1
    #         result.downHeap(1)
    #     return result

    def get_sorted_by_date(self):
        result = copy.deepcopy(self)
        for i in range(result.last, 1, -1): # self.last 부터 2까지
            result.swap_element(1, i)
            result.last -= 1
            result.downHeap_by_date(1)
        return result

    # def get_sorted_by_name(self):
    #     result = copy.deepcopy(self)
    #     for i in range(result.last, 1, -1): # self.last 부터 2까지
    #         result.swap_element(1, i)
    #         result.last -= 1
    #         result.downHeap_by_name(1)
    #     return result

    # def print_heap(self):
    #     print(self.data)

    # def print_heap_items(self):
    #     for item in self.data[1:]:
    #         print(item)

    # def build_heap(self, data_list): # 비재귀적 상향식 힙생성.
    #     self.data.extend(data_list)
    #     self.last = len(data_list)
    #     for i in range(self.last, 0, -1): # self.last 부터 1까지
    #         self.downHeap(i)
    #     return

    def build_heap_by_date(self, data_list): # 비재귀적 상향식 힙생성.
        self.data.extend(data_list)
        self.last = len(data_list)
        for i in range(self.last, 0, -1): # self.last 부터 1까지
            self.downHeap_by_date(i)
        return

  • Refrigerator.py

데이터 관리 프로그램의 CRUD를 처리하는 기능과 냉장고 속 식재료 데이터를 저장하는 텍스트 파일에서 데이터를 읽어오고, 변경되거나 추가된 데이터를 써서 저장하는 기능을 가지고 있는 가장 핵심이 되는 클래스입니다. 

파일을 읽고 쓰기위한 메소드는 File_manage.py 파일에 따로 만들어 두고, 파일에서 필요한 정보를 읽고, 쓸때 임포트해서 사용했습니다.

데이터 파일에 각 아이템의 포맷은 "type, item_name, date, stock" 입니다. ex) ingredient,당근,2021-01-03,3

def read_file(cls, file_name): # file_name : 읽어올 파일의 이름. (Refrigerator.txt)
        with open(file_name, 'rt') as file:
            data = file.readlines()
        return data

File_manager에 정의된 read_file 메소드로 데이터 파일을 open해서 데이터를 readlines()로 읽어온 데이터를 반환받습니다.

for type,name,use_by_date,stock in (item.strip().split(',') for item in FM.File_manager.read_file("Refrigerator.txt")):
...

반환받은 각 데이터 라인들이 하나의 item이라고 생각하시면 됩니다. 그리고 ','을 기준으로 split을 하여 type, name, use_by_date,stock에 저장합니다. 그리고 각 아이템들의 type에 따라 ingredient면 Ingredient 타입 배열에, dish면 Dish 타입 배열에 name, use_by_date, stock 값을 append해 줍니다. 

contents = list()
        for item in self.data[1:]:
            item_str = ",".join([item.type, item.name, item.use_by_date, str(item.stock)])
            contents.append(item_str)

수정 또는 추가되는 아이템들의 name, use_by_date, stock 값을 각각 받아 data 배열에 저장해 두고, 그 배열의 각 아이템들을 type, name, use_sy_date, stock 형식에 맞게 변환한 후, content 라는 배열에 append합니다.

    def write_file(cls, file_name, contents):
        with open(file_name, 'wt') as file:
            for item in contents:
                file.write(item+"\n")
        return

contents는 File_manager에 구현된 write_file 메소드로 전달됩니다. 기존 데이터 파일을 open하고, contents 배열에 저장된 아이템들을 write해줍니다. 

File_manager.py 소스 코드 보기:

더보기

class File_manager:
    @classmethod
    def read_file(cls, file_name): # file_name : 읽어올 파일의 이름. (Refrigerator.txt)
        with open(file_name, 'rt') as file:
            data = file.readlines()
        return data

    # file_name : 쓸 파일의 이름. (Refrigerator.txt) / file_contents : 파일에 쓸 내용이 담긴 list
    @classmethod
    def write_file(cls, file_name, contents):
        with open(file_name, 'wt') as file:
            for item in contents:
                file.write(item+"\n")
        return

#
if __name__ == "__main__":
    contents = ["test_data"]
    File_manager.write_file("test_data.txt", contents)

다음 주요 기능 CRUD 코드에 대한 설명입니다. 

def ingredient_create(self, name, use_by_date, stock): # 새로운 Ingredient 추가
        self.insert_item_by_date(Food.Ingredient(name, use_by_date, stock))

새로운 식재료 정보를 인자로 받아 추가하는 메소드입니다. 전달받은 인자들로 Food 중 ingredient 타입의 오브젝트를 만들어 아이템을 유효기간 날짜 순으로 정렬하면서 삽입하는 메소드로 전달합니다. 

# Class Heap:
...
def insert_item_by_date(self, item):
        self.last += 1
        self.data.append(item)
        self.upHeap_by_date(self.last)
...

새로운 식재료 객체를 전달받은 힙 자료구조는 전체 아이템 개수를 나타내는 last 변수를 1 증가시키고, 아이템 객체들의 배열인 data에 새로운 객체를 append하고, 정렬을 위해 Heap의 upHeap_by_date 메소드을 호출합니다. 

# Class Heap:

def upHeap_by_date(self, i):
        if not self.parent(i): # i가 루트라면,
            return

        if self.data[i].get_date() <= self.data[self.parent(i)].get_date(): # Heap 속성을 만족한다면,
            return

        self.swap_element(i, self.parent(i)) # i가 root가 아니고 Heap 속성도 만족하지 않는다면, swap
        self.upHeap_by_date(self.parent(i)) # parent에서 upHeap 수행.

아이템 전체 개수를 i 인자로 받은 upHeap_by_date는 현재 힙 자료구조가 빈 상태였다면 새로 추가된 객체가 루트일 수 있습니다. 이 경우 정렬할 필요가 없으므로 그냥 return 해줍니다. 그리고 유효기간의 날짜에 따라 재귀적으로 정렬을 수행하며 힙 속성을 복구해 줍니다.

def ingredient_read_all(self):
	cnt = 1
    for item in self.data[1:]:
    	print(f"{str(cnt),{item}")
        cnt += 1

저장되어 있는 식재료 데이터 전체를 보여주는 메소드입니다. data 배열 처음부터 끝까지 프린트해줍니다. cnt는 보여줄 때 인덱스로 사용하기 위해 사용했습니다. 

def ingredient_read_all_by_date(self):
	for item in self.get_sorted_by_date().data[:]:
    	print(item)

ingredient_read_all 메소드가 단순히 data 배열에 저장된 순서대로 아이템을 나열하는 메소드라면 이 메소드는 use_by_date를 기준으로 정렬된 순서로 item을 보여주는 메소드입니다. 위와 마찬가지로 data의 처음부터 끝까지 출력하는데 이때 get_sorted_by_date()를 먼저 호출해줍니다. 

# Class Heap: ...

def get_sorted_by_date(self):
        result = copy.deepcopy(self)
        for i in range(result.last, 1, -1): # self.last 부터 2까지
            result.swap_element(1, i)
            result.last -= 1
            result.downHeap_by_date(1)
        return result
        
...

** copy.deepcopy는 파이썬 내장 모듈인 copy을 임포트해서 사용하는 메소드로 원본 배열의 주소 값을 복사해서 참조하는 것이 아니라 원본 배열 객체 자체를 복사하는 것입니다.

이를 통해 Heap 클래스 객체 자체를 복사해서 result에 저장하고, 복사된 객체에 저장된 모든 노드를 for loop을 돌게하여 downHeap을 수행시킵니다. 

# class Heap: ...

def downHeap_by_date(self, i):
        if not self.left_child(i):
            return
        greater = self.left_child(i) # 왼쪽 자식이 있다면, 우선 greater = left

        if self.right_child(i): # 오른쪽 자식이 있다면,
            if self.data[self.right_child(i)].get_date() > self.data[greater].get_date(): # 오른쪽 자식과 왼쪽 자식의 key 비교, 오른쪽이 더 크다면,
                greater = self.right_child(i) # greater = right

        if self.data[i].get_date() >= self.data[greater].get_date(): # downHeap의 대상과 자식 중 더 큰것을 비교, 대상이 더 크거나 같다면,
            return # Heap 속성을 만족하므로 종료.

        self.swap_element(i, greater) # 자식이 더 크다면, 서로 위치를 바꾸고,
        self.downHeap_by_date(greater) # 바꾼 위치에서 다시 downHeap 수행.

downHeap을 수행할 현재 노드를 인자로 전달받습니다. 이 값에 왼쪽 자식 노드가 없다면 이 노드가 루트노드임을 의미하기 때문에 재귀를 끝냅니다. 오른쪽 자식 노드가 있다면 use_by_date을 비교해서 왼쪽 자식노드와 오른쪽 자식노드 중 그 값이 큰 값을 greater에 저장하고, 현재 노드와 greater 중 현재 노드가 더 크거나 같다면 힙 속성을 만족하는 것이므로 downHeap을 끝냅니다. 또는 자식이 더 큰 경우에는 그 값이 상위로 올라가야 하기 때문에 현재노드와 greater 노드의 위치를 스왑하고 바꾼 위치를 다시 재귀 함수 인자로 전달하여 downHeap을 수행합니다. 즉, 힙 속성을 만족하면 멈출 수 있는 재귀함수 입니다. 

def ingredient_update_name(self, idx, new_name):
        self.data[idx].update_name(new_name)
        
# Class Food:
..
def update_name(self, new_name):
        self.name = new_name

데이터의 인덱스 값으로 접근하여 해당 아이템에 새로운 이름을 지정하는 메소드입니다. Food 클래스의 setter 메소드인 update_name을 사용합니다. 

def ingredient_update_use_by_date(self, idx, new_use_by_date):
        old_use_by_date = self.data[idx].use_by_date
        self.data[idx].update_use_by_date(new_use_by_date)
        # key 가 달라지므로, heap 속성 복구를 위해 수행
        if old_use_by_date < new_use_by_date:
            self.upHeap_by_date(idx)
        else:
            self.downHeap_by_date(idx)

데이터의 인덱스로 접근해 use_by_date 값을 수정하는 메소드입니다. 이 값이 정렬 key이기 때문에 이 값을 수정하면 힙 속성 복구를 위해 upHeap 과 downHeap을 수행해야 합니다. 

def ingredient_update_stock(self, idx, new_stock):
        self.data[idx].update_stock(new_stock)
        
# Class Food :...
def update_stock(self, new_stock):
        self.stock = new_stock

재고수를 업데이트하는 메소드로 이 역시 Food 클래스 메소드를 사용합니다. 

def ingredient_delete_idx(self, idx):
     return self.remove_idx_by_date(idx) # return 값 : 삭제된 값 / 인덱스 범위 초과시 None

주어진 인덱스 값의 데이터를 제거하는 메소드입니다. 값을 제거한 후 힙 속성 복구를 위한 작업이 필요하기 때문에 힙 클래스의 메소드를 호출합니다. 

# Class Heap:...

def remove_idx_by_date(self, i):
        if i > self.last:
            return None # index 범위 초과

        self.swap_element(i, self.last)  # i와 last swap
        removed = self.data.pop()  # last 제거, removed에 값 저장.
        self.last -= 1

        self.upHeap_by_date(i) # Heap 속성 복구
        self.downHeap_by_date(i) # Heap 속성 복구
        return removed
        
        
...
def swap_element(self, i, j):
        self.data[i], self.data[j] = self.data[j], self.data[i]
        return

삭제하려고 하는 데이터의 인덱스가 현재 데이터에 저장된 아이디 보다 큰 경우, None을 반환합니다. 일단 제거하려는 아이템 인덱스와 객체 배열의 가장 마지막에 저장된 아이템 인덱스를 swap_elemet에 전달해 스왑합니다. 그러면 last에 제거하려는 값이 저장되기 때문에 이 값을 배열에서 pop하고 그 값은 반환하기 위해 removed 변수에 따로 저장해둡니다. 그리고 배열 전체 개수를 나타내는 변수 last를 1 감소 시킵니다. 그런 뒤, 힙 속성을 복구시키기 위해 추가된 아이템 노드를 기준으로 upHeap과 downHeap을 수행하고, removed 값을 반환합니다. 

다음은 추가 기능에 대한 설명입니다. 

def ingredient_passed_date(self):
        result = [item for item in self.data[1:] if item.use_by_date < str(datetime.date.today())]
        if result:
            return result
        return None

저장된 아이템 중 유통기한이 지난 아이템을 보여주는 메소드입니다. 모든 아이템들의 use_by_date 를 오늘 날짜와 비교하여 지난 아이템들을 result라는 변수에 저장해서 보여줍니다. 

def ingredient_near_date(self):
        result = [item for item in self.data[1:] if 0 <= date_comparison(item.use_by_date, str(datetime.date.today())) < 3]
        if result:
            return result
        return None
        
...

def date_comparison(date1, date2):
    date1_value = (int(date1[:4])-2020) * 365 + (int(date1[5:7]) * 31) + (int(date1[8:10]))
    date2_value = (int(date2[:4])-2020) * 365 + (int(date2[5:7]) * 31) + (int(date2[8:10]))
    return date1_value - date2_value

유통기한이 임박한 식재료를 알려주기 위한 메소드입니다. ingredient_passed_date 메소드와 유사하나 그 값을 오늘 날짜와 비교하여 0 보다 크거나 같은 값을 저장해 반환합니다.

다음은 가지고 있는 식재료를 이용하여 만들 수 있는 요리를 알려주는 기능을 위한 메소드에 대한 소개입니다.

def make_dish(self, recipe):
        if not self.check_condition(recipe):
            return False
        for idx in range(1,len(self.data) - 1):
            item = self.data[idx]
            if item.name in recipe.ingredients:
                if not item.reduce_stock():
                    self.remove_idx_by_date(idx)

        if recipe.name in self.kinds_of_items():
            for item in self.data[1:]:
                if item.name == recipe.name:
                    item.stock += 1
        else:
            self.ingredient_create(recipe.name, str(datetime.date.today()), 1)
        return True

...

def reduce_stock(self, idx):
        if not self.data[idx].reduce_stock():
            self.remove_idx_by_date(idx)
            return False
        return True

    def kinds_of_items(self):
        kinds = set()
        for item in self.data[1:]:
            kinds.add(item.name)
        return kinds

    def check_condition(self, recipe):
        for item in recipe.ingredients:
            if item not in self.kinds_of_items():
                return False
        return True

Recipe_book 클래스 객체를 이용합니다. Recipe_book 객체는 recipe 객체 배열을 가지고 있습니다. 이 배열은 recipe 객체는 "요리명/식재료1, 식재료,2.." 와 같은 포맷입니다. ex) 치즈감자전/감자,치즈

Recipe_book에 저장된 recipe 객체 배열 중 하나를 인자로 전달 받은 make_dish 메소드는 이 recipe에 필요한 레시피 중 하나라도 냉장고에 있는 식재료가 아닌 경우 만들 수 없으므로 false를 반환합니다. 이렇게 check_condition을 통과한 뒤, 냉장고에 저장된 모든 아이템에 중 recipe에 사용될 식재료는 사용했으므로 제고 수를 줄이고, 재고수가 더 이상 없는 경우도 만들 수 없으므로 false를 반환합니다. 

Refrigerator.py소스 코드 보기 : 

더보기

import Data_structure
import File_manager as FM
import Food
import datetime

def date_comparison(date1, date2):
    date1_value = (int(date1[:4])-2020) * 365 + (int(date1[5:7]) * 31) + (int(date1[8:10]))
    date2_value = (int(date2[:4])-2020) * 365 + (int(date2[5:7]) * 31) + (int(date2[8:10]))
    return date1_value - date2_value


class Refrigerator(Data_structure.Heap):

    #############################
    ##### File read / write #####
    #############################

    def __init__(self):
        super().__init__()

        items = list()
        for type,name,use_by_date,stock in (item.strip().split(',') for item in FM.File_manager.read_file("Refrigerator.txt")):
            if type == "ingredient":
                items.append(Food.Ingredient(name, use_by_date, stock))
            elif type == "dish":
                items.append(Food.Dish(name, use_by_date, stock))
            else:
                pass
        self.build_heap_by_date(items)

    def save_refrigerator(self):
        contents = list()
        for item in self.data[1:]:
            item_str = ",".join([item.type, item.name, item.use_by_date, str(item.stock)])
            contents.append(item_str)
        FM.File_manager.write_file("Refrigerator.txt", contents)

    ################
    ##### CRUD #####
    ################

    def ingredient_create(self, name, use_by_date, stock): # 새로운 Ingredient 추가
        self.insert_item_by_date(Food.Ingredient(name, use_by_date, stock))

    def ingredient_read_all(self):
        cnt = 1
        for item in self.data[1:]:
            print(f"{str(cnt)}. {item}")
            cnt += 1

    def ingredient_read_all_by_date(self):
        for item in self.get_sorted_by_date().data[1:]:
            print(item)

    def ingredient_update_name(self, idx, new_name):
        self.data[idx].update_name(new_name)

    def ingredient_update_use_by_date(self, idx, new_use_by_date):
        old_use_by_date = self.data[idx].use_by_date
        self.data[idx].update_use_by_date(new_use_by_date)
        # key 가 달라지므로, heap 속성 복구를 위해 수행
        if old_use_by_date < new_use_by_date:
            self.upHeap_by_date(idx)
        else:
            self.downHeap_by_date(idx)

    def ingredient_update_stock(self, idx, new_stock):
        self.data[idx].update_stock(new_stock)

    def ingredient_delete_idx(self, idx):
        return self.remove_idx_by_date(idx) # return 값 : 삭제된 값 / 인덱스 범위 초과시 None

    #########################################
    ########## Additional Features ##########
    #########################################

    def ingredient_passed_date(self):
        result = [item for item in self.data[1:] if item.use_by_date < str(datetime.date.today())]
        if result:
            return result
        return None

    def ingredient_near_date(self):
        result = [item for item in self.data[1:] if 0 <= date_comparison(item.use_by_date, str(datetime.date.today())) < 3]
        if result:
            return result
        return None

    def check_use_by_date(self):
        passed = self.ingredient_passed_date()
        near = self.ingredient_near_date()
        return passed, near

    ##### Recipe #####

    def reduce_stock(self, idx):
        if not self.data[idx].reduce_stock():
            self.remove_idx_by_date(idx)
            return False
        return True

    def kinds_of_items(self):
        kinds = set()
        for item in self.data[1:]:
            kinds.add(item.name)
        return kinds

    def check_condition(self, recipe):
        for item in recipe.ingredients:
            if item not in self.kinds_of_items():
                return False
        return True

    def make_dish(self, recipe):
        if not self.check_condition(recipe):
            return False
        for idx in range(1,len(self.data) - 1):
            item = self.data[idx]
            if item.name in recipe.ingredients:
                if not item.reduce_stock():
                    self.remove_idx_by_date(idx)

        if recipe.name in self.kinds_of_items():
            for item in self.data[1:]:
                if item.name == recipe.name:
                    item.stock += 1
        else:
            self.ingredient_create(recipe.name, str(datetime.date.today()), 1)
        return True


if __name__ == "__main__":
    main_refrigerator = Refrigerator()
    main_recipe_book = Food.Recipe_book()
    main_refrigerator.make_dish(main_recipe_book.data[0])
    

  • Class Food
class Food:
    def __init__(self, name, use_by_date, stock):
        self.name = name
        self.use_by_date = use_by_date
        self.stock = int(stock)

    def update_name(self, new_name):
        self.name = new_name

    def update_use_by_date(self, new_use_by_date):
        self.use_by_date = new_use_by_date

    def update_stock(self, new_stock):
        self.stock = new_stock

    def reduce_stock(self):
        self.stock -= 1
        if self.stock == 0:
            return False # 재고가 다 떨어져 자동으로 삭제될 경우 False
        return True # 재고가 남았을 경우 True

    def get_date(self):
        return self.use_by_date

    # def get_name(self):
    #     return self.name

    def __str__(self): # print(Ingredient)
        return f"{self.name:<6} : {self.use_by_date} ({self.stock:>2})"

class Ingredient(Food):
    type = "ingredient"

class Dish(Food):
    type = "dish"
  • Class Recipe_book
class Recipe_book:

    class recipe:
        def __init__(self, name, ingredients):
            self.name = name
            self.ingredients = ingredients # ingredients : 요리에 사용되는 식재료(Ingredient)를 포함하는 list

        def __str__(self):
            return f"{self.name:<10} : {', '.join(self.ingredients)}"

    def __init__(self):
        self.data = list()

        for line in FM.File_manager.read_file("Recipe.txt"):
            if len(line) < 3:
                continue
            name, ingredients = line.strip().split('/')
            ingredients = ingredients.split(',')
            self.data.append(self.recipe(name, ingredients))

    def create_recipe(self, name, ingredients):
        self.data.append(self.recipe(name, ingredients))

    def save_recipe(self):
        contents = list()
        for item in self.data:
            item_str = item.name + "/" + ",".join(item.ingredients)
            contents.append(item_str)
        FM.File_manager.write_file("Recipe.txt", contents)

    def print_all_recipe(self):
        for i in range(len(self.data)):
            print(f"{i+1}. {self.data[i]}")
  • 실행을 위한 메인 메소드 입니다. 
import Refrigerator
import Food

# User Interface
opening_str = """[Refrigerator CRUD]"""

choice_str = """\
*******************
[1] CREATE
[2] READ
[3] UPDATE
[4] DELETE
[5] Additional Features
[6] Exit
*******************

Enter the number : """

additional_str = """\
*******************
[1] Check Use by Date
[2] Food Consumption
[3] Cookable Dish
[4] Cook Dish
[5] Check Recipe
[6] New Recipe
[7] Return to main menu
*******************

Enter the number : """

division_str = "\n********************\n"

update_str = """\
*******************
[1] UPDATE name
[2] UPDATE use by date
[3] UPDATE stock
*******************

Enter the number : """

return_msg = "Returning to the main menu"
error_msg = "error : " + return_msg

print(f"\n{opening_str}\n")

main_refrigerator = Refrigerator.Refrigerator()
main_recipe_book = Food.Recipe_book()

while True:
    try:
        choice = (input(choice_str))
        choice = int(choice)
        print(division_str)
        print()

        if choice == 1:  # create - insert ingredient to the refrigerator
            name = input("name : ")
            use_by_date = input("use by date (YYYY-MM-DD) : ")
            stock = input("number of stock : ")
            main_refrigerator.ingredient_create(name, use_by_date, stock)

        elif choice == 2:  # show all items in refrigerator
            main_refrigerator.ingredient_read_all_by_date()

        elif choice == 3:  # update information of ingerdiens
            main_refrigerator.ingredient_read_all()
            idx = int(input("Enter the number to modify : "))
            update_op = int(input(update_str))
            if update_op == 1: # name update
                main_refrigerator.ingredient_update_name(idx, input(f"new name of ({main_refrigerator.data[idx]}): "))
            elif update_op == 2: # use by date update
                main_refrigerator.ingredient_update_use_by_date(idx, input(f"new use by date of ({main_refrigerator.data[idx]})(YYYY-MM-DD) : "))
            elif update_op == 3: # stock update
                main_refrigerator.ingredient_update_stock(idx, int(input(f"new stock of ({main_refrigerator.data[idx]}) : ")))
            else:
                print(error_msg)


        elif choice == 4:  # delete a specific ingredient
            main_refrigerator.ingredient_read_all()
            idx = int(input("Enter the number to delete : "))
            removed = main_refrigerator.ingredient_delete_idx(idx)
            if removed:
                print(f"{removed} deleted")
            else:
                print(error_msg)


        elif choice == 5: # Adittional
            choice = int(input(additional_str))
            print()
            if choice == 1: # check use by date
                passed, near = main_refrigerator.check_use_by_date()
                if passed:
                    print("***** Expired *****")
                    for item in passed:
                        print(item)
                if near:
                    print("***** near date *****")
                    for item in near:
                        print(item)
                if (not passed) & (not near):
                    print("all items has enough time.")

            elif choice == 2: # Food consumption
                print()
                main_refrigerator.ingredient_read_all()
                idx = int(input("Enter the number to consume: "))
                consumption = int(input("consumption : "))
                temp_name = main_refrigerator.data[idx].name
                if consumption > main_refrigerator.data[idx].stock:
                    print("not enough stock")
                    print(return_msg)
                for i in range(consumption):
                    if not main_refrigerator.reduce_stock(idx):
                        print(f"all {temp_name} consumed")
                input()


            elif choice == 3: # Cookable Dish
                flag = False
                for recipe in main_recipe_book.data:
                    if main_refrigerator.check_condition(recipe):
                        print(recipe)
                        flag = True
                if not flag:
                    print("nothing available")

            elif choice == 4: # Cook Dish
                print(main_recipe_book.print_all_recipe())
                if not main_refrigerator.make_dish(main_recipe_book.data[int(input("choose a number : ")) - 1]):
                    print("insufficient ingredients")

            elif choice == 5: # Check Recipe
                main_recipe_book.print_all_recipe()

            elif choice == 6: # New Recipe
                name = input("Dish name : ")
                ingredients = list()
                ingredients_cnt = int(input("number of ingredients (int) : "))
                for i in range(ingredients_cnt):
                    ingredients.append(input(f"ingredient{i+1} : "))
                main_recipe_book.create_recipe(name, ingredients)

            elif choice == 7: # Return to Main Menu
                print(return_msg)

            else:
                print(error_msg)

        elif choice == 6:
            main_refrigerator.save_refrigerator()
            main_recipe_book.save_recipe()
            print("file saved")
            print("exit program")
            break

        else:
            print(error_msg)

        input() # 입력 대기 : 바로 메인화면으로 넘어가는것을 방지.
    except:
        print(error_msg)
        if choice == "exit":
            break

프로젝트 리뷰

구현을 위해 힙 자료 구조에 대해 정확하게 공부할 수 있는 좋은 기회였습니다. 또한 프로젝트 구현 전, 제안서를 작성하면서 관련 다이어그램을 그려볼 수 있었습니다. 또한 파이썬 코딩 실력이 뛰어나신 팀원 분과 협업을 통해 개발을 하면서 파이썬 코드에 대해서도 많이 배우고, 새로운 모듈 등에 대해서도 공부할 수 있었습니다. 


소스 코드 및 관련 문서

DSC2_팀프로젝트_박준석, 김승윤.zip

0.98MB