SELECT 쿼리를 사용하여 DB의 값을 가져올 때, float, double과 같은 타입일 경우에 psycopg2가 Decimal 타입으로 가져오게 됩니다. 가져온 값을 그대로 다른 DB에 넣을 때 Decimal타입을 변경해줘야 하는데 귀찮으니 애초에 DB 값을 가져올 때, 아래와 같이 제거해서 결과를 출력할 수 있습니다.

from psycopg2.extensions import new_type, DECIMAL


DEC2FLOAT = new_type(DECIMAL.values, 'DEC2FLOAT', lambda value, curs: float(value) if value is not None else None)
psycopg2.extensions.register_type(DEC2FLOAT)
conn = psycopg2.connect()
cursor = conn.cursor(cursor_factory=RealDictCursor, )


먼저 우리가 흔히 아는 특수문자는 반각문자 입니다. (키보드에 존재하는 특수문자 !@#$% 등등) 전각문자는 윈도우 한자 키를 사용하여 생성된 특수문자입니다. (123abc?!등등)

전각문자를 사용해도 표현은 되지만 문자 사이의 간격이 반각보다 커서 가독성 문제나 123과 같은 숫자가 전각일 경우, 문자로 인식되는 것등의 문제가 있습니다. 

따라서 파이썬에서 전각문자를 반각문자로 변경하는 방법은 아래와 같습니다.


# 전각문자
full = '!'
# 반각문자
half = '!'
# 전각문자와 반각문자의 차이
diff = '0xfee0'
# 전각문자 블랭크
blank = '0x3000'

# 16진수인 ascii code
hex_ascii_full = ord(full)
hex_ascii_half = ord(half)
hex_ascii_diff = int(diff, 16)
hex_ascii_blank = int(blank, 16)

# 16진수 형태의 string
hex_full = hex(hex_ascii_full)
hex_half = hex(hex_ascii_half)
hex_blank = hex(hex_ascii_blank)

# 전각일 경우 전각 기준인 값을 차감해 반각으로 변경
if hex_ascii_full >= hex_ascii_diff:
    result = hex_ascii_full - hex_ascii_diff
# 빈칸이 전각일 경우는 위 공식에 어긋나므로 강제로 반각형태의 빈칸을 지정
elif hex_ascii_full == hex_ascii_blank:
    result = hex_blank


assert chr(result) == '!'


파이썬3은 string이 전부 unicode입니다. 따라서 ord() 함수를 사용하여 문자열을 아스키코드로 변환할 수 있습니다. (= Hexadecimal Ascii Code) 다음 변환된 아스키 코드를 hex()로 감싸면 원하는 유니코드표(=16진수)의 값을 확인할 수 있게 됩니다. 이러한 작업을 한 다음, 비교문을 통해 0xfee0 값 보다 큰 값은 전부 전각문자이므로 16진수 형태의 문자와 차를 구하게 되면 반각문자를 구하게 될 수 있습니다. 계산된 전각문자는 16진수이므로 내장함수인 chr() 함수를 사용해 우리가 알고있는 문자로 변환을 하면 끝입니다. 

만약 반각 → 전각 문자로 변경하고자 하면 0xfee0 값 보다 작은 값을 찾아, 더해주면 됩니다.


삽질

코드는 다른 언어에 비해 간단하다고 할 수 있는데 가장 기본인 파이썬3의 모든 string이 unicode이다 이 부분을 까먹으면 혼돈에 빠지게 됩니다.


1. 먼저 첫 번째로 한 삽질은 str.encode('unicode_escape') 를 사용해 문자를 unicode 형식으로 변경할 수 있습니다. 하지만 변경된 문자는 b'\\uac00' 와 같은형식으로 바이트 타입입니다. 혹시나 해서 바이트 타입을 벗기면 \uac00 와 같은 string 타입이 나오게 됩니다.. 만약 반각일 경우는 \u가 없는 바이트 타입이 나오게 됩니다. 이러한 방식은 전각 ↔ 반각 변경에 전혀 쓸 수 없습니다. (하려면 할 순 있지만 코드가 방대하고 어려워지게 됩니다.)

2. 두 번째로는 바이트로 변경하여 전각문자와 반각문자의 차이값인 0xfee0 만큼 빼려고 시도했습니다. 하지만 바이트는 더하거나 뺄 수없고 시프트로 자리값을 밀어야 합니다. 이 방법 또한 구현하기 복잡하고 디버깅이 어려워 바로 포기했습니다.



간단한 문제를 다양한 방법으로 삽질을 통해 해결하긴 했지만 가장 기본인 파이썬3의 모든 string이 unicode이다 가 제일 중요합니다.

Postgresql에서는 order by 부분에 null값이 가장 먼저 나올지, 나중에 나올지 설정할 수 있습니다. 

order by의 예는 아래와 같습니다.

SELECT name
FROM test
ORDER BY name DESC NULLS FIRST;


해당 쿼리는 name 컬럼에 null값인 행을 가장 앞으로 정렬하게되는 쿼리입니다. 


아래는 위 쿼리를 Django ORM으로 똑같이 표현한 예입니다.

from django.db.models import F


Test.objects.order_by(F('name').desc(nulls_first=True))

null값을 가장 뒤로 보내고 싶으면 nulls_first 부분을 nulls_last로 변경해주면 됩니다.

쿼리에서 aggregation함수(count(), max(), first() 등등) 와 필요한 컬럼을 출력하는 방법은 아래와 같습니다.

SELECT name AS changed_name, count(count)
FROM test
GROUP BY name;


위와 같은 쿼리는 아래와 같이 장고 ORM으로 표현할 수 있습니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
).values('max_count', 'changed_name')


