파이썬 프레임워크나 라이브러리에서 데코레이터는 자주 쓰이는데, 막상 일반 비즈니스 로직을 짤 때는 별로 안 쓰는 경우가 많다. 데코레이터 패턴이 어떻게 코드를 깔끔하게 만들고 유지보수하기 쉽게 해주는지 공유하고자 한다.

 

 

데코레이터 패턴 안 쓴 경우

 

우선 아래의 데코레이터를 사용하기 전 중복(복붙)이 많은 코드를 보라.

모든 메소드마다 로그인이 되어 있는지 체크하는 로직이 있고, 자기 본연의 비지니스 로직 외 로그인 중복을 체크하는 로직이 뒤섞여 있다. 이는 유지보수 시 굉장한 문제가 된다. 데코레이터 패턴을 사용해 깔끔한 코드를 만들어보자.

import os
from cloud_storage_client import CloudStorageClient
from exceptions import CloudStorageLoginFailedException

class CloudStorageManager:
    def __init__(self, access_key: str, secret_key: str, bucket_name: str):
        self.access_key = access_key
        self.secret_key = secret_key
        self.bucket_name = bucket_name
        self._is_logged_in = False
        self.client: CloudStorageClient | None = None

    @property
    def is_logged_in(self) -> bool:
        return self._is_logged_in

    def sign_in(self):
        if self.is_logged_in:
            return True

        try:
            self.client = CloudStorageClient(self.access_key, self.secret_key, self.bucket_name)
            self._is_logged_in = True
        except Exception as e:
            self._is_logged_in = False
            return False
        return True

    def upload_file(self, file_path: str, object_name: str = None):
        if not self.is_logged_in:
            raise CloudStorageLoginFailedException("로그인이 필요합니다.")
        if not object_name:
            object_name = os.path.basename(file_path)
        return self.client.upload_file(file_path, self.bucket_name, object_name)

    def download_file(self, object_name: str, download_path: str):
        if not self.is_logged_in:
            raise CloudStorageLoginFailedException("로그인이 필요합니다.")
        return self.client.download_file(self.bucket_name, object_name, download_path)

    def list_files(self):
        if not self.is_logged_in:
            raise CloudStorageLoginFailedException("로그인이 필요합니다.")
        return self.client.list_files(self.bucket_name)

    def delete_file(self, object_name: str):
        if not self.is_logged_in:
            raise CloudStorageLoginFailedException("로그인이 필요합니다.")
        return self.client.delete_file(self.bucket_name, object_name)
 

 

데코레이터 패턴 적용 후

 

require_sign_in 데코레이터를 사용함으로서 로그인 상태 확인 로직을 각 메소드에서 제거할 수 있다.

이렇게 하면 각 메소드가 본연의 기능에만 집중할 수 있어서 코드 읽기도 편해지고 관리하기도 편해진다.

import os
from functools import wraps
from cloud_storage_client import CloudStorageClient
from exceptions import CloudStorageLoginFailedException

def require_sign_in(func):
    @wraps(func)
    def wrapper(self: 'CloudStorageManager', *args, **kwargs):
        if not self.is_logged_in:
            if not self.sign_in():
                raise CloudStorageLoginFailedException("클라우드 스토리지 서비스에 로그인 할 수 없습니다.")
        return func(self, *args, **kwargs)
    return wrapper

class CloudStorageManager:
    def __init__(self, access_key: str, secret_key: str, bucket_name: str):
        self.access_key = access_key
        self.secret_key = secret_key
        self.bucket_name = bucket_name
        self._is_logged_in = False
        self.client: CloudStorageClient | None = None

    @property
    def is_logged_in(self) -> bool:
        return self._is_logged_in

    def sign_in(self):
        if self.is_logged_in:
            return True

        try:
            self.client = CloudStorageClient(self.access_key, self.secret_key, self.bucket_name)
            self._is_logged_in = True
        except Exception as e:
            self._is_logged_in = False
            return False
        return True

    @require_sign_in
    def upload_file(self, file_path: str, object_name: str = None):
        if not object_name:
            object_name = os.path.basename(file_path)
        return self.client.upload_file(file_path, self.bucket_name, object_name)

    @require_sign_in
    def download_file(self, object_name: str, download_path: str):
        return self.client.download_file(self.bucket_name, object_name, download_path)

    @require_sign_in
    def list_files(self):
        return self.client.list_files(self.bucket_name)

    @require_sign_in
    def delete_file(self, object_name: str):
        return self.client.delete_file(self.bucket_name, object_name)
 

 

 

