Giter Site home page Giter Site logo

blog's People

Contributors

tourun avatar

Watchers

 avatar

blog's Issues

Flask 消息流处理

  最近一直在使用Flask框架进行开发,虽然在大部分时间都在开发业务服务的相关代码,但是与之前在使用 Django开发一样,还是对其内部的实现逻辑充满好奇,也断断续续通过Flask的源码来了解其内部消息流处理的机制,但还未记录成一个完整的workflow,所以这里就总结下 Flask 在接收到请求之后,其内部消息流的一些处理逻辑。

Flask Framework

  Flask 是一个Python编写的轻量级web应用框架,其官方定义为微服务框架(micro framework)。这里的轻量级是相较于Django而言的,Django官方对其的定义为: The web framework for perfectionists with deadlines. 而它确实是一个终极杀器,Django内置了很多便捷的模块:DjangoForm、Django csrftoken、Django ORM、Django Cache……能够满足框架的大部分业务场景,能够让使用者更快速的上手,搭建一个功能完整的web application。Flask的设计理念与Django相反,它的设计更为灵活、轻便、易于扩展、可定制性更高,因此Flask只提供了web application的核心功能,其他的需要由用户来做决策,比如使用什么类型的ORM、如何进行Form Validation、或者如何处理用户login以及管理session……实际上这些常见的问题都有成熟解决方案,Flask提供了很多extension模块,在Flask Community上可以看到找到这些不同功能的extension。

  回归文章主题,通常将Flask作为web framework时,需要定义一个application对象,实际上它是一个Flask类实例,以下是一个简单的 app 创建方法以及应用程序的启动:

def create_app(config)
    app = Flask(__name__)
  	# 设置跨域资源共享
  	COSR(app, resources=[r"/external-api/*"], origins=[r"https?:.*"])
  	# 加载配置文件
    app.config.from_pyfile(config)
    # 设置数据库连接
		app.config['SQLALCHEMY_DATABASE_URI'] = construct_db_url(config)
    # init extensions
    database.init_app(app)
    babel.init_app(app)
    security.init_app(app)
    
    # 注册各个子模块的路由
    register_blueprint(app)
    
    return app
  
if __name__ == "__main__":
    app = create_app("config.py")
    app.run("0.0.0.0", port=5000)

  上面启动了一个简单的Flask web app,监听并处理5000端口的请求,注意这里只是用作演示,而在生产环境中,通常会使用ngixn配合gunicorn或者uwsgi来更加高效的处理http请求,因为Flask框架本身(也即是Python语言)处理并发的能力很弱。Flask 类定义了 __call__方法,所以app是一个可调用对象,当接收到请求时,实现了WSGI协议的gunicorn或者uwsgi的worker会调用app来处理请求:

def __call__(self, environ, start_response):
    """Shortcut for :attr:`wsgi_app`."""
    return self.wsgi_app(environ, start_response)

  wsgi_app 方法是整个Flask数据流处理的核心,定义如下,关于wsgi协议相关内容,以及参数的含义可以参考之前的一篇转载:

def wsgi_app(self, environ, start_response):
	  ctx = self.request_context(environ)
    error = None
    try:
        try:
	          ctx.push()
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.make_response(self.handle_exception(e))
        except:
            error = sys.exc_info()[1]
         		raise
        return response(environ, start_response)
     finally:
        if self.should_ignore_error(error):
            error = None
            ctx.auto_pop(error)

  要搞清楚wsgi_app方法中每个步骤的含义,需要先了解Flask中上下文(context)的概念。在使用Flask框架处理请求(实现API)时,通常会使用current_app对象来获取配置信息,使用request对象来获取请求参数信息,而current_apprequest作为全局变量,用户不需要考虑处理单元之间(比如线程)的安全问题,实际上在不同处理单元中,current_apprequest是相互独立的,当请求到来时,Flask app会创建处理该请求线程的上下文信息,其中current_apprequest分别为应用上下文(App Context)和请求上下文(Request Context)

App Context

The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.

The application context is created and destroyed as necessary. When a Flask application begins handling a request, it pushes an application context and a request context. When the request ends it pops the request context then the application context. Typically, an application context will have the same lifetime as a request.

Request Context

