大毒瘤!卸载WeGame解决XPS 15蓝屏问题

潜伏期

去年4月底买了一台美版XPS 15 9560,用了几个月之后就会偶尔出现蓝屏问题,由于之前就在论坛上看到很多吐槽XPS品控的帖子,以为自己也中了枪,好在蓝屏也不频繁,不影响使用也就没管。

上升期

进入今年5月,升级了Win10 1803后(基本上软件有更新我都会第一时间升级,体验最新的改进),蓝屏的次数开始多了起来,开始变得影响使用,于是开始着手查找问题。

由于心急,并没有看转储文件,而是直接Google了XPS 15的蓝屏问题,希望能尽快找到方案。很快发现具有类似问题的人不在少数,也有很大一部分人以此说XPS 15品控不好,看了很多内容后,把怀疑的方向放在了驱动层面(Dell有一个Dell Update软件,一有更新提示我就会更新驱动),于是从更新频率最高的显卡驱动入手,降级显卡驱动到上一个版本,然而并没有什么用。于是又降级了Wifi驱动,因为Wifi驱动也是最近更新的,并且XPS 15的Killer显卡兼容性似乎没有那么好。

XPS 15的驱动列表,可以看到Wifi和显卡驱动都是最近发布的

降级完驱动之后,电脑进入了短暂的回光返照阶段,用了几个小时都没有蓝屏,然而紧接着更加不幸的问题就要发生了。

爆发期

用QQ接收一个大文件,大概1G,进行到一半的时候忽然蓝屏,重启后重新接收,准备传完之后再解决蓝屏问题,然而不幸的是传了一半又蓝屏了… 这时我把怀疑的方向转向了硬盘,于是降级了Intel RST驱动,心想降级要是没用的话,有可能是SSD跪了… 降级后怀着忐忑的心情再次重试,竟然…又蓝屏了…

这次把怀疑的方向转到了QQ或者Wifi上,毕竟是一用QQ接收大文件就出问题,那么不是QQ就是Wifi了,于是卸载QQ,用迅雷下载一个大文件,果不其然,又跪了…

然而已经降级了Wifi驱动,之前也一直没有什么问题,难道真的是硬件出了问题?这个时候,我内心已经有点放弃PC,转而在京东上看MBP的价格了…

然而MBP高企的售价让我冷静了下来,回来死马当活马医,继续找问题吧。

既然自己怀疑的方向都不对,只能信仰科学了,安装了WinDbg和BlueScreenView这两款软件来查看和分析转储文件(具体过程不再赘述,可以参考文末的相关资源)。

从BlueScreenView里可以看到,机器以近乎疯狂的频率蓝屏

从BlueScreenView里并没有看到太多信息,现在只剩下WinDbg这一根救命稻草了。

不过好在WinDbg非常给力,分析了几个不同时间的转储文件,都提示问题可能是由tdx.sys文件造成的。

tdx是TDI Translation Driver的意思,TDI是传输层驱动接口的意思,那么可以肯定问题来自于网络,这也印证了每次使用网络传输大文件就会蓝屏的现象。紧接着,看到了另外一段话:

WARNING: Unable to verify timestamp for QMTgpNetFlow764.sys

这个QMTgpNetFlow764.sys又是什么,看前缀似乎像第三方的文件,先Google一下。

真相大白

Google了一下这个QMTgpNetFlow764.sys,终于拨云见日。

原来早有人发现了QMTgpNetFlow764.sys引发蓝屏问题的秘密

根据网友们的分析(具体可以查看文末的相关资源),这5个文件来自于腾讯的游戏平台WeGame,于是卸载了腾讯系的游戏,并且重启删除了C:\Windows\system32\drivers目录下以QMT开头的5个文件,没关机使用至今(大约半个月)再也没有出现过蓝屏的情况,至此可以证明,腾讯WeGame平台是引起蓝屏的主要原因。

毒瘤们

相关资源

BlueScreenView下载地址:https://www.nirsoft.net/utils/blue_screen_view.html

WinDbg下载地址:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

WinDbg使用说明:https://jingyan.baidu.com/article/9f7e7ec0b0aea36f281554df.html

知乎用户Weasley Frank的专栏文章TGP/WeGame驱动导致WSL网络服务异常 对这个问题进行了简单分析

你所不知道的Python | 字符串格式化的演进之路