'python' 카테고리의 다른 글

파이썬 데코레이터 활용(로깅)  (1) 2021.09.02
python oracle 데이터 json serialize(django)  (0) 2020.02.25
python super  (0) 2019.12.26
python 메모리 관리(memory allocation)  (4) 2019.11.15
클래스 관계 IS-A 상속  (0) 2019.11.07

Django 프로젝트에서 비지니스 로직에 영향을 주지 않고 최대한 깔끔하게 로깅 하는 방법을 찾다보니 데코레이터가 떠올랐다.(Spring 에서 많이 사용하는 AOP(Aspect Oriented Programming) 느낌으로 접근하면 이해가 편할 것 같다.)

 

로그를 남기는 데이터는 별것없다. 누가, 언제, 어떤 action 3개의 데이터만 해당 view 에 접근했을때 Logging DBinsert 하려한다.

view


DRF를 이용해서 view를 만들었다. @api_view 또한 데코레이터를 사용하기 때문에 순서가 중요하다.
api_view 먼저 실행 시키고 그다음에 나의 custom logging 데코레이터를 실행 하려한다.

@api_view(['GET'])
@Logging.log_decorator('고객을 가져오는 액션!')
def get_customer(request: Request) -> Response:



데코레이터


Logging Modelstatic 메소드로 데코레이터를 생성했다.
데코레이터가 무려 두개나 있는데 이는 action 이라는 매개변수를 전달 받기 위함이다.

(stackoverflow 참조: https://stackoverflow.com/questions/5929107/decorators-with-parameters)


function(*args, **kwargs)은 기존의 view 로직을 실행하는 구문이다.
필자는 기존의 view 로직을 수행하기 전에 로깅을 하기 원한다.
Logging 데이터를 추가하기 전에 argsapi_view 데코레이터를 처리한 뒤 반환된 값이라 Django에서 사용하는 user 객체를 이용할 수 있다. 해서 user 데이터를 args[0].user.username을 사용해서 추가했다.

필자가 실행한 전처리보다는 후처리를 하고 싶은 경우엔 # 후처리 부분에 로직을 넣으면 된다.

class Logging(TimeStampedModel):
    user = models.CharField(max_length=255)
    action = models.CharField(max_length=255)
    created = models.DateTimeField(auto_now_add=True)

    @staticmethod
    def log_decorator(action: str):
        def decorator(function):
            def wrapper(*args, **kwargs):
                try:  # 전처리
                    Logging.objects.create(user=args[0].user.username, action=action).save()
                except Exception as e:
                    print("###Error####", e)
                result = function(*args, **kwargs)
                # print('GoodBye~')  # 후처리
                return result

            return wrapper

        return decorator





결론 : 비지니스 로직에 Logging 로직을 넣지 말고 데코레이터를 이용해 넣자. 깔끔해진다.

cx_oracle

파이썬에서 oracle DB에 쿼리를 날릴려면 cx_oracle을 설치해야한다.(pip install cx_oracle)

 

Oracle Service Class
import cx_Oracle
import os
from cx_Oracle import Connection, Cursor


class OracleService:
    def __init__(self, query: str):
        self.result: list = []
        self.cursor: Cursor = self.init_env().cursor()
        self.get_result(query)

    def init_env(self) -> Connection:
        os.environ["NLS_LANG"] = ".AL32UTF8"
        dsn_tns = cx_Oracle.makedsn('ip', '1521', service_name='service_name')
        return cx_Oracle.connect(user=r'user', password='password', dsn=dsn_tns)

    def get_result(self, query) -> None:
        self.cursor.execute(query)
        dict_temp: dict = {}
        for (RPICU, RPTAX, RPALPH, RPDC, RPZ5DEBITAT, RPDL02, RPRMK) in self.cursor:
            dict_temp['RPICU'] = RPICU
            dict_temp['RPTAX'] = RPTAX.strip()
            dict_temp['RPALPH'] = RPALPH.strip()
            dict_temp['RPDC'] = RPDC.strip()
            dict_temp['RPZ5DEBITAT'] = str(RPZ5DEBITAT).strip()
            dict_temp['RPDL02'] = RPDL02.strip()
            dict_temp['RPRMK'] = RPRMK.strip()
            self.result.append(dict_temp)

1. 간단하게 init_env 메서드를 활용해서 Connection을 만든다.

  - os.envrion을 지정해줘야 한글이 깨지지 않는다.

 

2. get_result 메서드를 이용해서 쿼리를 날리고 결과를 result에 담는다.

 

너무 심플하다.

 

Django HttpResponse
    query = "select *  from 오라클 TABLE"
    service = OracleService(query)
    return HttpResponse(content=json.dumps(service.result), content_type='application/json')

1. django에서 httpResponse를 날릴때 json.dumps를 이용해서 json 데이터로 response를 보낼 수 있도록하자.

python에서 super란 정확히 무엇인가?? Django 프레임워크와 같이 상속을 이리저리 받고 MIXIN 쓰고 하면 헷갈린다.

 

 

super

super() alone returns a temporary object of the superclass that then allows you to call that superclass’s methods.

 

슈퍼(부모) 클래스의 임시 object를 리턴한다. 이 임시 object로 슈퍼 클래스의 메소드를 사용할 수 있다.!

그러니 super()라고 쓰면 부모 클래스의 object를 가져와서 부모 클래스의 say_hi() 같은 메소드를 사용할 수 있다는 말이다.

 

 

간단한 Super 예제(상속 전)
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square:
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length * self.length

    def perimeter(self):
        return 4 * self.length

square = Square(4)
print(square.area())

rectangle = Rectangle(2,4)
print(rectangle.area())

Square(정사각형)은 Rectangle(직사각형)의 특별한 케이스다. 그러므로 상속으로 코드를 줄일 수 있다.

 

간단한 Super 예제(상속 후)
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)
        
        
square = Square(4)
print(square.area())