The request context keeps track of the request-level data during a request. Rather than passing the request object to each function that runs during a request, the request and session proxies are accessed instead.

When the Flask application handles a request, it creates a Request object based on the environment it received from the WSGI server. Because a worker (thread, process, or coroutine depending on the server) handles only one request at a time, the request data can be considered global to that worker during that request. Flask uses the term context local for this.

  在处理请求过程中,app context用于追踪application-level的数据,比如app的config配置信息,request context用于追踪request-level的数据,比如请求的param、method等。当Flask app收到请求时,它会先后push app context和request context,使context当前的处理单元绑定,当请求处理结束之后,Flask app会先后pop request context和app context,解除context与处理单元的绑定并销毁。在一个处理单元中生命周期中,可以将request、session视为全局变量。

理解Flask全局变量

   在深入了解wsgi_app方法之前,需要先掌握current_app、request、session这些核心数据类型。查看current_app、request、session的定义,会发现在Flask框架中,它们都是LocalProxy类实例:

# flask/global.py
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

  LocalProxy 是werkzeug中定义的代理类,它内部重写了 __setitem____getitem__ 等元方法,当对LocalProxy实例进行操作时,都会forward到它所代理的对象上,LocalProxy类的定义如下(这里只展示部分):

class LocalProxy(object):
  
    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
        if callable(local) and not hasattr(local, '__release_local__'):
            object.__setattr__(self, '__wrapped__', local)
    
    def _get_current_object(self):
        if not hasattr(self.__local, '__release_local__'):
            return self.__local() 
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__)
            
    def __getattr__(self, name):
        if name == '__members__':
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)
            
		def __setitem__(self, key, value):
        self._get_current_object()[key] = value
        
    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
    __getitem__ = lambda x, i: x._get_current_object()[i]

   在__init__方法中,通过 object.__setattr__ 定义了一个名为 __local 的实例属性,它的值指向在创建 LocalProxy 时传入的第一个参数,__local 实际就上LocalProxy的代理实现,定义了 __name__ 属性来与 __local 关联。需要注意的是在python中以双下划线的开头的变量或者方法表示私有,_LocalProxy__local 是python语言对于私有变量的一种特殊的访问方式。对私有变量的访问限制,python并没有C++那样严格:

Python performs name mangling of private variables. Every member with double underscore will be changed to _object._class__variable. If so required, it can still be accessed from outside the class, but the practice should be refrained.

  对LoalProxy对象的操作,并不直接forward到其 __local 属性上,而是先调用 _get_current_object() 方法,然后将对LocalProxy的操作forward到前者返回结果上。回到current_app、request、和session的定义,会发现它们代理的对象都是方法而非对象,current_app代理的是 _find_app 方法,request和session代理的是 _lookup_req_object 的偏函数。再结合LocalProxy中 _get_current_object() 方法的定义,可以看出,它返回的是这些函数的执行结果,这一点对于Flask中全局变量(current_app、request等)的在处理单元中的相互隔离非常重要,稍后会提到。

  接着来看下current_app、request、session代理的方法是如何定义的:

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

   current_app 代理的方法 _find_app 返回 _app_ctx_stask.top.app,request和session代理的 _lookup_req_object 偏函数分别返回 _request_ctx_stack.top中名为request和session的属性。从命名即可看出,_app_ctx_stack_request_ctx_stack 分别表示上文提到的应用上下问和请求上下文,它们都是LocalStack类型,这是一种后进先出的数据结构,且在栈顶进行操作,定义如下:

