Closure

What is closure

Closure 簡單來說,就是某函數在另一個函數內被創造並且參照了創建函數的某些變數,此時該變數會存留於記憶內,儘管創建函數已經結束。

First time meet to Closure

N年前在學習 Common Lisp 時教學內出現了一個陌生又奇特的技巧,Closure,以下是他的實做

(let ((counter 0))
  (defun reset ()
    (setf counter 0))
  (defun stamp ()
    (setf counter (+ counter 1))))
(list (stamp) (stamp) (reset) (stamp))
; (1 2 0 1)

為了怕正常人看不懂,以下用 Python 翻譯

def gen_counter():
    counter = 0

    def reset():
        nonlocal counter
        counter = 0
        return counter

    def stamp():
        nonlocal counter
        counter += 1
        return counter

    return reset, stamp


reset, stamp = gen_counter()
print(stamp())                  # 1
print(stamp())                  # 2
print(reset())                  # 0
print(stamp())                  # 1

可以看出在 gen_counter 內的 兩個函數 (reset, stamp) 一同共用內部變數 counter 儘管 gen_counter 已經回傳並且結束,但是在之後的程式卻還是擁有當初初始化的 count,亦即 counter 在記憶體中不會因為 gen_counter 已經回傳就被回收。

Django Q

What is Django Q

Django 的 ORM 十分的簡易讓新手們可以簡單的寫出一般的增刪改查,但是如果要用比較進階的搜尋 (SQL WHERE CLAUSE),例如:正常人都寫的出來的 OR

SELECT * FROM post
WHERE content LIKE '%HELLO%' OR title LIKE '%HELLO%';

在 Django ORM 就必須要使用 Q 來達成

from django.db.models import Q


Post.objects.filter(
    Q(content__contains='HELLO') | Q(title__contains='HELLO')
)

其中使用 | 作為 OR 所有的 Q 就如同原本寫 ORM 的條件

Dancing with

在專案中有一項很常見的功能,就是關鍵字搜尋,很容易想像的是,如果使用一般的 SQL 就用 LIKE 慢慢組起來,但是在每個需要搜尋的功能中使用 Django Q 來組建實在很不好維護,以下利用簡化版的真實專案的案例演示 Closure 如何使它看起來更優雅

models.py

from django.db import models
from django.contrib.postgres.fields import JSONField


class Content(models.Model):
    title = models.CharField(max_length=200)
    title_pinyin = models.CharField(max_length=400)
    tags = JSONField(default=list)
    content = models.TextField()
    content_pinyin = models.TextField()

sql.py

from django.db.models import Q


def gen_keywords_search():
    q = Q()

    def icontains(contain=None):
        nonlocal q
        if contain:
            q = (q |
                 Q(title__icontains=contain) |
                 Q(title_pinyin__icontains=contain) |
                 Q(tags__icontains=contain) |
                 Q(content__icontains=contain) |
                 Q(content_pinyin__icontains=contain))
        return q
    return icontains

views.py

from rest_framework.decorators import api_view

from api.page import pager

from .models import Content
from .sql import gen_keywords_search
from .serializer import PostListSerializer, SearchContentSerializer


@api_view(['post'])
def search(request):
    serializer = SearchContentSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    data = serializer.data
    qs = request.user.post_set.all()

    if 'keywords' in data:
        q = gen_keywords_search()
        [q(key) for key in data['keywords'].split()]
        qs = qs.filter(q())

    return pager(
            request,
            qs,
            PostListSerializer,
            page_size=5
        )