rectangle = Rectangle(2,4)
print(rectangle.area())
        

이런식으로 Rectangle을 상속 받아서 사용하면 부모 클래스인 Rectangle의 are() 메소드를 사용할 수 있다.

Square의 __init__에서 super().__init__(length, length)라고 코딩했는데 이 호출에 따라서 Rectangle의 Class 오브젝트를 생성한다. 이렇게 Rectangle 오브젝트를 생성해야 area 메소드를 사용할 수 있다. 왜냐하면 area 메소드는 self.length와 self.width를 가지기 때문이다. (__init__을 호출해야 self.length, self.width 값이 정확히 생성됨)

 

 

super() vs super(클래스 이름, self)

위의  예제에서는 super()라고 포현했는데 다른 코드를 보면 super(Squre, self).__init__() 이런 식으로 표현하는 곳도 볼 수있다.

super()와 super(Squre, self) 은 무엇이 다를까?

사실 super는 두개의 파라미터를 받는데 처음은 하위클래스의 이름, 두번째는 하위클래스의 object다. 위의 예제에서는

super()와 super(Squre, self) 두개로 표현될 수 있는데 위의 예제의 경우에는 어떻게 표현하든 똑같다.

하지만 다중 상속 및 할머니 상속일 경우 의미가 살짝 다른데 첫번째 매개변수의 클래스 이름에 따라 super 가 탐색하는 범위가 달라진다.

 

 

부모의 부모(할머니)로 부터 상속
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        print('할머니 area')
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width


class Square(Rectangle):
    def __init__(self, length):
        self.length = length
        super().__init__(length, length)

    def area(self):
        print('부모의 area')
        return self.length * self.length


class Cube(Square):
    def surface_area(self):
        face_area = super(Square, self).area()
        return face_area * 6

    def volume(self):
        face_area = super().area()
        return face_area * self.length


cube = Cube(3)
print(cube.surface_area())
# 할머니 area
# 54
print(cube.volume())
# 부모의 area
# 27

Cube Class의 surface_area와 volume 메소드는 모두 부모의 area 메소드가 필요하다.

이때 하나는 super(Square, self)를 하나는 super()를 사용했다. 

 

super(Square, self)를 사용한 surface_area는 할머니 즉 Rectangle에서 가져왔고 그냥 super()를 사용한 volume은 부모의 area를 사용했다.

 

아까 말했다 싶이 탐색범위가 달라진다고 했다 그러니깐 Square를 명시한 super는 Square의 부모로 부터 탐색한다. 그래서 Rectangle(할머니)의 area를 호출했다.

 

참고 사이트 : https://realpython.com/python-super/