字符串格式化对于每个语言来说都是一个非常基础和常用的功能,学习Python的同学大概都知道可以用%语法来格式化字符串。然而为了让我们更方便的使用这个常用功能,语言本身也在对字符串格式化方法进行迭代。

Python 2.6以前:%操作符

在Python 2.6出现之前,字符串迭代只有一种方法,就是%(也是取模)操作符,%操作符支持unicode和str类型的Python字符串,效果和C语言中的sprintf()方法相似,下面是一个使用%格式化字符串的例子:

1
print("I'm %s. I'm %d year old" % ('Tom', 27))

%符号前面使用一个字符串作为模板,模板中有标记格式的占位符号,%后面是一个tuple或者dict,用来传递需要格式化的值。占位符控制着显示的格式,下面列表展示了占位符的种类:

占位符 内容
%d 十进制整数
%i 十进制整数
%o 八进制整数
%u 无符号整数
%x 无符号十六进制(小写)
%X 无符号十六进制(大写)
%e 浮点型(科学记数法,小写)
%E 浮点型(科学记数法,大写)
%f 浮点数
%F 浮点数
%g 浮点型,如果小数位数超过4位,使用科学记数法表示(小写)
%G 浮点型,如果小数位数超过4位,使用科学记数法表示(大写)
%c 单个字符
%r 字符串(调用repr()方法生成)
%s 字符串(调用str()方法生成)

除了对数据类型的指定,%操作符还支持更复杂的格式控制:

1
%[数据名称][对齐标志][宽度].[精度]类型
名称 内容
数据名称 数据名称用于字典赋值,如果%符号后面传递的数组就不需要填写了
对齐标志 有+、-、0、‘ ’四种,+表示显示正负数符号,-表示左对齐,空格表示在左侧填充一个空格,0表示用0填充
宽度 表示格式化后的字符串长度,位数不足用0或空格补齐
精度 小数点后的位数
类型 数据类型(参考占位符种类)

例如print(‘%053f’ % ‘12.34’)会输出0012.340

Python 2.6:format函数

到Python2.6时,出现了一种新的字符串格式化方式,str.format()函数,相比于%操作符,format函数使用{}和:代替了%,威力更加强大,在映射关系方面,format函数支持位置映射、关键字映射、对象属性映射、下标映射等多种方式,不仅参数可以不按顺序,也可以不用参数或者一个参数使用多次,下面通过几个例子来说明。

