Python 实现class_property

Python的property可以让我们很方便地将一个object的函数当做一个属性来操作,可以赋值、读取。

但是不支持class,如果想使用这个特性,就要实例化对象。我去参考了celery实现的class_property,发现有一行特别难懂,花了很长时间琢磨,终于是看懂了。

celery的代码如下:

使用方法如下:

这段代码能work,而是没有任何问题。装饰在class内的一个函数上,可以将此函数的返回值作为一个class的属性,可以通过class和instance来访问,并且还支持setter。

首先,__init__()比较好理解,传入getter和setter,将其转换为classmethod然后绑定到内部的变量上,以便后来调用。

setter()方法也比较好理解,可以当做装饰器调用, 然后已有的self.__get和传入的setter重新初始化一个class_property,如果已经有getter的话,这里也不会对getter造成影响。参考下面的例子:

然后看__get____set__方法,很明显,这里使用的描述器。(关于描述器,我之前有一篇博文详细讲了描述器——理解Python对象的属性和描述器。)第一个if判断是为了确保用class_property装饰的属性通过instance也能访问到。

最后到了最让人不理解的部分了,如果要我实现的话,是直接返回getter就可以了:

return self.__get()

这样看起来也没问题:访问Logging类的时候,发现没有这个already_setup这个属性,然后试图访问描述器,描述器__get__()中返回self.__get()不正好是getter会返回的结果吗?

于是我将这行return改成了上面我的意思,结果报错:

classmethod为什么不是callable的呢?因为classmethod本来就不是callable的!在之前的文章中,我模拟了classmethod的实现:

它的原理是这样的:Class读一个属性的时候,如果Class.__dict__有该属性并且属性不是一个描述器,就返回该属性;如果该属性是一个描述器,就调用它的__get__方法。

举个例子说,下面的代码:

在调用的时候,先看Foo.__dict__里面的bar是一个描述器,那么调用就转换成:

Foo.__dict__['bar'].__get__(None, Foo)()

回到我们的class_property上来,拿最初的Logging举例。用了class_property装饰的already_setup使用起来(Logging.already_setup)就是:

Logging.__dict__['already_setup'].__get__(None, Logging)()

执行我们的class_property的__get__之后,也就是

Logging.__dict__['already_setup'].__get()

此时,橙色的部分拿到的是一个class_property处理之后的classmethod,而上面我们可以看到,classmethod并不是callable的,而是通过描述器协议,class在调用的时候会默认调用__get__,然后拿到一个和class绑定之后的func,这时候的func才是可以调用的。

celery里面原来写的return,调用链展开就是:

Logging.__dict__['already_setup'].__get.__get__(obj, type)()

其中橙色的部分依然是拿到了classmethod,即:

Logging.classmethod_get.__get__(obj, type)()

classmethod的__get__()返回的是一个unbound method,即

Logging.already_setup()

这样调用就没问题了。

所以说,这里手动调用__get__(obj, type)的原因是,描述器协议之能调用一次,如果返回的还是一个描述器,那么并不会继续调用__get__方法,需要手动调用。

 

Python脚本的编码声明

在《Python文件的标准顺序》这篇文章中,介绍了一个Python文件标准的顺序。可以看到,Unix的shebang是优先于Python的文件编码的。本文详细介绍Python文件编码的声明。注意这里指的是Python脚本本身的编码,不是在Python中处理文件的编码。

Python程序员可能对这句话并不陌生,# -*- coding: utf-8 -*-。并且也在自己的脚本中这样写了。为什么要写的这么花哨呢?其实,这是Emacs的编码声明,这样写可以被Emacs编辑器和Pyhton解释器都兼容。对于使用Vim的程序员,其实应该写成这样: vim: set fileencoding=<encoding name> :。(好吧其实并没有Emacs好看)

如何声明?

PEP263中定义了对Python源码文件编码声明。

要定义Python的源代码编码,需要在第一行或第二行编写像下面这样的“magic comment”:
# coding=<encoding name>

编码方式是以一个正则来匹配的:

所以如果写成下面这样,也能工作:

其中编码的名字必须能被Python词法分析器认识,否则会报语法错误:

如果不写,那么将使用默认的编码ASCII(python2):