class LocalStack(object):
  	def __init__(self):
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()

    def _get__ident_func__(self):
        return self._local.__ident_func__

    def _set__ident_func__(self, value):
        object.__setattr__(self._local, '__ident_func__', value)
    __ident_func__ = property(_get__ident_func__, _set__ident_func__)
    
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)

    def push(self, obj):
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        stack = getattr(self._local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

  LocalStack对外提供了push、pop、top三个方法,其内部实现是将操作委托给内部成员 _local ,以 _local.stack 属性来模拟栈的行为,_local 是在初始化时会新建的一个Local类型的成员对象。LocalStack 类中定义了 __call__ 方法,因此它是一个可调用对象。在LocalStack的定义中引入了Local类型,再简单看下Local类的定义(部分):

class Local(object):
    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __call__(self, proxy):
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

  Local内部维护了一个名为 __storage__ 的字典结构,其内容都由这个字典结构来进行存储。Local类设计的巧妙之处在于,它定义了一个 __ident_func__ 属性,其值指向get_ident的方法,后者返回一个unique non-zero integer,用以标识当前的处理单元,处理单元可以是线程,也可以是协程(greenlet)。Local类重写了 __getattr____setattr__,当访问或修改Local对象属性时,会分别调用这两个方法,且这两个方法都是先通过 __ident_func__ 属性获取到当前处理单元的标识,然后对当前处理单元指向的数据结构(也是字典类型)进行操作。正是因为 __ident_func__ 属性能够标识当前的处理单元,所以在 getter/setter 时,操作的数据只对本处理单元有效,而对其他处理单元不可见,能够保证线程(处理单元)安全,不需要进行加锁或者其他同步操作。

  回到Flask框架,在API或者视图函数中会经常使用current_app、request、session这三个全局变量,这里的全局表示在进程的处理单元(比如线程/协程)**享,当以单进程多线程的方式来启动Flask应用时,打印current_app、request、session变量的id,会发现总是相同的,这常常会让初学Flask的人感到非常疑惑(至少我当时是这样的):为什么不同的线程,可以使用同一个变量来读取不同的上下文,处理不同请求,同时不需要处理线程安全问题。原因正是因为这些全局变量在实现时,使用了werkzeug定义的Local相关的数据结构,通过代理或者懒加载的技术与当前处理单元绑定,使接口变得更简单,也能让开发者更专注与业务逻辑的处理。

  总结下三个Local数据结构的作用,首先Local类将其内部的存储结构 __storage__ 属性与 __ident_func__ 绑定,使各个处理单元之间的操作互不干扰,相互隔离。LocalStack类内部定义了一个Local类的属性,并将对LocalStack实例的操作forward到其内部Local属性上,因此它也可以实现与Local类似的功能,区别在于LocalStack是一个后进先出的栈类型,所有操作都在栈顶。LocalProxy类的作用是对其代理的对象实现延迟绑定(懒加载)。比如,request是LocalProxy对象,它代理partial(_lookup_req_object, 'request')这个偏函数,当每次访问或操作request时会调用上述的偏函数,返回_request_ctx_stack.top.session,我们已经知道 _request_ctx_stack 是LocalStack类型,所以对它的访问都会绑定到当前处理单元。试想,如果request不通过LocalProxy对象来代理 partial(_lookup_req_object, 'request') 方法,而是直接指向 partial(_lookup_req_object, 'request') 返回的对象:

request = partial(_lookup_req_object, 'request')

  那么request的值始终是固定的,即始终指向首次访问它时的 _request_ctx_stack.top.request 。同理,如果session对象也使用上面的方式定义,那么不同处理单元便无法关联其自身上下文session。 下图展示了LocalProxy的运行原理:

wsgi_app 处理Flask消息流

  再次回到wsgi_app方法,我们来逐一看下它都完成了哪些操作。首先 ctx = self.request_context(environ),就是根据environ环境变量,为当前的处理单元创建一个RequestContext对象,它包含了所有与当前请求相关的信息。接着调用RequestContext的push方法(ctx.push),将其绑定到当前处理单元的上下文,push 方法实现如下:

class RequestContext(object):
	  # other methods
    def push(self):
      	top = _request_ctx_stack.top
        	if top is not None and top.preserved:
              top.pop(top._preserved_exc)

        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        _request_ctx_stack.push(self)
        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()

   我们已经知道 _request_ctx_stack 和 _app_ctx_stack 分别表示请求上下文和应用上下文,那么push方法进行了以下操作:

  • 将根据当前请求环境变量生成的RequestContext 对象,入栈(push)到_request_ctx_stack 中,即完成当前请求与_reques_ctx_stack的绑定,这一步非常关键。_request_ctx_stack 对象是LocalStack类型,它可以保证处理单元之间的隔离性,再结合globals.py中request和session的定义,可以知道在request和session指向的值是在这里与当前处理单元关联的,而实际上,它们指向的值就是RequestContext对象的同名属性
  • 将AppContext对象,入栈(push)到_app_ctx_stack中,后者同样为LocakStack类型,再根据globals.py中current_app的定义,可以知道current_app指向的值同样是在这里与当前处理单元关联的
  • 打开/创建session对象,Flask默认使用SecureCookieSessionInterface接口,默认从cookie中读取session信息,可以复写Flask的session_interface,从而修改session的读取/存储方式,比如使用自定义的redis_interface将session持久化到redis中。上文已经提到,在处理单元中使用的session全局对象,实际上就是RequestContext的session属性,也就是这里通过open_session得到的值

   在绑定请求上下文和应用上下文之后,调用Flask.full_dispatch_request对request进行分发处理:

def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        request_started.send(self)
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
        return self.finalize_request(rv)

  通过分析代码,在dispatch方法中,主要进行了如下操作:

  • 在处理request之前需要执行预处理操作,其中包含对before_first_requestbefore_request的钩子函数的调用,这些钩子函数通常是使用@app.before_first_request和@app.before_request装饰器包裹的函数,它们将会在请求真正处理之前被调用
  • 执行dispatch_request,它会根据请求url匹配到对应blueprint中定义的视图方法,这些方法通常是用来处理我们的业务逻辑,在执行完视图方法之后,根据其返回信息构造response对象,与处理请求时类似,在返回response之前,要先对通过after_this_request装饰器注册的钩子函数进行调用
  • 执行save_session,将在处理请求之前通过open_session新建的session保存起来,这里Flask默认仍然使用SecureCookieSessionInterface接口来完成,主要是对session信息进行签名,并保存在cookie中,通过http response返回到客户端。可以修改session_interface,复写save_session方法,将session持久化到redis或者其他存储

  在上述步骤执行完成之后,意味着当前处理单元的一次任务结束了,所以 wsgi 方法最后一步使用 ctx.pop 方法,将当前的上下文信息退栈,与入栈(push)相对应。

总结

   通过源码分析了Flask处理http请求的过程,简单总结,主要有以下步骤:

  • 接收由WSGI网关转发过来的http请求
  • 根据environ创建上下文(AppContext和RequestContext)
  • 上下文信息入栈(push),与当前处理单元关联
  • 分发请求(dispatch_request),根据请求的url信息,找到对应的method,处理业务逻辑
  • 生成response,上下文对象出栈(pop)
  • 通过WSGI网关将response返回给客户端

   用简单的UML图表示如下:

   实际上,Flask在处理消息流时,完成的操作比上述更加复杂,比如在处理消息的不同阶段,支持信号量的发送,方便其他模块完成一些异步操作,提高扩展性,比如在绑定AppContext时,会创建request级别的全局变量g,用来支持request级别的数据共享。

后记

   离上次post已经过去好久了,直到今天才整理完成一篇新的,文章只有5000字的篇幅,且半数都是源码,但是整理的过程却花费了很多的时间和精力。同时也越发觉得,就技术而言,相较于自己的知晓或者掌握,通过文字的方式记录下来并且能够使他人在阅读时,清晰的理解你所要表达的观点并从中得到收货,这种软技能也是自己所非常欠缺的,以后在这方面还是需要多加锻炼,提高自己的文字能力。

可视化图表选择经验

  要建立一个Dashboard来进行数据的可视化,需要针对不同的数据,来选择更合适的图表,这样展示的效果更加直观。数据以适合的方式进行展示才更有价值。要建立Dashboard,除了要选择合适的可视化图表之外,考虑它的受众群体同样重要,不同受众所关心的内容不用,需要获取的的信息不同,所以呈现的内容也不同,因此需要了解你的受众,来作出更明智的选择。

可视化图表选择经验

   这里结合项目中一些的应用场景,来谈谈数据可视化时,图表选择的经验,以及可视化数据背后的analytics api对应的 dimensions 和 metrics 之间的关系。

  • Big Number Chart (总览)
    如果想要对某一metric进行总览,比如查看本月的访问量、上季度的销售额等,到big number chart是最好的选择,同时它也是最容易构建的可视化类型,唯一的考虑因素是展示数据的时间范围,比如数据是整个历史的还是这一季度的。在数字一旁可以添加环比的上升或下降趋势,以体现近期的数据变化,比如下图:
    Big number chart 通常会放在Dashboard更明显的位置,比如最顶部,以便让读者最快的了解总览概况,同时也不宜太多,比如不要超过看板顶部一行,以为其他类型的图表留出展示空间。
    Big number chart 只展示一个metic,且不需要按照dimension进行聚合,可以提供一个时间过滤条件。
  • Line Chart (折线图)
    Line chart 表示为二维坐标系中,各个是数值点之间的连接,显示了数据在一段时间内的变化趋势。 X 轴通常表示时间(dimension),Y 轴则表示具体的metric:

    当dimension和metric都唯一时,比如上图中最近七天的访客数,只会出现一条线。要显示不同设备在某段时间内的访问量,那么需要根据时间和设备两个dimension进行聚合,图表中也会产生多条线(与设备种类数相同):

    当行数过多时,会使图表变得复杂,尤其当线之间的间距很近,这时就需要考虑布局或者以其他图表类型来展示。
  • Bar Chart(柱状图)
    Bar chart 是使用具有长度比例的条形图来比较同一维度不同类别之间的数据的图表,其条形图可以是水平方向或者垂直方向。
    水平方向的Bar chart非常适合用于TOP5/TOP10这样的排名,而不是全部的类别,数值越大标签越长,通常X轴表示metric,Y轴表示dimension,如下图展示科技公司的研发投入:

    垂直方向的Bar chart通常显示X轴表示时间(dimension),Y轴表示metric,体现不同类别之间的差异。可以在时间基础上添加一个二级dimension,展示更细节的数据差异,比如展示按季度展示不同饮品的销量,那么在X轴的同一时间,会有多个条形图,分别代表不同的饮品,这种多维度的Bar Chart通常也被称为Cluster Bar Chart

    Bar chart与Line chart有些类似,区别在于Bar chart使用条形图的高度来进行类别之间的对比,而Line chart更多考虑展示变化趋势。
  • Pie Chart (饼图)
    Pie chart 是一个圆形图,每个切片代表一类数据,切片的大小和它代表的数值成正比,所有切片部分的总和等于100%,体现的是部分-整体的关系。Pie chart常用于展示不同类别之间的比较,或者说明一种类型对于其他类型的优势,使读者能够更快速了解数据的分布比例。比如通过饼图来展示苹果2018第一季度的收入来源:

    图中各业务占比非常清晰,其中手机业务达到七成。但实际上,Pie chart一直以来都备受争议,因为它经常被滥用,展示出的效果会让人很难抓住重点。来看看这样一张图:

    因为数值非常接近,所以一眼很难分辨出四支球队中,哪一支队伍的支持率更高,即使支持率的百分比也显示在图表中。
    因此每个维度的指标占比大致相同时,Pie Chart不是一个很好的选择,这时应该考虑使用Bar Chart来展示 。同时,组成饼图的部分越少,显示的效果就越有效。
    通常情况下,生成饼图的数据源,dimension和metric都是唯一的。
  • Stacked Bar Chart(堆叠柱状图)
    Stacked Bar Chart将一个Bar Chart中的每个条形图再按照某一维度分割成几部分时(条形图的总数不变),每部分代表一个不同的类别的占比,它强调的是部分-整体的关系。Stacked Bar Chart在通常在时间基础上,还需要一个额外的dimension来生成stacked数据,而metric是唯一。
    对比上述的Pie Chart,同样是一种部分-整体的关系,可以用于展示不同部分的占比情况,但是如果想要同时展示不同时期的数据占比,那么Stacked Bar Chart是一个很好的选择,比如下图:

  • Heatmap (热力图)
    Heap map 带来的颜色以及层次冲击性很强,它是在我看来最让人印象深刻的也是最容易理解的图表,程序员们对它应该再熟悉不过了吧(如果经常上github的话)。Heatmap的使用场景也非常多,比如基于时间统计访客的分布情况(google-analytics):

    基于国家地理的雾霾指数分布情况:
    还可以表示NBA赛场上Nash投篮热区(颜色越深,投篮越准,无奈老衲投篮过准导致毫无对比性):

    这些Heatmap的共同特点是利用不同的颜色(深浅),来呈现数据的集中性(密集性),读者能够快速的找到最重要的数据区域。Heatmap通过颜色来表达数值,颜色越深数值越大,颜色比数字更容易区分和理解,尤其在可视化大量的数据分布时,呈现出的趋势更有价值。
  • Tables (表格)
    Tables 就像sql的查询结果,行表示数据,列表示数据的属性。对于其他的图表,往往只是展示某维度单个值的指标,而Tables可以展示这个维度的一些属性信息,比如Line Chart和Tables都可以展示学生的访问情况,前者重在展示学生的访问量的趋势,而后者展示数值的同时,也会展示出学生的一些属性信息,比如院系、年级、角色等,并且Tables可以根据访问信息进行排名:
    相比于其他图表,Table可以展示多个metric,只是需要注意metric之间是否是同一范围,下图展示了不同浏览器访问网站时,产生的不同指标信息:

   数可视化据分析,还有很多其他的展示方式,以上是我在项目中常用的一些,在今后如果会用到其他的图表,也会更新在这里。

Dimensions vs Metrics

  最近一段时间都在做数据平台相关的工作,期间参考Google Analytics API中的一些概念,实现数据平台的analytics-api。GA中最重要的两个概念是dimensions(维度)和metrics(指标),GA Report的展示,大部分都是由dimensions和metrics组合而成的,先来看几个图例:

图中展示了网站通过不同设备访问产生的会话数

图中展示了网站最近一周每天的流量信息

图中展示了网站在不同浏览器访问时产生的流量信息

  虽然上面各图的展示方式不同(分别为饼图、折线图、表格),但是从数据分析的角度来看,它们都根据某一dimension的不同分类,聚合算出某一或某些metric的值。比如图一按设备分类,分别计算出桌面、移动和平板的会话数(single metric),图二按天(分类)计算每天的用户数等信息(multiple metrics),图三按浏览器分类,计算出chrome、safiri、app的用户数情况。

Dimensions vs Metrics

  • dimensions 即维度,表示数据的属性,通常表示一类事物的集合,比如设备维度表示产生event的设备,如电脑手机平板。再比如LMS系统中角色维度表示产生event的角色,如学生教师或者管理员。值得注意的是,在数据分析中,可以根据时间进行指标的计算,比如按天、按月、按小时等,因此时间也是一个维度。
  • metrics即指标 ,表示量化衡量标准,通常是一个具体的量词,其值通常是数字或者比率,比如用户数会话数访问量出席率等。

  对于网站来说,dimensions通常包含用户的一些属性,比如用户的身份使用的设备来自于哪里等,而metrics常用来体现用户产生的动作和行为。在数据分析中,metrics是必须的,可以为一个或多个,而dimensions可以不指定,它展示的是metrics的总量,此时往往会加上一个时间区间的filter条件。当有多个dimensions时,次级dimension是其父级在细分的结果,比如下图展示了网站不同角色根据不同设备的访问情况,其次级dimension(设备)的metric之和就是其父级dimension(角色)的metric:

角色(dimension) 设备(次级dimension) 访问量(metric)
学生 手机 6,379
  电脑 14,713
教师 手机 1,822
  电脑 6,409
管理员 手机 341
  电脑 1,423

Dimensions和Metrics的有效组合

   并非所有dimensions和metrics都可以组合在一起,每个dimension和metric都有各自的范围,只有当它们的范围相同时,组合在一起才有意义。例如,平台中有出勤率的metric,它需要与院系(或者课程)一级的dimension来搭配使用,如果将出勤率设备或者浏览器等dimension组合,就不合逻辑,让人费解。

   当多个dimension可以组合在一起时,metric是dimension一级一级drill down的结果,每一级dimension的metric之和是其上级dimension的metric值。而metric通常来说是独立的,要同时展示多个metric,那么需要metric各自的dimension范围一致,比如访问量访客数两个指标,都可以通过设备维度进行展示,但是如果要同时展示访问量出勤率,则无法通过一个表格完成,因为访问量可以从设备dimension计算,但出席率不行。

异常处理经验

  异常处理时任何一种高级语言都会涉及到的议题。在编码过程中,正确高效的异常处理,来避免一些错误导致的应用程序中止,使得程序更健壮,同时能够更快的定位且修复问题。在Python中通过 try/except/[else]/[finally] 语句来捕获异常,其中 elsefinally 关键字可以省略。要抛出异常,需要使用 raise 关键字。关于python 中异常处理的更多内容可以查看官方文档,这里不再赘述。

python 异常体系

  python异常的根类是 BaseException,在标准库中还有四种builtin异常类直接继承自 BaseException,分别是 GeneratorExitSystemExitKeyboardInterrupt 以及 Exception 类。其他builtin的异常类,都继承自 Exception查看python标准库中异常类的层次结构。即使python官方提供了有很多builtin的异常类,但是在编写应用程序时,仍然需要我们自定义与业务相关的异常类。在自定义异常时,

   python官方推荐其继承自Exception或者Exception的子类而非BaseException。这是因为Exception表示应用程序中最普遍且并非系统退出导致的异常出错,其他三个BaseException的子类都有特殊的含义,GeneratorExit在生成器执行close()方法抛出的,而它从技术上讲并非是一种错误,KeyboardInterrupt通常在用户执行中断操作如Control-C时抛出的,它继承自BaseException可以避免被捕获Exception的代码意外捕获,从而阻止python解释器的退出,SystemExit在执行sys.exit()方法时抛出,与KeyboardInterrupt一样,它也不会被捕获Exception的代码意外捕获。应用程序在捕获异常时,更关注的是Exception类而非BaseException类,因此自定义的异常应该继承自Exception

更好的使用异常

   在现实中,应尽量避免直接使用*raise Exception()*来抛出异常,因为它不够具体,没有实际的意义。正确的做法时根据业务逻辑,来自定义不同的异常。我们知道,定义异常需要继承自Exception,定义最简单的异常类:

class MyException(Exception):
    """customize exception"""
    pass

   捕获所有的异常:

try:
    do_something()
except Exception:
    # catch any exception !
    handle_exception()

   在自定义异常类时,通常需要一些额外信息,来表明当前异常发生的上下文,那么需要来了解下异常类的初始化方法。奇怪的是,尽管通过pycharm查看python typeshed,在builtins.pyi文件中可以找到BaseExceptionException的声明如下(每个Python模块都由扩展名为 .pyi 的 "stub file" 表示,它是一个普通的python文件,只包含模块的公共接口的描述,不包含任何实现,类似于java的接口类):

class BaseException:
    def __init__(self, *args: object, **kwargs: object) -> None: ...
        
class Exception(BaseException): ...

   实际上异常基类并不支持关键字参数,而只支持位置参数,网上查了下已经有issue了:

In [1]: e = Exception("trivial", error_code=-1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-67c4664d4f81> in <module>
----> 1 e = Exception(error_code=-1)

TypeError: Exception does not take keyword arguments

   因此要在自定义的异常类中,调用其基类的初始化方法,不能传递关键字参数。调试程序时,常常使用 print(exception) 来输出异常的内容,print方法会调用异常类的 __str__方法。查看cpython源码,在exception.c文件中找到其定义:

static PyObject *
BaseException_str(PyBaseExceptionObject *self)
{
    switch (PyTuple_GET_SIZE(self->args)) {
    case 0:
        return PyUnicode_FromString("");
    case 1:
        return PyObject_Str(PyTuple_GET_ITEM(self->args, 0));
    default:
        return PyObject_Str(self->args);
    }
}

   switch语句决定输出的内容,并且使用到了位置参数args,转化为python语法如下:

def __str__(self):
    str_len = len(self.args)
    if str_len == 0:
        return ""
    elif str_len == 1:
        return str(self.args[0])
    else:
        return str(self.args)

   因此如果要使用默认的 _str_ 输出异常信息时,自定义异常类最好将需要输出的内容以单个参数形式来初始化Exception基类。当然,子类也可以复写基类的 str方法。

   当需要异常提供的更多细节时,可以在初始化异常实例时传递更多的参数,如:

class CarError(Exception):
    """Basic exception for errors raised by cars"""
    def __init__(self, car, msg=None):
        if msg is None:
            # Set some default useful error message
            msg = "An error occured with car %s" % car
        super(CarError, self).__init__(msg)
        self.car = car

class CarCrashError(CarError):
    """When you drive too fast"""
    def __init__(self, car, other_car, speed):
        super(CarCrashError, self).__init__(
            car, msg="Car crashed into %s at speed %d" % (other_car, speed))
        self.speed = speed
        self.other_car = other_car

try:
    drive_car(car)
except CarCrashError as e:
    # If we crash at high speed, we call emergency
    if e.speed >= 30:
        call_911()

   实现一个library时,定义一个继承自Exception的异常基类,会让使用者更容易的捕获从这个library中抛出的任何异常,比如sqlalchemy中的异常定义:

class SQLAlchemyError(Exception):
    """Generic error class."""

class ArgumentError(SQLAlchemyError):
    """Raised when an invalid or conflicting function argument is supplied.
    This error generally corresponds to construction time state errors."""

class ObjectNotExecutableError(ArgumentError):
    """Raised when an object is passed to .execute() that can't be
    executed as SQL."""

class NoForeignKeysError(ArgumentError):
    """Raised when no foreign keys can be located between two selectables
    during a join."""
    
# other exceptions

   这样,在使用SQLAlchemy可以简单的通过 except SQLAlchemyError 来捕获任何SQLAlchemy相关的异常。查看sqlalchemy/exc.py,会发现很多异常的定义都很简单,不同的类名代表不同异常抛出的含义,这是因为对不同类型的异常,并非平等对待,事实上更希望以不同的方式来对它们做出反应或者处理。这样就需要不同的 except 语句,来捕获不同的异常,同时要注意except语句的顺序,保证子类异常的捕获总在父类之前。当然,并不需要捕获所有异常,好的做法是只捕获您愿意处理的异常

组织异常结构

   在何时何处定义异常并无限制,与其他类型一样,可以定义在任何模块,函数,类甚至闭包中。大部分library将它们的异常类都定义在同一个模块中,比如SQLAlchemy的异常定义在exc.py中,Requests定义在exceptions.py中。在使用这些library时,很容易就能import它们的异常模块,并且在编写处理异常的代码时,知道它们是在何处定义的。这种方式并非强制性的,当library的规模很小时,没有必要将异常与其他模块分割为不同的文件。

   一些应用程序往往由不同的子系统组合而成,而每个子系统又有各自的异常模块。这种情况下,将这些子系统的异常统一到同一个模块,比如都放在myapp.exceptions,并不是一种好的做法。例如,当应用程序有两个子系统组成,分别是定义在myapp.http模块的HTTP REST API服务,和定义在myapp.tcp模块的TCP服务。这两个服务都可以定义与自己的协议相关的异常。如果这些异常都定义在一个myapp.exceptions模块,那么只会为了一些无用的一致性,而使得代码分散。如果在子系统中维护各自的异常模块,只需要将它们定义在文件顶部某处,这样会简化代码的维护。

包装异常

   包装异常是将一个异常封装到另一个异常之中的做法。为什么不直接抛出异常,而要在其上封装一层呢?试想当我们在开发自己的lib时,在其中使用了 requests,且未将requests的异常类封装到自定义的lib异常类中,这样就会出现layer violation。任何应用程序在使用到我们定义的lib时,可能会收到 requests.exceptions.ConnectionError类似的异常,这正是问题所在:

  • 应用程序并不清楚我们的lib使用了 requests,而它也并不需要知道
  • 为了处理这个异常,应用程序需要 import requests.exceptions,因此需要依赖于requests库,即使并未直接使用它
  • 或许之后的某天,我们会使用其他的lib比如httplib来替换掉requests,在出现异常时会抛出httplib.HTTPConnection而非requests相关的异常,此时应用程序的异常捕获处理已经不再正确

   因此在任何场合,务必将异常从其他模块封装到自己的异常处理中,就像下面这样:

class MylibError(Exception):
    """Generic exception for mylib"""
    def __init__(self, msg, original_exception):
        super(MylibError, self).__init__(msg + (": %s" % original_exception))
        self.original_exception = original_exception

try:
    requests.get("http://example.com")
except requests.exceptions.ConnectionError as e:
     raise MylibError("Unable to connect", e)

参考文章:

https://julien.danjou.info/python-exceptions-guide/)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.