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!
如觉有用点个赞呗~

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