aggregation 함수를 사용하기 전, group by를 진행할 필드를 지정해 줍니다.

queryset = Test.objects.values('name')


group by를 진행할 필드를 작성한 다음, annotate()를 사용해서 aggregation과 변경할 이름을 작성합니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
)


마지막으로 호출하고자 하는 필드를 지정해 줍니다. 이때는 annotate()에서 변경한 이름을 지정합니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
).values('max_count', 'changed_name')


lambda, map, filter 같은 경우는 사실 가독성 부분에서 좋지 않다는 생각이 들어 지양하고 있었는데 장점이 있어서 정리해 봅니다.

lambda(람다) 함수

lambda (이후 '람다'라고 호칭)함수는 익명함수로 메모리를 절약하는 이점이 있습니다. 또한 가독성을 향상시킨다 라고도 나오지만 함수와 비교했을 때 가독성이 좋은건지는 잘 모르겠습니다. (개인적인 의견)

일반적인 함수는 객체를 만들고, 재사용을 위해 함수 이름(메모리)를 할당을 합니다.

# lambda 인수1, 인수2, ... : 인수를 이용한 표현식
sum = lambda a, b: a+b
result = sum(3,4)
print(result)
# 7

왜 사용할까?

익명함수이기 때문에 한번 쓰이고 다음줄로 넘어가면 힙(heap) 메모리 영역에서 증발하게 됩니다. (단발성으로 사용하게 될 때 유용)

자세하게 설명하자면 파이썬에서 모든 것이 객체로 관리되고 각 객체는 레퍼런스 카운터를 갖게 되는데 해당 카운터가 0 - 어떤 것도 참조를 하지 않게 되면 메모리를 환원하게 됩니다. - 이러한 역할을 하는 것이 가비지 컬렉터입니다.

모든 람다 함수가 사용 후 메모리에서 증발할까?

위의 예시와 같은 경우는 람다함수(객체)를 sum이라는 변수에 저장합니다. 위의 설명대로라면 sum()를 사용 후 다음 줄에서 삭제가 되어야 하지만 실제로는 삭제되지 않습니다. sum이라는 변수의 메모리에 람다함수의 메모리가 저장되어 있어있기 때문입니다. 람다함수가 사용 후 메모리에서 증발하기 위해선 함수의 인자값에 전달 되었을 때 증발하게 됩니다. 또 함수 내부에서 사용하는 람다일 경우 해당 함수가 종료되는 순간 메모리에서 증발합니다.

