Flask之強大的first_or_404

基礎用法:

在Flask框架內,使用SQLAlchemy 來進行ORM數據庫查詢,示例如下:

# 從User表中查詢數據
user = User.query.filter_by(username="張三").first()

這種寫法,需要自己對結果進行判空:

# 從User表中查詢數據
user = User.query.filter_by(username="張三").first()
if user:
	# do something

但是,Flask提供了更爲便捷的方法:first_or_404

user = User.query.filter_by(username="張三").first_or_404()

當查詢結果不存在時,會主動拋出異常,跳轉到Flask默認的404頁面!返回頁面如下

在這裏插入圖片描述


first_or_404內部實現

此代碼位於 site-packages \ flask_sqlalchemy \ __init__.py

from sqlalchemy import orm

class BaseQuery(orm.Query): # Flask-SQLAlchemy是 SQLAlchemy的進一步封裝
	... # 以上部分代碼省略
    def first_or_404(self, description=None):
      
        rv = self.first() # Step1 執行firest() 對數據庫進行查詢
        if rv is None: # Step2 判斷數據庫返回結果是否爲空
            abort(404, description=description)
        return rv # Step3 返回firest()查詢結果

初步探尋

當查詢拿結果爲空時,函數執行abort(404, description=description)代碼,我們來一探究竟。

# site-packages\werkzeug\exceptions.py

def abort(status, *args, **kwargs):
    """
    爲給定的狀態碼或WSIG應用報錯HTTPException 錯誤
	如果給定狀態代碼,將引發該異常。如果是通過WSGI應用程序發起的請求,
	將把它包裝成代理WSGI異常並引發:

       abort(404)  # 404 Not Found
       abort(Response('Hello World'))
    """
    return _aborter(status, *args, **kwargs) # 調用Aborter()類的call方法

_aborter = Aborter() # _aborter 爲 Aborter() 函數實例化對象

Aborter()類源碼:

# site-packages\werkzeug\exceptions.py

class Aborter(object):
    """
    當傳遞一個字典或code,報出異常時 exception items 能作爲以可回調的函數使用
    如果可回調函數的第一個參數爲整數,那麼將會在mapping中虛招對應的值,
    如果是WSGI application將在代理異常中引發
    其餘的參數被轉發到異常構造函數。
    """

    def __init__(self, mapping=None, extra=None): # 構造函數
        if mapping is None:
        	# 由 line 763 得知default_exceptions 爲字典
            mapping = default_exceptions 
        self.mapping = dict(mapping) # 再次進行轉化
        if extra is not None:
            self.mapping.update(extra)

    def __call__(self, code, *args, **kwargs):
        if not args and not kwargs and not isinstance(code, integer_types):
            raise HTTPException(response=code)
        if code not in self.mapping: # 如果code 不在 mapping中
            raise LookupError("no exception for %r" % code)
        # 報出mapping中對應函數的錯誤
        raise self.mapping[code](*args, **kwargs)

此時,整個流程脈絡爲:
在這裏插入圖片描述


查看mapping

# site-packages\werkzeug\exceptions.py

default_exceptions = {}
"""
通過在模塊文件中設置 __all__ 變量,
當其它文件以“from 模塊名 import *”的形式導入該模塊時,
該文件中只能使用 __all__ 列表中指定的成員。
"""
__all__ = ["HTTPException"] 


def _find_exceptions(): # 暫且忽略此函數作用
	# globals() 函數會以字典類型返回當前位置的全部全局變量。
	# iteritems()返回一個迭代器
    for _name, obj in iteritems(globals()): 
        try:
        	# issubclass() 方法用於判斷參數 obj 是否繼承自 HTTPException 
            is_http_exception = issubclass(obj, HTTPException)
        except TypeError:
            is_http_exception = False
        if not is_http_exception or obj.code is None:
            continue
        # 將繼承自HTTPException 的obj 類名添加至__all__
        __all__.append(obj.__name__) 
        old_obj = default_exceptions.get(obj.code, None)
        if old_obj is not None and issubclass(obj, old_obj):
            continue
        # 更新default_exceptions字典  {404: "NotFound"}
        default_exceptions[obj.code] = obj

_find_exceptions() # 調用_find_exceptions() 函數
del _find_exceptions # 刪除引用 回收內存

函數_find_exceptions():

globals() 函數會以字典類型返回當前位置的全部全局變量。

class A:
    pass

class B:
    pass

print(globals())

# 結果如下: 
{'__name__': '__main__', 
... 省略了一部分,自行嘗試 ...
'A': <class '__main__.A'>, 'B': <class '__main__.B'> # 包含了所有類名與對象
}