Everything is object in Python 이라는 말이있다. 근데 정확히 무슨말인지 이해하기가 좀 까다롭다.

C언어는 메모리를 바로 까볼수 있어서 느낌이 팍팍 오는데 Python은 뭔가 애매하고 한글로된 자료가 많이 없어서 정리하고자 한다.

 

 

1.  Everything is object의 핵심 개념

 

변수와 object의 관계

1번(빨간색)

C언어에서 x = 10 이렇게 변수를 할당하면 메모리에 해당 값이 바로 박히지만 파이썬에서는 10이라는 int object를 만들어 놓고 변수 x가 그것을 가르키는 형태이다.(보통 레퍼런스라고 하며 Pointer의 개념, 진짜 중요한 말이다 이게 바로 Everything is object in Python의 핵심 개념!

 

 

그리고 y = x 라고 하면 x에 의해서 이미 만들어진 10 이라는 int object를 y는 그냥 가르키기만 한다. 그래서 x와 y는 같은 object를 가르킨다 라고 말한다. 결국 10 int object를 가르키는건 x, y 2개

 

2번(하늘색)

x = x +1은 11(10 + 1)이라는 새로운 int object를 생성한다. 그리고 x는 새롭게 만들어진 int object를 가르킨다.

그리고 z = 10은 10이라는 int object를 가르키는데 10 int object는 이미 생성되어 있으니 만들 필요 없고 그냥 가르키기만 한다.

 

3번(보라색)

z = Car()는 Car라는 object를 생성하고 z는 car object를 가르킨다.(아주 단순)

 

 

2-1.  메모리 할당(Stack, Heap)

메모리 할당

 

main 함수

main 함수에서 y=5( 5 int object 생성 and 가르키기)과 f1(y)함수를 Call 한다. f1(y)를 호출하면 Stack 영역에 f1()영역이 생성 된다.

 

f1 함수

f1 함수에서 x = x * 2(5 * 2)로 10 int object 생성 및 가르키기를 하고 f2(x)를 호출하면 Stack 영역에 f2()영역이 생성 된다.

 

f2 함수

f2 함수에서 x = x +1 (10 + 1)로 11 int object 생성 및 가르키기를 한다.

 

 

2-2.  메모리 할당 해제

 

메모리 할당 해제

 

할당 해제

우선 Stack의 가장 위에 있는 f2함수가 해제 된다.(특이사항 없다)
그다음 f1함수가 해제 되는데 f1함수의 변수 x가 없어짐에 따라 10 int object도 없어진다.(10 int object를 아무도 가르키지 않기 때문) 이것이 바로 reference counting이 0이 됨에 따라 object가 없어지는 가비지 컬랙터이다. 파이썬은 레퍼런스 카운팅을 이용해서 메모리를 관리한다. 원리는 f1함수가 해제될때 10 int object가 사라지는 것이 대표적이 예이다.

 

마지막으로 main 함수의 변수 z는 11을 가르킨다.

 

3. Class instance 메모리 관리(self는 정확히 무엇인가)

 

Class instance memory

 

할당 해제

main에서 c = Car(4)은 class instance를 생성하는데 처음에 생성자 함수 __init__이 호출된다.

여기서 self, w 2개를 매개변수로 받는데 이는 매개변수 즉 지역변수 이기 때문에 Stack 영역에 생성된다.

 

w는 당연히 4인데, self는 정확히 무엇일까? 그림에서 보듯 Car instance(object)의 주소(reference) 값이다. 그러니깐 모든 class 메소드는 self를 매개변수로 가지는데 self는 자신의 Class instance의 주소 값이다. 너무나 중요한 말이고 너무나 당연한 말이다. self를 매개변수로 받지 않으면 어떻게 instance값을 참조 할 것인가? 찾지를 못한다.

 

그리고 self.wheels은 instance의 변수 즉 속성(attribute)이다. attribute는 Heap 영역에 할당된다. 왜냐하면 __init__ 함수가 없어지고 Stack 영역이 사라져도 self.wheels은 존재해야 하니깐! 어려운 말인데 잘 생각해 보길 바란다. 

마지막으로 self.wheels은 w가 가르키는 4를 가르킨다.

 

 

 

메소드 실행

 

메소드 실행

c.getWheels()를 실행하면 Stack 영역이 만들어지고 매개변수로 self가 생성된다. self는 instance의 주소 값을 가지고 있는 변수이니 self를 통해 Heap 영역의 wheels 변수에 접근한다. 접근 값은 4이고 main함수의 n 값은 Heap영역의 4를 가르킨다.

 

4. 결론 및 정리

본 글은 Everything is object in Python을 이해하기 위한 글이다.

C의 메모리(Stack, Heap), 포인터의 개념이 있어야 확실히 정리가 될 것이다.

정말 기본중에 기본이니 알고 있는 것이 좋을듯하다!

 

참조 영상 : https://www.youtube.com/watch?v=arxWaw-E8QQ

인도 아저씬데 기가맥히다.....

'python' 카테고리의 다른 글

파이썬 데코레이터 활용(로깅)  (1) 2021.09.02
python oracle 데이터 json serialize(django)  (0) 2020.02.25
python super  (0) 2019.12.26
클래스 관계 IS-A 상속  (0) 2019.11.07
python openpyxl을 이용한 Django DB insert  (0) 2019.06.21

한번씩 기술 블로그를 볼때마다 IS-A , HAS-A 어쩌고 저쩌고 하던데 컴퓨터 사이언스 부트캠프 with 파이썬 책에 잘 나와있어서 정리해본다

 

IS-A: 상속

 - IS-A는 ~은 ~의 종류다 라는 뜻인데 A laptop IS-A computer 즉 랩탑(노트북)은 컴퓨터의 한 종류다! 이 예시가 제일 적절하다. 그러니깐 컴퓨터는 부모 클래스고 노트북은 자식 클래스이다.

 

컴퓨터와 노트북은 IS-A 상속 관계

class Computer:

    def __init__(self, cpu, ram):
        self.cpu = cpu
        self.ram = ram

    def browse(self):
        print('브라우저로 웹서칭')

    def work(self):
        print('일하기')


class Laptop(Computer):

    def __init__(self, cpu, ram, battery):
        super().__init__(cpu, ram)
        self.battery = battery

    def move(self):
        print('노트북은 이동 가능해요')


laptop = Laptop('2core', '4GB', '100%')
print(laptop.cpu)
laptop.move()

위와 같은 관계를 IS-A라고 부른다! ~은 ~의 한종류다.

엑셀파일

위와같은 엑셀파일을 Django ORM을 이용해서 insert 하고자한다.

 

소스코드


import openpyxl
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "subul.settings")
django.setup()