map 함수

내장함수이며 입력받은 자료형의 각 요소가 함수에 의해 수행된 결과를 묶어서 map iterator 객체로 반환합니다. map함수는 게으른 연산을 진행해서 메모리를 크게 절약할 수 있습니다. 게으른 연산을 진행하기 때문에 값을 한번 사용하면 사용했던 값은 다시 호출할 수가 없습니다.

# map(function, iterable)
# 예시
li = [1, 2, 3]
result = list(map(lambda i: i ** 2 , li))
print(result)
print(result)


# [1, 4, 9]
# result

게으른 연산(lazy evaluation)이란?

필요할 때만 가져다 사용합니다. 보통 iterator 객체들이 게으른 함수입니다. (아닐 수도 있음 )

iterator 객체

  • next() 메소드로 데이터를 순차적으로 호출 가능
  • 마지막 데이터 이후 next()를 호출하면 StopIteration 에러 발생
  • for 문을 사용할 때, 파이썬 내부에서는 임시로 list를 iterator로 변환해서 사용

iterator 예시

map()

li = [1, 2, 3]
result = map(lambda i: i * i, li)

print(type(result))


print(next(result))
print(next(result))
print(next(result))

# next(result) # StopIteration 발생


# 결과
# <class 'map'>
# 1
# 4
# 9

iter()

li = [1, 2, 3]
result = iter(li)

print(type(result))

print(next(result))
print(next(result))
print(next(result))
# next(result) # StopIteration 발생



# 결과
# <class 'list_iterator'>
# 1
# 2
# 3

generator expression

li = [1, 2, 3]
result = (val*val for val in li)

print(type(result))

print(next(result))
print(next(result))
print(next(result))
# next(result) # StopIteration 발생


# 결과
# <class 'generator'>
# 1
# 4
# 9


  1. 현승우 2019.01.16 21:01

    안녕하세요 lambda 설명하실 때
    ' 익명함수이기 때문에 한번 쓰이고 다음줄로 넘어가면 힙(heap) 메모리 영역에서 증발하게 된다. '
    라고 하셨는데 sum 이라는 객체에 선언했기 때문에 예시에서는 증발하지 않는 것이 맞는지 질문 드립니다.
    다른 함수들의 func인자에 lambda를 쓰는 경우에만 사용하고 나면 증발하게 되는 것 아닌가요?

    • 불곰1 2019.01.16 23:15 신고

      안녕하세요.

      말씀하신 부분이 정확하게 맞는 것 같습니다. sum 이라는 변수에 익명함수의 주소 값을 넣게 되므로 사용 후 다음 줄로 넘어가도 메모리에서 사라지지 않는 것을 확인했습니다.
      예시를 주신 것처럼 함수의 인자값에 lambda를 사용해야 메모리에서 삭제가 됩니다.

      좋은 의견 감사합니다.

Value()

Value(value, output_field=None)

value() 객체는 나타낼 수 있는 가장 작은 값을 표현할 수 있습니다. 예를 들어 쿼리에서 integer나 boolean 또는 string 을 표현하고자 할 때 Value()로 감싸서 나타낼 수 있습니다.

사실상 Value()를 직접 사용할 필요는 드뭅니다. F('foo') + 1 이라는 표현식이 있을 때, 장고는 암시적으로 1을 Value()로 감싸 복잡한 표현식에서 단순한 값을 사용하게 합니다. 만약 string 값을 표현식에 나타내고자 할 때 Value()를 사용하여 나타낼 수 있습니다. 대부분의 표현식은 Lower('name')과 같이 해석하고 있습니다.


output_field는 IntegerField() 나 BooleanField()와 같은 장고 모델에서 제공하는 인스턴스를 작성해야하고 장고는 데이터베이스에서 값을 가져온 후 값을 로드합니다. 일반적으로 데이터 유효성 검사와 관련된 argument(max_length, max_digits 등)가 표현식의 출력 값에 적용되지 않으므로 모델 필드를 인스턴스화 할 때는 arguments가 필요하지 않습니다.