但是在Python3中默认的编码是UTF-8,所以上面的代码在Python3中运行没有错误。

如何安排编码声明和shebang的顺序?

Unix系统规定,可以使用shebang指定脚本默认的编辑器。例如:

而它被如此调用(”$”是命令提示符)

该命令的输出等同于

shebang的定义如下:

计算机科学中,Shebang(也称为 Hashbang )是一个由井号叹号构成的字符序列 #! ,其出现在文本文件的第一行的前两个字符。 在文件中存在 Shebang 的情况下,类 Unix 操作系统程序载入器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,并将载有 Shebang 的文件路径作为该解释器的参数。

所以说shebang要求文件的第一二个字符是#!,而Python为了兼容shebang,定义文件编码必须在第一行或第二行。

结论就很清晰了,应该先写shebang在写Python的文件编码。

配合Unicode BOM

在windows平台中,也是兼容Unicode BOM的。在文件的开头声明 \xef\xbb\xbf表示文件是UTF-8编码。

如果同时使用了Unicode BOM和文件编码注释,则文件编码注释必须和Unicode BOM相同(UTF-8),否则抛出错误。

 

推荐一个快速在Github打开当前代码行的Vim插件

我们公司用的仓库托管是bitbucket,有时候和同事讨论需要贴代码的链接,需要在网页上扒拉来吧拉去的(代码经常是用文件夹组织的比较深),痛苦啊,今天终于解决了这个痛点。

一开始想的是,在用octotree这种插件可以不刷新快速找代码。然而即使这样还是要你手动去扒拉的,这很反人类。后来我想,用Vim找代码多方便,如果有一个Vim插件,可以调用浏览器在Bitbuck上直接打开当前的代码行就好了。最后找到了/vim-gh-line !完美。我发现有时候Github的搜索框比Google都好用。

这个插件可以使用Vundle安装:

运行BundleInstall即可。

默认的快捷键是 <leader>gh,按下就会调用浏览器打开Github。这个插件从名字就可以看出,只支持Github的,所以我fork了一个版本,添加了<leader>bb可以支持Bitbucket。向原仓库提交了一个PR support bitbucket! #5 ,感觉作者维护这个仓库挺干净的,issue和PR都关掉了,我的这个PR应该也会很快处理吧。

临时可以安装我的fork来同时支持Bitbucket和Github。

这个作者应该是个重度的Vim用户,主页上有很多Vim有关的项目。


PR已经合并并重构,可以直接安装原作者的版本了,自动判断仓库是在github还是Bitbucket。

 

为什么要“包含头不包含尾”?

我记得有一次面试的时候,面试官让我实现一个函数,get_num(start, end),要求返回start和end之间的数字。我给出了一个包含了start不包含end的版本。面试官说这是错的,我反驳道,Python内置的range()就是这样的。记忆尤新。最近终于知道了这样做的原因。

首先从实用方面来看,这样做有很多好处。当然,我会拿Python来举例。

  • 当只有最后一个位置信息的时候,我们可以快速看到到底有几个元素。比如range(4)就是4个元素,my_list[:5]是5个元素
  • 当看到开始下标和结束下标的时候,我们一下就可以算出其中有多少个元素,例如my_list[2:11]就是9个元素
  • 我们可以使用下标将序列表示成两部分,不会重叠。例如,my_list[:x]my_list[x:]是既没有重叠也没有遗漏的两个部分

其实这个问题就像下标应该从0开始还是从1开始一样,又像先有鸡还是先有蛋一样,是很容易引起争议的问题。但是Edsger Wybe Dijkstra对这个问题给出了一个无懈可击的答案,看了之后,我觉得包含头不包含尾就应该是天经地义的!

Dijkstra教授(是的就是那个Dijkstra,以下简称教授)曾手写过一份备忘录,题目是Why numbering should start at zero,虽然题目是“为什么下标应该从0开始”,但是教授在备忘录中很可爱又认真地解释了为什么2, 3, ..., 12就应该表示为2 ≤ i < 13,其他的都是错的,都应该绑在十字架上烧死(后半句是我加的)。此备忘录笔记清洗漂亮,pdf可以在 http://www.cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF 看到,文字版可以在 https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html 看到。