from order.models import Order
from core.models import Location
from product.models import ProductCode

import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "subul.settings")
django.setup()

 

위의 3코드는 Django의 설정파일을 set해주고 django.setup()으로 django를 실행한다고 생각하면 편하겠다

 


wb = openpyxl.load_workbook('productOrder.xlsx') # productOrder.xlsx 파일을 열어서 wb 변수에 할당
sheet1 = wb['Sheet1'] # 엑셀의 Sheet1을 open
rows = sheet1['A3':'L310'] # sheet1의 A3부터 L310까지 rows 변수에 할당

for row in rows:

    # rows는 여러줄이니 row는 한줄

    # row[0]은 가로 한줄의 제일 첫번째 세로칸 즉 A3,B3.... L3


     dict = {}
     dict['id'] = row[0].value
     dict['type'] = row[1].value
     dict['specialTag'] = row[2].value
     dict['ymd'] = row[3].value
     dict['orderLocationName'] = row[5].value
     dict['codeName'] = row[6].value
     dict['amount'] = row[7].value
     dict['count'] = row[8].value
     dict['price'] = row[10].value
     dict['memo'] = row[11].value

     product_Instance = ProductCode.objects.get(codeName=dict['codeName'])
     toLocation_instance = Location.objects.filter(codeName=dict['orderLocationName']).filter(type='05').first()
     dict['code'] = product_Instance.code
     dict['productCode'] = product_Instance
     dict['orderLocationCode'] = toLocation_instance
     Order(**dict).save() # 킬링포인트 **딕셔너리를 활용해서 쉽게 insert


'python' 카테고리의 다른 글

파이썬 데코레이터 활용(로깅)  (1) 2021.09.02
python oracle 데이터 json serialize(django)  (0) 2020.02.25
python super  (0) 2019.12.26
python 메모리 관리(memory allocation)  (4) 2019.11.15
클래스 관계 IS-A 상속  (0) 2019.11.07

+ Recent posts