背景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
若是有興趣,你們能夠翻閱下源碼,具體瞭解內容。