之前一直很好奇像sanic或者aiohttp是如何实现并发的,其实从网上看无非就是使用了asyncio,更底层就是事件驱动,libevent之类的,但是纯从应用层来讲,这个会是怎么一回事呢,举个例子:
1 |
|
从上面代码来看,整个执行周期为3s,但是有个问题是:如果请求一个一个来,如何不阻塞呢??
1 |
|
如果按照例如eventlet的做法:
1 | procpool = GreenPool(size=poolsize) |
消息过来,直接抛给pool,并不会进行阻塞,并且有poolsize控制池大小,
额,这里是sanic源码中的一部分…
1 | # -*- coding: utf-8 -*- |
目前在开发的内部爬虫框架中,对于从消息队列中取任务部分,如下图所示,遇到了一些问题。
因为我们整个模型为async的,底层使用的asyncio,对于消息队列的客户端来讲,可选择有的pika和kombu,对于kombu,的确是一个不错的选择,相比pika callback的写法,的确封装的更为高级,写起来比较方便,另外官方支持与eventlet工作,例如openstack的nova底层使用的就是eventlet与kombu,但是kombu目前不支持asyncio,官方将于5.0版本提供支持,我很看好,题外话。
那么目前能够选择貌似只有pika了,官方示例,奈何实力不高,只能在callback的基础上加代码,此处分成两个版本,如下所示。
1 |
|
异步取来的任务,将其抛给线程池/进程池处理,那么上层应用者直接在get_list
和get_data
中进行处理,那么此处就有非常大的问题了,因为一个好好的异步模型活生生的给改成了有点同步的感觉了,如果我想在业务层执行异步任务,会发现在当前线程中无法get eventloop,哈哈,好尴尬。这点实际在tornado官方文档中描述如何执行同步代码中有提示,是不是和这个很类似.
1 |
|
上面的实在受不了,太烂了,所以此处还是要想办法给异步了,此处斜眼。
此前在看nameko中消费者处理时,他是使用eventlet.spawn方法开启一个新的协程进行处理,从而不阻塞当前loop,那么在asyncio中也一定有相应的方法,下面出场asyncio.async和asyncio.ensure_future方法,其实也是一个方法,asyncio.async将被放弃啦,所以所以介绍ensure_future方法。
1 |
|
由于使用的pika连接器是asyncio的,那么根据pika的官方文档描述,获取到的任务只有在完成的时候才会进行下发新的任务,
如果如上get_list
方法下面的使用者写的是同步代码,会导致效率非常地下,所以此处会强制提升业务代码至异步模型,貌似有点激进,所以暂时不更新.
额,几天没有更新了,经过几天的思考,目前采用多线程/多进程模型,为什么没有使用异步呢?我觉得可以从一下几方面总结:
先把第一个版本做稳定了,因为目前还是有一些问题的,因为分布式爬虫框架,程序异常退出以及退出迟迟没有在server端看到客户端下线,还要定位原因以及加强处理,等稳定后再加入timer等其他功能。
1 |
|
1 |
|
但是今天将介绍另外两种不同的写法,而这两种写法也在库中比较常见
1 | class App(object): |
为什么叫flask-like呢?因为我也没有想到好的名字来叫,另外这种写法也是在flask中见,因为他没有包装func(*args, **kwargs)这层,而是仅给func传递参数.
1 |
|
今天在同事的推荐下,看了下扇贝的sea代码,
没细看,突然看到了cached_property
的代码,这个让我突然想到了我们内部爬虫框架的cached_property
,当时我在写这部分代码的时候主要目的有点类似如下:
1 | class A(object): |
A
为一个类,然后B
有点类似A
的一个超集,平常使用的使用为实例化B
, 然后为了操作A
下面的方法,基本流程为:
1 | >>> b = B() |
当时设计的时候,并没有想到通过property这个将方法变成属性的方法,当时想我只需要初始化两次就行,如下:
1 |
|
但是后来我就意识到我这种设计有点low,因为其他同事调用的时候可能不会这么使用,另外因为B
下面还会有其他类似A
这种东西,例如:
1 | class B(object): |
难道每次使用的时候都拿到c这个实例吗,有点傻,所以参考了Flask
的cached_property
实现,改成了如上了流程.那么进入主题,cached_property
到底干了什么事情?
先看Flask
的实现:
1 |
|
再看sea
的实现:
1 |
|
唯一不同点应该就在于加了一个锁吧,那么抛去锁的部分,单纯讲cached_propery
的实现
1 |
|
学过Python的应该蛮清楚关于装饰器这个概念,当将cached_property加在b上时,就已经完成了cached_property类的实例化(看最后一个类版本的计算时间装饰器),那怎么传进去的呢?
1 |
|
调用的时候怎么个过程?看Python Document
1 | class Property(object): |
1 | def __get__(self, instance, cls=None): |
1 | class B(object): |
剩下自行理解吧..
1 | # 类版本的计算时间的装饰器 |
这里说的 AdjacencyList , 就是最常用来在关系数据库中表示树结构的,parent
方式:
id | name | parent |
---|---|---|
1 | 一 | null |
2 | 二 | 1 |
3 | 三 | 2 |
上面的数据, 表示的结构就是:
一
|- 二
|- 三
模型定义很好做:
1 | # -*- coding: utf-8 -*- |
这里不让parent
字段有null
, 而使用0
代替.
这个例子在关系上, 有一个纠结的地方, 因为 node
这个表, 它是自关联的, 所以如果想要children
和 parent_obj
这两个关系时:
1 | children = relationship('Node') |
呃, 尴尬了.
如果是两个表, 那么 SQLAlchemy 可以通过外键在哪张表这个信息, 来确定关系的方向:
1 | class Blog(BaseModel): |
因为外键在 Blog
中, 所以 Blog -> User
的 user_obj
是一个 N -> 1
关系.
反之, User -> Blog
的 blog_list
则是一个 1 -> N
的关系.
而自相关的 Node
无法直接判断方向, 所以 SQLAlchemy
会按 1 -> N
处理, 那么:
1 | children = relationship('Node') |
这两条之中, children
是正确的, 是我们想要的. 要定义 parent_obj
则需要在 relationship
中通过参数明确表示方向:
1 | parent_obj = relationship('Node', remote_side=[id]) |
这种方式就定义了一个, “到 id” 的 N -> 1
关系.
现在完整的模型定义是:
1 | class Node(BaseModel): |
查询方面没什么特殊的了, 不过我发现在自相关的模型关系, lazy
选项不起作用:
1 | children = relationship('Node', lazy="joined") |
都是无效的, 只有在查询时, 手动使用 options()
定义:
1 | n = session.query(Node).filter(Node.name==u'一')\ |
如果要一次查出多级的子节点:
1 | n = session.query(Node).filter(Node.name==u'一')\ |
多个 joinedload()
串连的话, 可以使用 joinedload_all()
来整合:
1 | from sqlalchemy.orm import joinedload_all |
在修改方面, 删除的话, 配置了 cascade
, 删除父节点, 则子节点也会自动删除:
1 | children = relationship('Node', lazy='joined', cascade='all') # 1 -> N |
如果只删除子节点, 那么 delete-orphan
选项就很好用了:
1 | children = relationship('Node', lazy='joined', cascade='all, delete-orphan') # 1 -> N |
每次使用mock都记不住,今天也是这样,但是这里出现了不一样的地方,先举个例子:
1 | from mock import Mock, patch |
上述看也没什么问题,但是此处讲一个Where to patch
的问题.
简单描述:
1 | a.py |
如上,a.py定义了一个SomeClass,然后b.py引入这个Class,然后在某个地方进行实例化此类,ok,如果要patch SomeClass这个类,要从a.py进行patch呢还是从b.py进行patch呢?
例如:
1 |
|
1 |
|
上述哪一种方法才能被正常mock??
引用外文的描述:
1 | Now we want to test some_function but we want to mock out SomeClass using patch. The problem is that when we import module b, which we will have to do then it imports SomeClass from module a. If we use patch to mock out a.SomeClass then it will have no effect on our test; module b already has a reference to the real SomeClass and it looks like our patching had no effect. |
翻译成中文(翻译不好-_-!)
1 |
|
- mock class staticmethod
1 |
|
- 如何mock一个异常
1 | with patch( |
- mock class method
1 |
|
创建一个m位的位数组(bitmap),先将所有的位数组初始化为0。然后选择k个不同的哈希函数。第i个哈希函数对应的字符串str哈希的结果记为h(i, str),且h(i, str)的范围要在0至m-1。如下图所示。
如何判断字符串是否存在呢?
字符串也经过h(i, str),h(2, str),h(3, str)…哈希映射,检查每一个映射到m位的位数组上是否为1,如果不全为1,则表示一定不存在,否则,不能说明完全存在,有误差率在里面,为什么不能说一定存在呢,没看懂,可看:BloomFilters
既然不能完全表示存在,那么如何计算这个误差率呢?一共有三个参数:k,m,n。
参数 | 表示 |
---|---|
k | 哈希个数 |
m | 位数组大小 |
n | 字符串个数 |
下图表示m/n的结果与k个哈希函数的选择出现的错误率表格。
如上,举例简单说明,如果声明一个为数组大小为2,只传入一个字符串,那么m/n为2,如果选择k个哈希,导致的容错率分别是1.39,0.393,0.400。ok,如果要存1亿个字符串,那么大概为多少呢?
简单计算:如果容错率要求为5.73e-06,那么m/n=32,如果n为1亿的话,那么m为32亿,32亿 / 8/ 1024/1024=381.4697265625MB内存。
还是相当可以的。关于更多的容错率,可以看BloomFilters。
Python实现:python-bloomfilter
C实现:pybloomfiltermmap
混合属性, 官方文档中称之为Hybrid Attributes
. 这种机制表现为, 一个属性, 在 类 和层面, 和实例 的层面, 其行为是不同的. 之所以需要关注这部分的差异, 原因源于 Python 上下文和 SQL 上下文的差异.
类 层面经常是作为 SQL 查询时的一部分, 它面向的是 SQL 上下文. 而 实例 是已经得到或者创建的结果, 它面向的是 Python 上下文.
定义模型的 Column() 就是一个典型的混合属性. 作为实例属性时, 是具体的对象值访问, 而作为类属性时, 则有构成 SQL 语句表达式的功能.
1 | class Interval(BaseModel): |
实例行为:
1 | ins = session.query(Interval).first() |
类行为:
1 | ins = session.query(Interval).filter(Interval.end - Interval.start > 10).first() |
这种机制其实一直在被使用, 但是可能大家都没有留意一个属性在类和实例上的区别.
如果属性需要被进一步封装, 那么就需要明确声明Hybrid Attributes
了:
1 | from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method |
setter
的定义同样使用对应的装饰器即可:
1 | class Interval(BaseModel): |
前面说的属性, 在类和实例上有不同行为, 可以看到, 在类上的行为, 其实就是生成 SQL 表达式时的行为. 上面的例子只是简单的运算, SQLAlchemy 可以自动处理好 Python 函数和 SQL 函数的区别. 但是如果是一些特性更强的 SQL 函数, 就需要手动指定了. 于时, 这时的情况变成, 实例行为是 Python 范畴的调用行为, 而类行为则是生成SQL 函数的相关表达式.
同时是前面的例子, 对于 length 的定义, 更严格上来说, 应该是取绝对值的.
1 | class Interval(BaseModel): |
但是, 如果使用了 Python 的abs()
函数, 在生成 SQL 表达式时显示有无法处理了. 所以, 需要手动定义:
1 | from sqlalchemy import func |
这样查询时就可以直接使用:
1 | ins = session.query(Interval).filter(Interval.length > 1).first() |
对应的 SQL :
1 | SELECT * |
总体上没有特别之处:
1 | class Account(BaseModel): |
查询时:
1 | user = session.query(User).first() |
这里涉及的东西都是 Python 自己的, 包括那个sum()
函数, 和SQL
没有关系.
如果想实现的是, 使用SQL
的sum()
函数, 取出指定用户的总账户金额数, 那么就要考虑把balance 作成表达式的形式:
1 | from sqlalchemy import select |
这样的话,User.balance
只是单纯的一个表达式了, 查询时指定字段:
1 | user = session.query(User, User.balance).first() |
注意, 如果写成:
1 | session.query(User.balance).first() |
意义就不再是”获取第一个用户的总金额”, 而变成”获取总金额的第一个”. 这里很坑吧.
像上面这样改, 实例层面就无法使用 balance 属性. 所以, 还是先前介绍的, 表达式可以单独处理:
1 | @hybrid_property |
定义了表达式的 balance , 这部分作为查询条件上当然也是可以的:
1 | user = session.query(User).filter(User.balance > 1).first() |
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true