表示上面2到12之间的数据,无外乎以下四种:

a) 2 ≤ i < 13

b) 1 < i ≤ 12

c) 2 ≤ i ≤ 12

d) 1 < i < 13

首先,a)和b)有很大优势,因为a)和b)一眼就能看出元素的个数(上文也提到了);而且根据已知的推论,“两个相邻的序列”应该是,前一个序列的上界应该是后一个序列的下界,这样两个序列可以不重叠又相邻。从这两点看,a)和b)都更好。

然后,我们看下界。我们知道,存在最小的自然数,不存在最大的自然数。所以理应让下界应该是小于等于。如果是小于,那么下界就感觉不是从最小的自然数开始的,ugly!所以我们偏爱a)和c)。

最后我们来看上界。我们已经让序列从最小的自然数开始了,如果上界使用小于等于,那么如果序列收缩为空的时候,怎么表示? 2 ≤ i ≤ ? ugly!如果上界使用小于的话,我们就可以正常的表示成 2 ≤ i < 2 (不存在)了。

综上,a)得到4分,以巨大优势胜出。另外,教授提到有些人对理由充分但是没有实际应用的证据的结论感到不舒服,所以教授在REMARK提到,Xerox PARC开发的Mesa编程语言就讨论了这个问题并且强烈建议使用第一种,后三种及其的UGLY的!

最后教授引申除了为什么将0作为下标(好像教授一直在跑题哦)。我们一定确定要使用a)的表示方法,那么表示长度为N的序列如何表示呢?我们有两个选择: 0 ≤ i < N 1 ≤ i < N + 1。教授认为从0开始更好看,因为这样元素的下标就能表示在它前面的元素的个数。教授说,“这个故事告诉我们,最好将0作为一个自然数。(过了十几个世纪人们才意识到!)”

有意思的是,备忘录后面还有一段,讲了讨论这件事的起因。教授的一个数学同事(当然也是个教授,不过不是计算机的)对他的一些学生发飙了!因为数学教授发现一些年轻的搞计算机的竟然从0开始数数!这不是挑衅嘛!

教授很赞同Antony的一句话:在宗教方面,异教徒必须被烧死不是因为异教徒错了,而是因为“异教徒”有可能是正确的。

 I think Antony Jay is right when he states: “In corporate religions as in others, the heretic must be cast out not because of the probability that he is wrong but because of the possibility that he is right.”

参考资料:

  1. 本文实用角度的三个优点来自于Fluent Python一书第一章
  2. 感谢德州大学保存了教授珍贵的手稿
 

奇葩网站吐槽第三弹

本文是吐槽奇葩网站的第三弹,主要是吐槽网站制作者偷懒或者智商低留下的设计缺陷,可能需要一定的web开发知识才能看懂。之前两篇:

  1. 吐槽一些神奇的政府网站
  2. 奇葩网站吐槽第二弹

170814更新:是一个行政处罚的网站,打开一看,网站给处罚人的身份证打了码,嗯,挺好的,没毛病。

但是打开网络一看……

大哥好歹打码用心一点啊……


20170908更新:一般来说,爬虫都是通过列表页拿到详情页的url,然后主要是从详情页抓数据的。

今天遇到一个列表页的url超级复杂的,根本不知道这些参数怎么得到的。而且请求时不时地返回500(这说明网站开发者也不知道这些参数怎么来的)。网页虽然不是单页应用但写的很烂,嵌套的frame。

就在要放弃的时候,我打开详情页看了一眼……详情页url长这个样子……

这个pkid不就是“primary key id”吗?于是改成1试了一下,果然有数据。大兄弟你列表页的反爬白做了,我直接从pkid=1抓到33w就可以了。


171011更新:今天抓的一个网站上有验证码,不过这个是可以通过机器学习训练模型解决的。有个同事专门负责这个,于是我把这个验证码地址告诉他。奇怪的是,下午继续研究这个网站的时候打不开了…… 后来同事告诉我,他下载验证码太快(4000个),网站挂掉了……

弱不禁风啊。


171024更新:今天遇到一个网站,在meta里面写上了页面的所有内容……大哥您这SEO强势!


20172025更新:复制粘贴编程……