简析Python中的四种队列

队列是一种只允许在一端进行插入操作,而在另一端进行删除操作的线性表。 在 Python 文档中搜索队列(queue)会发现,Python 标准库中包含了四种队列,分别是 queue.Queue / asyncio.Queue / multiprocessing.Queue / collections.deque。 collections.deque deque 是双端队列(double-ended queue)的缩写,由于两端都能编辑,deque 既可以用来实现栈(stack)也可以用来实现队列(queue)。 deque 支持丰富的操作方法,主要方法如图: 相比于 list 实现的队列,deque 实现拥有更低的时间和空间复杂度。list 实现在出队(pop)和插入(insert)时的空间复杂度大约为 O(n),deque 在出队(pop)和入队(append)时的时间复杂度是 O(1)。 deque 也支持 in 操作符,可以使用如下写法: q = collections.deque([1, 2, 3, 4]) print(5 in q) # False print(1 in q) # True deque 还封装了顺逆时针的旋转的方法:rotate。 # 顺时针 q = collections.deque([1, 2, 3, 4]) q.rotate(1) print(q) # [4, 1, 2, 3] q.rotate(1) print(q) # [3, 4, 1, 2] # 逆时针 q = collections.deque([1, 2, 3, 4]) q.rotate(-1) print(q) # [2, 3, 4, 1] q.rotate(-1) print(q) # [3, 4, 1, 2] 线程安全方面,通过查看 collections.deque 中的 append()、pop()等方法的源码可以知道,他们都是原子操作,所以是 GIL 保护下的线程安全方法。 ...

五月 22, 2018 · 2 分钟 · Zhiya

你真的会正确使用断言吗?

什么是断言 断言是作为一种调试工具被发明出来的,用来检查那些“代码写对了就肯定成立”的条件。例如我们要断言一个变量 a 必须要大于 2,就可以这样写: assert a > 2 当条件不满足时,就会抛出 AssertionError 异常,等同于如下代码: if not assert_condition: raise AssertionError 由于断言是一个 debug 工具,Python 的实现也符合这个设计哲学,在 Python 中 assert 语句的执行是依赖于__debug__变量的,当__debug__为 true 时,assert 语句才会被执行。 if __debug__ and not assert_condition: raise AssertionError 默认情况下,当我们执行一个 Python 文件时,__debug__是会被设置为 True 的,只有加参数-O 或-OO 时,__debug__才会被设置为 False。 新建一个 assert.py 文件,写下如下代码: print(__debug__) assert 2 > 5 当使用 python assert.py 运行时,__debug__会输出 True,assert 2 > 5 语句会抛出 AssertionError 异常。 当使用 python -O assert.py 运行时,__debug__会输出 False,assert 2 > 5 语句由于没有执行不会报任何异常。 断言 or 异常 我们思考这几个问题:断言应该用在哪些情境下?异常和断言的区别是什么? 用一句话来概括断言的使用场景和与异常的区别: 检查先验条件使用断言,检查后验条件使用异常 我们定义一个 read_file 函数: ...

五月 7, 2018 · 1 分钟 · Zhiya

用装饰器封装Flask-WTF表单验证逻辑

Don’t repeat yourself 在使用Flask-WTF的时候,常会用下面这样的代码来验证表单数据的合法性: from flask import Flask app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def index(): form = TestForm() # 判断是否合法 if not form.validate_on_submit(): return 'err', 400 # 主要逻辑 对于有很多提交接口的项目来说,需要在每个路由下写相同的的逻辑,造成了大量的代码重复。在Flask-Login中,要把一个路由设置为登录后才能访问,只需要在路由上加一个@login_required装饰器,不需要额外的代码。能不能像Flask-Login一样,用装饰器来封装对表单的验证逻辑呢? 实现表单验证装饰器 由于不同路由使用的表单类不一样,所以需要为装饰器传入一个表单类参数,并且在路由函数中需要用到表单中的值,所以还需要将验证通过的表单传给路由函数。 上代码: def validate_form(self, form_cls): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): if not form.validate_on_submit(): return 'error', 400 return fn(form, *args, **kwargs) return wrapper return decorator 使用方式如下: @validate_form(TestForm) # 需要传入要验证的表单类 @app.route('/', methods=['GET', 'POST']) def index(form): # 执行到这里说明表单验证通过 经过在项目中的应用,发现装饰器还是有一些缺陷: 无法自定义处理非法表单的逻辑 不支持get方式提交的表单(查看validate_on_submit()源码可知其只支持对post和put方式提交的表单进行验证) 丰富一下 要自定义处理非法表单的逻辑,需要增加一个可以传入自定义逻辑的接口。表单非法时接口的返回往往是一致的,所以我们为所有应用装饰器的路由传入一个统一的处理逻辑。将装饰器封装在一个类中,在类中添加一个配置处理逻辑的方法。 from functools import wraps from flask import request class FormValidator(object): def __init__(self, error_handler=None): self._error_handler = error_handler def validate_form(self, form_cls): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): if not form.validate_on_submit() and self._error_handler: return self._error_handler(form.errors) return fn(form, *args, **kwargs) return wrapper return decorator def error_handler(self, fn): self._error_handler = fn return fn error_handler也是一个装饰器,被它修饰的方法就是处理非法表单的方法。 ...