僞代碼示例,幫助理解上述_find_exceptions():

from werkzeug._compat import iteritems

default_exceptions = {}
__all__ = []

class Base(object):
    code = 1

class A(Base):
    code = 2

class B(Base):
    code = 3

def _find_exceptions():
    for _name, obj in iteritems(globals()): 
        try:
            is_http_exception = issubclass(obj, Base)
        except TypeError:
            is_http_exception = False
        if not is_http_exception or obj.code is None:
            continue
        __all__.append(obj.__name__)
        old_obj = default_exceptions.get(obj.code, None)
        if old_obj is not None and issubclass(obj, old_obj):
            continue
        default_exceptions[obj.code] = obj

if __name__ == '__main__':
    _find_exceptions()
    print(default_exceptions)
    print(__all__)

# 運行結果如下
{1: <class '__main__.Base'>, 2: <class '__main__.A'>, 3: <class '__main__.B'>}
['Test', 'Base', 'A', 'B']

繼承自HTTPException的類們

exceptions.py 中構建了大量與對應error的函數,以下僅貼出403,404類

# site-packages\werkzeug\exceptions.py
...

class Forbidden(HTTPException):
    code = 403
    description = (
        "You don't have the permission to access the requested"
        " resource. It is either read-protected or not readable by the"
        " server."
    )
    
class NotFound(HTTPException):

    code = 404
    description = (
        "The requested URL was not found on the server. If you entered"
        " the URL manually please check your spelling and try again."
    )
 ...

HTTPException

# site-packages\werkzeug\exceptions.py

@implements_to_string
class HTTPException(Exception):
    """
    所有HTTP異常的基類。此異常可以被WSGI應用程序跳轉到錯誤頁面時調用,
    或者您可以捕獲子類獨立地呈現更好的錯誤消息頁面。
    """

    code = None # 錯誤狀態碼,403,404等
    description = None # 描述

    def __init__(self, description=None, response=None):
        super(HTTPException, self).__init__() # 繼承父類的Exception方法
        if description is not None:
            self.description = description
        self.response = response
    ...省略部分...
    
	def get_description(self, environ=None): # 返回描述內容
		"""Get the description."""
		return u"<p>%s</p>" % escape(self.description).replace("\n", "<br>")

    def get_body(self, environ=None): # 返回頁面主題內容
        """Get the HTML body."""
        return text_type(
            (
                u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
                u"<title>%(code)s %(name)s</title>\n"
                u"<h1>%(name)s</h1>\n"
                u"%(description)s\n"
            )
            % {
                "code": self.code,
                "name": escape(self.name),
                "description": self.get_description(environ),
            }
        )

    def get_headers(self, environ=None): # response headers
        """Get a list of headers."""
        return [("Content-Type", "text/html; charset=utf-8")]
	
    def get_response(self, environ=None):
       """
       返回錯誤響應信息,即開頭所看見的錯誤頁面
       """
       from .wrappers.response import Response
       if self.response is not None:
           return self.response
       if environ is not None:
           environ = _get_environ(environ)
       headers = self.get_headers(environ)
       return Response(self.get_body(environ), self.code, headers)
# site-packages\werkzeug\_internal.py

def _get_environ(obj):
    env = getattr(obj, "environ", obj)
    assert isinstance(env, dict), (
        "%r is not a WSGI environment (has to be a dict)" % type(obj).__name__
    )
    return env

回頭看mapping

mapping爲錯誤狀態碼及其繼承自HTTPException 類對象所構成的字典

raise self.mapping[code](*args, **kwargs)
raise self.mapping[404](*args, **kwargs)
raise NotFound(*args, **kwargs)
return Response(self.get_body(environ), self.code, headers)


對手動處理HTTPException說No

當使用first_or_404() 查詢語句之後,未查找到結果,報出HTTPException 404,將返回Flask默認的404頁面,這是不美觀的

此時,想手動處理HTTPException 404,讓它跳轉到自定義的404頁面。

try:
	user = User.query.filter_by(username="張三").first_or_404()
except Exceptions as e:
	# do something

FLask提供了app_errorhandler裝飾器, 攔截指定的error code,實現自己的方法。
ps:藍圖對象中也有此裝飾器
代碼示例如下:

from flask import Flask

app = Flask(__name__) 

@app_errorhandler(404)  # 也可以未其他error code
def not_found(e):
	return render_template("your 404.html"), 404

END!
如覺有用點個讚唄~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章