1
2
3
4
5
6
7
8
9
10
11
'{1} {0}'.format('abc', 123# 可以不按顺序进行位置映射,输出'123 abc'

'{} {}'.format('abc', 123# 可以不指定参数名称,输出'abc 123'

'{1} {0} {1}'.format('abc', 123# 参数可以使用多次,输出'123 abc 123'

'{name} {age}'.format(name='tom', age=27# 可以按关键字映射,输出'tom 27'

'{person.name} {person.age}'.format(person=person)  # 可以按对象属性映射,输出'tom 27'

'{0[1]} {0[0]}'.format(lst)  # 通过下标映射

可以看到,format函数比%操作符使用起来更加方便,不需要记住太多各种占位符代表的意义,代码可读性也更高。在复杂格式控制方面,format函数也提供了更加强大的控制方式:

1
[[填充字符]对齐方式][符号标志][#][宽度][,][.精度][类型]

例如:

1
'{:S^+#016,.2f}'.format(1234# 输出'SSS+1,234.00SSSS'

我们以上面的代码为例,通过表格说明一下format格式控制参数:

类型 说明 示例说明
填充字符 不填时默认用空格填充 S表示用S填充
对齐方式 ^表示居中对齐、<表示左对齐、>表示右对齐 ^表示居中对齐,左右位数不足部分会用填充字符填充
符号标志 +表示有符号(正数前显示+,负数前显示-),空格表示整数前加一个空格以和负数对齐 +表示正数前显示空格
# 表示是否在二进制、八进制、十六进制前显示0b、0o、0x等符号 #表示显示进制符号,由于是十进制,所以不显示
宽度 表示输出字符串的宽度 16表示字符串宽度为16,不足部分会补齐
, 表示使用,作为千位分隔符 ,表示使用千位分隔符
精度 表示小数点后数字位数 .2表示精度为2为
类型 s表示字符串类型,c表示字符类型,b\o\d分别表示二八十进制,x\X表示小写和大写十六进制,e\E表示小写和大写的科学记数法,f表示浮点型 f表示浮点型数字

可以看到format函数在%基础上丰富了格式控制种类,并且使输出更容易。

Python 3.6:f-string

不少使用过ES6的小伙伴会知道其中的模板字符串,采用直接在字符串中内嵌变量的方式进行字符串格式化操作,Python在3.6版本中也为我们带来了类似的功能:Formatted String Literals(字面量格式化字符串),简称f-string。

f-string就是以f’’开头的字符串,类似u’’和b’’,字符串内容和format方法中的格式一样,但是可以直接将变量带入到字符串中,可读性进一步增加,例如:

1
2
amount = 1234
f'请转账给我{amount:,.2f}元'  # '请转账给我1,234.00元'

同时,f-string的性能是比%和format都有提升的,我们做一个简单的测试,分别使用%操作符、format和f-string将下面语句执行10000次:

1
2
3
'My name is %s and i'm %s years old.' % (name, age)
'My name is {} and i'm {} years old.'.format(name, age)
f'My name is {name} and i'm {age} years old.'

用时结果如下:

总结

如果你的项目使用的Python版本已经提升到3.6,f-string格式化是首选方式,不仅在保持功能强大的同时语义上更容易理解,而且性能也有较大的提升。如果项目还没有提升到3.6或者使用的2.7,更建议使用format,虽然性能上没有优势,但是语义上还是比%操作符更加便于理解的,功能也更加强大。

欢迎关注我的公众号【Python私房菜】

简析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操作符,可以使用如下写法:

1
2
3
q = collections.deque([1, 2, 3, 4])
print(5 in q)  # False
print(1 in q)  # True

deque还封装了顺逆时针的旋转的方法:rotate。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 顺时针
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保护下的线程安全方法。

1
2
3
4
5
6
7
static PyObject *
deque_append(dequeobject *deque, PyObject *item) { 
    Py_INCREF(item);
    if (deque_append_internal(deque, item, deque->maxlen) < 0
        return NULL;
    Py_RETURN_NONE;
}

通过dis方法可以看到,append是原子操作(一行字节码)。

综上,collections.deque是一个可以方便实现队列的数据结构,具有线程安全的特性,并且有很高的性能。

queue.Queue & asyncio.Queue

queue.Queue和asyncio.Queue都是支持多生产者、多消费者的队列,基于collections.deque,他们都提供了Queue(FIFO队列)、PriorityQueue(优先级队列)、LifoQueue(LIFO队列),接口方面也相同。

区别在于queue.Queue适用于多线程的场景,asyncio.Queue适用于协程场景下的通信,由于asyncio的加成,queue.Queue下的阻塞接口在asyncio.Queue中则是以返回协程对象的方式执行,具体差异如下表:

queue.Queue asyncio.Queue
介绍 同步队列 asyncio队列
线程安全
超时机制 通过timeout参数实现 通过asyncio.wait_for()方法实现
qsize() 预估的队列长度(获取qsize到下一个操作之间,queue有可能被其它的线程修改,导致qsize大小发生变化) 准确的队列长度(由于是单线程,所以queue不会被其它线程修改)
put() / set() put(item, block=True, timeout=None),可以通过设置block是否为True来配置put和set方法是否为阻塞,并且可以为阻塞操作设置最大时长timeout,block为False时行为和put_nowait()方法一致。 put()方法会返回一个协程对象,所以没有block参数和timeout参数,如果需要非阻塞方法,可以使用put_nowait(),如果需要对阻塞方法应用超时,可以使用coroutine asyncio.wait_for()。

multiprocessing.Queue

multiprocessing提供了三种队列,分别是Queue、SimpleQueue、JoinableQueue。

multiprocessing.Queue既是线程安全也是进程安全的,相当于queue.Queue的多进程克隆版。和threading.Queue很像,multiprocessing.Queue支持put和get操作,底层结构是multiprocessing.Pipe。

multiprocessing.Queue底层是基于Pipe构建的,但是数据传递时并不是直接写入Pipe,而是写入进程本地buffer,通过一个feeder线程写入底层Pipe,这样做是为了实现超时控制和非阻塞put/get,所以Queue提供了join_thread、cancel_join_thread、close函数来控制feeder的行为,close函数用来关闭feeder线程、join_thread用来join feeder线程,cancel_join_thread用来在控制在进程退出时,不自动join feeder线程,使用cancel_join_thread有可能导致部分数据没有被feeder写入Pipe而导致的数据丢失。

和threading.Queue不同的是,multiprocessing.Queue默认不支持join()和task_done操作,这两个支持需要使用mp.JoinableQueue对象。

SimpleQueue是一个简化的队列,去掉了Queue中的buffer,没有了使用Queue可能出现的问题,但是put和get方法都是阻塞的并且没有超时控制。

总结

通过对比可以发现,上述四种结构都实现了队列,但是用处却各有偏重,collections.deque在数据结构层面实现了队列,但是并没有应用场景方面的支持,可以看做是一个基础的数据结构。queue模块实现了面向多生产线程、多消费线程的队列,asyncio.queue模块则实现了面向多生产协程、多消费协程的队列,而multiprocessing.queue模块实现了面向多成产进程、多消费进程的队列。

参考

https://docs.python.org/3/library/collections.html#collections.deque
https://docs.python.org/3/library/queue.html
https://docs.python.org/3/library/asyncio-queue.html
https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue
https://bugs.python.org/issue15329
http://blog.ftofficer.com/2009/12/python-multiprocessing-3-about-queue/
http://cyrusin.github.io/2016/04/27/python-gil-implementaion/

欢迎关注我的公众号【Python私房菜】

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

什么是断言

断言是作为一种调试工具被发明出来的,用来检查那些“代码写对了就肯定成立”的条件。例如我们要断言一个变量a必须要大于2,就可以这样写:

1
assert a > 2

当条件不满足时,就会抛出AssertionError异常,等同于如下代码:

1
2
if not assert_condition:
raise AssertionError

由于断言是一个debug工具,Python的实现也符合这个设计哲学,在Python中assert语句的执行是依赖于__debug__变量的,当__debug__为true时,assert语句才会被执行。

1
2
if __debug__ and not assert_condition:
raise AssertionError

默认情况下,当我们执行一个Python文件时,__debug__是会被设置为True的,只有加参数-O或-OO时,__debug__才会被设置为False。

新建一个assert.py文件,写下如下代码:

1
2
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函数:

1
2
def read_file(file_path):
pass

read_file函数要求在开始执行的时候满足一定条件:file_path必须是str类型,这个条件就是先验条件,如果不满足,就不能调用这个函数,如果真的出现了不满足条件的情况,证明代码中出现了bug,这时候我们就可以使用assert语句来对file_path的类型进行推断,提醒程序员修改代码,这样的推断在生产环境中是不需要的,也可以使用if + raise语句来实现assert,但是要繁琐很多。

1
2
def read_file(file_path):
assert is_instance(file_path, str)

read_file函数在被调用执行后,依然需要满足一定条件,比如file_path所指定的文件需要是存在的,并且当前用户有权限读取该文件,这些条件称为后验条件,对于后验条件的检查,我们需要使用异常来处理。

1
2
3
4
5
6
def read_file(file_path):
assert is_instance(file_path, str)
if not check_exist(file_path):
raise NotFoundError()
if not has_privilege(file_path):
raise PermissionError()

文件不存在和没有权限,这两种情况并不属于代码bug,是代码逻辑的一部分,上层代码捕获异常后可能会执行其他逻辑,因此我们不能接受这部分代码在生产环境中被忽略。并且,相比于assert语句只能抛出AssertionError,使用异常可以抛出更细致的错误,方便上层代码针对不同错误执行不同的逻辑。

关注Python私房菜

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

Don’t repeat yourself

在使用Flask-WTF的时候,常会用下面这样的代码来验证表单数据的合法性:

1
2
3
4
5
6
7
8
9
10
11
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一样,用装饰器来封装对表单的验证逻辑呢?

实现表单验证装饰器

由于不同路由使用的表单类不一样,所以需要为装饰器传入一个表单类参数,并且在路由函数中需要用到表单中的值,所以还需要将验证通过的表单传给路由函数。

上代码:

1
2
3
4
5
6
7
8
9
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

使用方式如下:

1
2
3
4
@validate_form(TestForm)  # 需要传入要验证的表单类
@app.route('/', methods=['GET', 'POST'])
def index(form):
# 执行到这里说明表单验证通过

经过在项目中的应用,发现装饰器还是有一些缺陷:

  • 无法自定义处理非法表单的逻辑
  • 不支持get方式提交的表单(查看validate_on_submit()源码可知其只支持对post和put方式提交的表单进行验证)

丰富一下

要自定义处理非法表单的逻辑,需要增加一个可以传入自定义逻辑的接口。表单非法时接口的返回往往是一致的,所以我们为所有应用装饰器的路由传入一个统一的处理逻辑。将装饰器封装在一个类中,在类中添加一个配置处理逻辑的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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也是一个装饰器,被它修饰的方法就是处理非法表单的方法。

1
2
3
@form_validator.error_handler
def error_handler(errors):
return jsonify({'errors': errors}), 400

接下来支持get方法,在flask中,我们可以通过request.args来获取到get方法提交的参数。思路是用获取到的参数生成一个表单类的实例,然后就可以通过调用表单类的validate()方法来判断是否合法了。修改validate_form装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def validate_form(self, form_cls):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if request.method == 'GET':
form = form_cls(formdata=request.args)
elif request.method in ('POST', 'PUT'):
form = form_cls()
else:
return fn(*args, **kwargs)
if not form.validate() and self._error_handler:
return self._error_handler(form.errors)
return fn(form, *args, **kwargs)
return wrapper
return decorator

大功告成!使用上面的装饰器,就可以免除在路由函数中重复写表单验证逻辑,并且同时支持put、post和get方法提交的表单。

开箱即用

笔者已经把上面的代码封装成了一个库发布到了PyPI,想直接用的朋友可以使用pip install flask-wtf-decorators安装,项目源码也已经发布到Github。

查看PyPI

查看Github

欢迎关注Python私房菜

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

面试的时候,有没有被问到Python传参是传引用还是传值这种问题?有没有听到过Python传参既不是传值也不是传引用这种说法?一个小小的参数默认值也可能让代码出现难以查找的bug?

如果你也遇到过上面的问题,不妨我们来探究下Python函数传递的种种。

万物皆对象

Python中有一个非常重要的概念——万物皆对象,无论是一个数字、字符串,还是数组、字典,在Python中都会以一个对象的形式存在。

1
a = 123

对于上面这行代码,在Python看来就是创建一个PyObject对象,值为123,然后定义一个指针a,a指向这个PyObject对象。

可变对象和不可变对象

Python中的对象分为两种类型,可变对象和不可变对象,不可变对象指tuple、str、int等类型的对象,可变对象指的是dict、list、自定义对象等类型的对象,我们用一段代码说明他们的区别。

1
2
3
4
5
6
7
8
9
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指向这个新的对象。

参数传递时的表现

了解了对象的原理后,我们就可以来尝试理解一下参数传递时他们的不同表现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

通过上面的代码可以看出,修改传进的可变参数时,会对外部对象产生影响,修改不可变参数时则不会影响。

概括地说,Python参数传递时,既不是传对象也不是传引用,之所以会有上述的区别,跟Python的对象机制有关,参数传递只是给对象绑定了一个新的变量(实际上是传递C中的指针)。

参数传递时的坑

理解了参数传递的逻辑,我们需要注意一下这种逻辑可能引发的问题。

1
2
3
4
5
6
7
def test(b=[]):
b += [1]
print(b)

test() # [1]
test() # [1, 1]
test() # [1, 1, 1]

上面的代码的输出,按照可变对象传参的逻辑,应该每次调用都输出[1]才对,而实际输出看上去好像默认参数好像只生效了一次。原因在于Python的函数也是对象(万物皆对象),这个对象只初始化一次,加上参数又是不可变对象,所以每次调用实际上都修改的是一个对象。

解决这个问题,推荐再参数传递可变对象时,默认值设置为None,在函数内部对None进行判断后再赋予默认值。

1
2
3
4
5
6
7
8
def test(b=None):
b = b or []
b += [1]
print(b)

test() # [1]
test() # [1]
test() # [1]

再看一段代码。

1
2
3
4
5
6
i = 1
def test(a=i):
print(a)

i = 2
test() # 1

由于参数默认值是在函数定义时而不是函数执行时确定的,所以这段代码test方法的参数默认值时1而不是2。

关注Python私房菜

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

asyncio于Python3.4引入标准库,增加了对异步I/O的支持,asyncio基于事件循环,可以轻松实现异步I/O操作。接下来,我们用基于asyncio的库实现一个高性能爬虫。

准备工作

Earth View from Google Earth是一款Chrome插件,会在打开新标签页时自动加载一张来自Google Earth的背景图片。

Earth View from 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请求库来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的语法糖。

加入并行逻辑

上面的代码是抓取单张图片的逻辑,批量抓取图片,需要再嵌套一层方法:

1
2
3
4
5
6
7
async def fetch_all_images():
# 使用Semaphore限制最大并发数
sem = asyncio.Semaphore(10)
ids = [id for id in range(1000, 8000)]
for current_id in ids:
async with sem:
await fetch_image_by_id(current_id)

接下来,将这个方法加入到asyncio的事件循环中。

1
2
3
event_loop = asyncio.get_event_loop()
future = asyncio.ensure_future(fetch_all_images())
results = event_loop.run_until_complete(future)

使用uvloop加速

uvloop基于libuv,libuv是一个使用C语言实现的高性能异步I/O库,uvloop用来代替asyncio默认事件循环,可以进一步加快异步I/O操作的速度。

uvloop的使用非常简单,只要在获取事件循环前,调用如下方法,将asyncio的事件循环策略设置为uvloop的事件循环策略。

1
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

使用上面的代码,我们可以快速将大约1500张的图片爬取下来。

爬取下来的Google Earth图片

性能对比

为了验证aiohttp和uvloop的性能,笔者使用requests+concurrent库实现了一个多进程版的爬虫,分别爬取20个id,消耗的时间如图。

可以看到,耗时相差了大概7倍,aiohttp+uvloop的组合在爬虫这种I/O密集型的场景下,可以说具有压倒性优势。相信在不远的将来,基于asyncio的库会将无数爬虫工程师从加班中拯救出来。

扫码关注Python私房菜

拯救加班的你

与面试官谈笑风生 | Python面向对象之访问控制

Python从设计之初就是一门面向对象的语言,面向对象思想的第一个要素就是封装。所谓封装,通俗的讲就是类中的属性和方法,分为公有和私有,公有可以被外界访问,私有不能被外界访问,这就是封装中最关键的概念——访问控制。

面向对象编程

访问控制有三种级别:私有、受保护、公有

私有(Private):只有类自身可以访问
受保护(Protected):只有类自身和子类可以访问
公有(Public):任何类都可以访问

由于Python不像Java,有访问控制符(private / public / protected),所以Python的访问控制也是容易被应聘者忽视和搞错的。

公有(Public)

在Python的类中,默认情况下定义的属性都是公有的。

1
2
3
4
5
6
7
8
9
10
class Foo(object):
bar = 123

def __init__(self, bob):
self.bob = bob

print(Foo.bar) # 123

foo = Foo(456)
print(foo.bob) # 456

上面类Foo中的bar属性就是类属性,__init__方法中定义的bob是实例属性,barbob都是公有的属性,外部可以访问,分别print类中的bar和实例中的bob,输出了对应的值。

受保护(Protected)

在Python中定义一个受保护的属性,只需要在其名字前加一个下划线_,我们将Foo方法中的bobbar改为_bob_bar,他们就变成了受保护的属性了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo(object):
_bar = 123

def __init__(self, bob):
self._bob = bob


class Son(Foo):

def print_bob(self):
print(self._bob)

@classmethod
def print_bar(cls):
print(cls._bar)


Son.print_bar() # 123

son = Son(456)
son.print_bob() # 456

定义一个类Son继承自Foo,由于受保护的对象只能在类的内部和子类中被访问,不能直接调用print(Son._bar)print(son._bob)来输出这两个属性的值,所以定义了print_barprint_bob方法,实现在子类中输出,这段代码也正常的输出了_bar_bob的值。

接下来,试着反向验证一下,在类的外部,能不能访问其属性,将上面代码的输出部分修改如下:

1
2
3
4
print(Son._bar)  # 123

son = Son(456)
print(son._bob) # 456

(假装)惊讶的发现,竟然没有报错,也输出了正确的值。

Python中用加下划线来定义受保护变量,是一种约定的规范,而不是语言层面真的实现了访问控制,所以,我们定义的保护变量,依然可以在外部被访问到(这是个feature,不是bug)。

私有(private)

Python定义私有属性,需要在属性名前加两个下划线__,把上面的代码修改一下,运行一下会发现下面的代码中的任何一个print都会报错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo(object):
__bar = 123

def __init__(self, bob):
self.__bob = bob


class Son(Foo):

def print_bob(self):
print(self.__bob) # Error

@classmethod
def print_bar(cls):
print(cls.__bar) # Error


print(Son.__bar) # Error

son = Son(456)
print(son._bob) # Error

深入一下——私有属性真的就访问不到了吗?

要了解私有属性是否真的访问不到,需要从Python是如何实现私有属性入手。CPython中,会把双下划线的属性变为_ClassName__PropertyName的形式,用代码演示一下:

1
2
3
4
5
class Foo(object):
__bar = 123


print(Foo._Foo__bar) # 123

运行一下可以知道,正常输出了__bar的值,但是不推荐这样去访问私有属性,因为不同的Python解释器对于私有属性的处理不一样。

特例

使用双下划线定义私有属性,有一种特殊情况,当属性后也有两个下划线的时候,这个属性会被Python解释器当做魔术方法,从而不做私有处理。

1
2
3
4
5
class Foo(object):
__bar__ = 123


print(Foo.__bar__) # 123

上面代码输出了123,证明Python解释器并没有把__bar__当做私有属性。当定义私有属性时,需要注意名字最后最多只能有一个下划线。

另一个特例

假如定义的属性名就叫__呢?不妨直接试一下:

1
2
3
4
5
class Foo(object):
__ = 123


print(Foo.__) # 123

可以发现名字叫__的属性也不会被认为是私有属性,名字是多个下划线的属性也不是私有属性(比如_______)。

函数的访问控制

前面主要介绍了属性的访问控制,在Python中函数是一等公民,所谓一等公民,就是函数可以像变量一样使用,所以函数的访问控制和属性一样,一样应用上面的规则。

关注Python私房菜

只发优质Python文章的公众号

用Python批量提取Win10锁屏壁纸

使用Win10的朋友会发现,每次开机锁屏界面都会有不一样的漂亮图片,这些图片通常选自优秀的摄影作品,十分精美。

但是由于系统会自动更换这些图片,所以就算再好看的图片,也许下次开机之后就被替换掉了。

借助Python,我们可以用简单的几行代码,批量提取这些精美的锁屏图片。把喜欢的图片设置成桌面背景,就不用担心被替换掉啦。

提取原理

Win10系统会自动下载最新的锁屏壁纸,并将他们保存在一个系统文件夹中,路径是C:\Users\[用户名]\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets

随机命名的锁屏图片

直接打开这个文件夹,里面会有随机命名的多个文件,每一个文件就是一张图片。但是由于文件没有扩展名,所以并不能预览。为了不搞坏系统文件,并且把这些文件变成可以预览的格式,我们用Python把这些文件复制出来,加上JPG作为扩展名。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os, shutil
from datetime import datetime


# 把这个文件所在目录wallpapers文件夹作为保存图片的目录
save_folder = dir_path = os.path.dirname(
os.path.realpath(__file__)) + '\wallpapers'
# 动态获取系统存放锁屏图片的位置
wallpaper_folder = os.getenv('LOCALAPPDATA') + (
'\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy'
'\LocalState\Assets')
# 列出所有的文件
wallpapers = os.listdir(wallpaper_folder)
for wallpaper in wallpapers:
wallpaper_path = os.path.join(wallpaper_folder, wallpaper)
# 小于150kb的不是锁屏图片
if (os.path.getsize(wallpaper_path) / 1024) < 150:
continue
wallpaper_name = wallpaper + '.jpg'
save_path = os.path.join(save_folder, wallpaper_name)
shutil.copyfile(wallpaper_path, save_path)
print('Save wallpaper ' + save_path)

首先确定系统存放锁屏图片的文件夹位置,由于文件夹位于用户的个人文件夹内,每个用户的用户名是不一样的,所以我们需要通过系统的LOCALAPPDATA变量动态的获取路径。代码会把提取出来的图片保存在wallpapers文件夹下,所以代码文件所在的目录没有wallpapers文件夹,需要手工创建一个。

在代码文件旁新建一个wallpapers文件夹

执行上面这段Python代码,再打开wallpapers文件夹,就可以看到提取出的锁屏图片了。

关注Python私房菜

原创Python实战文章