四月 26, 2018 · 1 分钟 · Zhiya

Python参数传递,既不是传值也不是传引用

面试的时候,有没有被问到 Python 传参是传引用还是传值这种问题?有没有听到过 Python 传参既不是传值也不是传引用这种说法?一个小小的参数默认值也可能让代码出现难以查找的 bug? 如果你也遇到过上面的问题,不妨我们来探究下 Python 函数传递的种种。 万物皆对象 Python 中有一个非常重要的概念——万物皆对象,无论是一个数字、字符串,还是数组、字典,在 Python 中都会以一个对象的形式存在。 a = 123 对于上面这行代码,在 Python 看来就是创建一个 PyObject 对象,值为 123,然后定义一个指针 a,a 指向这个 PyObject 对象。 可变对象和不可变对象 Python 中的对象分为两种类型,可变对象和不可变对象,不可变对象指 tuple、str、int 等类型的对象,可变对象指的是 dict、list、自定义对象等类型的对象,我们用一段代码说明他们的区别。 a = [1, 2, 3] print(id(a)) # 2587116690248 a += [4] print(id(a)) # 2587116690248 b = 1 print(id(b)) # 2006430784 b += 1 print(id(b)) # 2006430816 上面代码中我们分别定义了一个可变对象和一个不可变对象,并且对他们进行修改,打印修改前后的对象标识可以发现,对可变对象进行修改,变量对其引用不会发生变化,对不可变对象进行修改,变量引用发生了变化。 上图是一个可变对象,当修改对象时,例如删除数组中的一个元素,实际上把其中一个元素从对象中移除,对象本身的标识是不发生变化的。 改变一个不可变对象时,例如给一个 int 型加 2,语法上看上去是直接修改了 i 这个对象,但是如前面所说,i 只是一个指向对象 73 的一个变量,Python 会将这个变量指向的对象加 2 后,生成一个新的对象,然后再让 i 指向这个新的对象。 参数传递时的表现 了解了对象的原理后,我们就可以来尝试理解一下参数传递时他们的不同表现了。 a = [1, 2, 3] print(id(a)) # 1437494204232 def mutable(a): print(id(a)) # 1437494204232 a += [4] print(id(a)) # 1437494204232 mutable(a) b = 1 print(id(b)) # 2006430784 def immutable(b): print(id(b)) # 2006430784 b += 1 print(id(b)) # 2006430816 immutable(b) 通过上面的代码可以看出,修改传进的可变参数时,会对外部对象产生影响,修改不可变参数时则不会影响。 ...

四月 22, 2018 · 1 分钟 · Zhiya

实战 | 用aiohttp和uvloop实现一个高性能爬虫

asyncio 于 Python3.4 引入标准库,增加了对异步 I/O 的支持,asyncio 基于事件循环,可以轻松实现异步 I/O 操作。接下来,我们用基于 asyncio 的库实现一个高性能爬虫。 准备工作 Earth View from Google Earth是一款 Chrome 插件,会在打开新标签页时自动加载一张来自 Google Earth 的背景图片。 使用 Chrome 开发者工具观察插件的网络请求,我们发现插件会请求一个地址如https://www.gstatic.com/prettyearth/assets/data/v2/1234.json的 JSON 文件,文件中包含了经过 Base64 的图片内容,观察发现,图片的 ID 范围大致在 1000-8000 之间,我们的爬虫就要来爬取这些精美的背景图片。 实现主要逻辑 由于爬取目标是 JSON 文件,爬虫的主要逻辑就变成了爬取 JSON–>提取图片–>保存图片。 requests 是一个常用的 http 请求库,但是由于 requests 的请求都是同步的,我们使用aiohttp这个异步 http 请求库来代替。 async def fetch_image_by_id(item_id): url = f'https://www.gstatic.com/prettyearth/assets/data/v2/{item_id}.json' # 由于URL是https的,所以选择不验证SSL async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) as session: async with session.get(url) as response: # 获取后需要将JSON字符串转为对象 try: json_obj = json.loads(await response.text()) except json.decoder.JSONDecodeError as e: print(f'Download failed - {item_id}.jpg') return # 获取JSON中的图片内容字段,经过Base64解码成二进制内容 image_str = json_obj['dataUri'].replace('data:image/jpeg;base64,', '') image_data = base64.b64decode(image_str) save_folder = dir_path = os.path.dirname( os.path.realpath(__file__)) + '/google_earth/' with open(f'{save_folder}{item_id}.jpg', 'wb') as f: f.write(image_data) print(f'Download complete - {item_id}.jpg') aiohttp 基于 asyncio,所以在调用时需要使用 async/await 语法糖,可以看到,由于 aiohttp 中提供了一个 ClientSession 上下文,代码中使用了 async with 的语法糖。 ...

四月 10, 2018 · 1 分钟 · Zhiya