F()

F(name)

F() 객체는 모델 필드의 값을 나타냅니다. 데이터베이스에서 파이썬 메모리로 데이터를 가져 오지 않고 모델 필드 값을 참조하고 사용하여 데이터베이스 작업을 수행 할 수 있습니다. 대신 장고는 F() 객체를 사용하여 데이터베이스 수준에서 필요한 작업을 설명하는 SQL 표현식을 생성합니다.

아래 예제에서 F() 객체의 사용법을 알 수 있습니다.


reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()


위 파이썬 구문은 reporter.stories_filed의 값을 데이터베이스에서 메모리로 가져와 파이썬 연산자를 사용하여 조작 한 다음 데이터베이스에 다시 저장했습니다. 아래는 F() 객체를 사용하여 위와 동일한 결과를 생성하는 구문입니다.

from django.db.models import F
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()


reporter.stories_filed = F ( 'stories_filed') + 1은 값을 인스턴스 속성에 할당 한 것처럼 보이지만 데이터베이스에 대한 연산을 설명하는 SQL 구문입니다.  장고는 F()의 인스턴스를 만나면 파이썬 연산자를 오버라이드하여 캡슐화된 SQL 표현식을 생성합니다. 이 경우 reporter.stories_filed가 나타내는 데이터베이스 필드를 증가 시키도록 데이터베이스에 지시합니다. reporter.stories_filed나 다른 어떠한 값이 있어도 데이터베이스에 의해 처리되기 때문에 파이썬은 알지 못합니다. 

위 방법으로 저장한 새 값을 얻기 위해선 다시 호출해야 합니다.

reporter = Reporters.objects.get(pk=reporter.pk)



위 구문을 .update()와 F() 객체를 사용하여 줄일 수 있습니다.

reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)


F()를 사용하면 race condition을 피할수 있다

F()의 또 다른 유용한 이점은 파이썬이 아닌 데이터베이스가 필드 값을 업데이트하면 race condition을 피할 수 있다는 것입니다. 

예를 들어, 첫 번째 예시(F()를 사용하지 않은 예시)를 두개의 파이썬 thread를 사용해 실행하면 A라는 thread가 값을 받아 파이썬 메모리에 저장할 때 B라는 thread는 값을 추출, 증가, 저장할 수 있습니다. A라는 thread는 값을 증가, 저장하게 되면 B 라는 thread가 작업했던 내용이 손실됩니다. (A는 B의 작업 이전에 값을 가져왔기 때문..)

F()를 사용하면 위 문제를 해결할 수 있습니다. 메모리에 가져온 값을 기반으로 작업하는 것이 아닌 save() 나 update()가 실행될 때, 데이터베이스의 필드 값을 기준으로 작업하기 때문입니다.

F()는 여러가지로 사용할 수 있다

filter()에서의 사용

SELECT a, b, c, d
FROM test
WHERE a = c


위와 같은 쿼리가 있다고 가정하면 F()를 사용하여 아래와 같이 표현할 수 있습니다.

from django.db.models import F

Test.objects.filter(a=F('c'))

annotate()에서의 사용 - 필드명 변경

SELECT a, b, c, d as dddd
FROM test


위처럼 d라는 컬럼의 이름을 dddd로 변경하고자 할 때 F()를 사용하여 아래와 같이 표현할 수 있습니다.


from django.db.models import F

Test.objects.annotate(dddd=F('d'))

요약

  • F()와 Value()는 연관이 없다.
  • Value()는 사실상 안써도 무관하다.
  • F()는 좋다.



참조문서:

https://docs.djangoproject.com/en/1.7/ref/models/queries/#f-expressions

https://docs.djangoproject.com/en/2.0/ref/models/expressions/#value-expressions

https://docs.djangoproject.com/en/1.7/topics/db/queries/#using-f-expressions-in-filters

+ Random Posts