關於最近遇到的坑 - if queryset

背景python

在python語法中,if obj是一種很簡潔優雅的語法糖,能夠用來判斷字符串是否爲空,某個參數是否爲None,列表是否爲空。因此,在面對queryset對象時便堅決果斷的用if queryset來作判斷,致使了性能問題。數據庫

class PageNumberPaginator(PageNumberPagination):
    .....省略無關代碼

    def paginate_queryset(self, queryset, request, ModelClass, view=None):
        self.total = None if queryset else get_total(queryset, ModelClass)
        return PageNumberPagination.paginate_queryset(self, queryset=queryset, request=request)

    def get_paginated_response(self, data, total=None):
        query_count = self.page.paginator.count

        return Response(OrderedDict([
            ('result', 'ok'),
            ('paging', OrderedDict([
                ('draw', self.draw),
                ('page', self.page_param),
                ('page_size', self.page_size),
                ('count', query_count),
                ('total', total if total else self.total or query_count),
                ])),
            ('data', data)
            ]))

如今就來具體拆分並分析爲何不能用if queryset來判斷queryset是否爲空。django

 

技術拆解-關於if判斷併發

關於if判斷的問題其實就是bool類型的判斷。默認狀況下函數

1.if會嘗試bool(obj)性能

2.調用obj.__bool__()fetch

3.調用obj.__len__()spa

具體解析,在判斷布爾類型時,if會調用內置的bool(obj),此函數只能返回True或False。bool(obj)的背後會先嚐試調用obj.__bool__()方法,存在則返回obj.__bool__()方法的結果,若是不存在,bool(obj)會嘗試調用x.__len__()。若返回0,則bool返回0,不然True。code

因此咱們能夠本身擴展並撰寫一個自定義知足布爾類型判斷的對象。對象

class LenFunc:
    def __len__(self):
        print('len step >>>>>>>')

        return 1

    def __repr__(self):
        return 'LenFunc for __len__ test'


class BoolFunc:
    def __bool__(self):
        print('bool step >>>>>>>')
        return True

    def __repr__(self):
        return 'BoolFunc for __bool__ test'


class MixFunc:
    def __len__(self):
        print('len step >>>>>>>')
        return 1

    def __bool__(self):
        print('bool step >>>>>>>')
        return True

    def __repr__(self):
        return 'MixFunc for __len__&__bool__ test'


func_list = [LenFunc(), BoolFunc(), MixFunc()]

for obj in func_list:
    print(obj)
    if obj:
        print(obj, 'is True \r\n')

運行結果以下:

特殊方法的調用是隱式的,通常狀況下,咱們是不須要關注這些內容的,除非對於咱們自定義的類型進行擴展,想嘗試python的內置語法糖,才須要知足協議。

另外若是是python內置的類型,好比list, str, bytearray那麼CPython會抄近路,__len__實際上會直接返回PyVarObject裏的ob_size屬性。PyVarObject是表示內存中長度可變的內置對象的C語言結構體。直接讀取這個值比調用一個方法快多了。

 

技術拆解-queryset

queryset其實是Django的內置的Queryset的實例。具體由來這邊就不作闡述了,有興趣的能夠翻閱源碼。下面貼上QuerySet的定義

 

相信對django熟悉的都知道queryset對數據庫的查詢結果是懶加載的。其實,真正的加載查詢數據庫數據的步驟,在__len__和__iter__均可以發現。

在__len__中會有兩步調用

class QuerySet(object):
    .....

    def __len__(self):
        self._fetch_all()
        return len(self._result_cache)

    def _fetch_all(self):
        if self._result_cache is None:
            self._result_cache = list(self._iterable_class(self))
        if self._prefetch_related_lookups and not self._prefetch_done:
            self._prefetch_related_objects()

第一步self._fetch_all()會查詢可迭代對象內全部結果,即進行數據庫查詢操做,賦值數據庫查詢的結果列表給當前實例queryset的_result_cache屬性,把查詢結果加載內存中。

第二步返回查詢數據結果列表的長度

 

 

綜上分析緣由

當在分頁查詢內進行if queryset判斷時,默認先尋找__bool__方法,當前對象沒有,便會調用__len__方法。此時queryset做爲一個懶加載對象,自己是在分頁完聚焦在分頁的那幾條數據(10, 20 ,50, 100 whatever)進行數據庫查詢,卻由於在if調用了__len__方法而提早被觸發數據庫查詢。

當面臨查詢結果特別大的時候,如須要被分頁的結果有4w條,數據便會被加載在內存中,致使內存佔用太高,特別是在大併發的線上環境,尤爲會影響性能,形成內存佔用,內存緊張,影響服務總體性能,形成卡頓,甚至服務直接down掉

 

建議使用queryset.exists()來替代

def exists(self):
    if self._result_cache is None:
        return self.query.has_results(using=self.db)
    return bool(self._result_cache)

 

django版本1.11.2

若是有興趣,你們能夠翻閱下源碼,具體瞭解內容。

相關文章
相關標籤/搜索