分类目录归档:python

python入门与提高::::::演道网python专栏提供一线python研发人员在学习工作中的经验,减少大家走的弯路,大量源码可以直接使用。

Python信号相关概念及运用

Python信号相关概念及运用

介绍

信号是软件中断。信号提供了一种处理异步事件的方法。

不存在编号为0的信号。kill函数对信号编号0有特殊的应用。POSIX.1将此信号编号值称为空信号。

很多条件可以产生信号:

  • 当用户按某些按键时,引发终端产生的信号。(DELETE,Ctrl+C,Ctrl+\,Ctrl+Z)。这些键可以自定义。
  • 硬件异常产生信号:除数为0、无效的内存引用等。
  • 进程调用kill(2)函数可将信号发送到另一个进程或进程组。有限制:只能发送给同一用户的进程,或者发送信号的进程所有者是超级用户。
  • 用户可用kill(1)命令将信号发送给其他进程。
  • 当检测到某种软件条件发生,并应将其通知有关进程时也产生信号。例句SIGURG、SIGPIPE、SIGALRM。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。
可以要求内核在某个信号出现时按照下列方式之一进行处理:

  1. 忽略此信号。大多数都可以,但有两个不能忽略,他们是SIGKILL和SIGSTOP。原因是:它们向超级用户提供了使进程终止或停止的可靠方法。 另外,有些如果忽略由硬件异常引发的异常(如除0),则进程的行为是未定义的。
  2. 捕捉信号。自定义信号处理函数。注意:不能捕捉SIGKILL和SIGSTOP信号。
  3. 执行系统默认动作。针对大多数信号的系统默认动作是终止进程。

UNIX系统信号

名字 说明 默认动作 细节说明
SIGABRT 异常终止(abort) 终止+core 调用abort函数时产生此信号
SIGALRM 超时(alarm) 终止 调用alarm函数设置的计时器超时,由setitimer函数设置的间隔时间超时时,产生此信号
SIGBUS 硬件故障 终止+core 指示一个实现定义的 硬件故障。当出现某些类型的内存故障时,实现常常产生此信号
SIGCANCEL 线程库内部使用 忽略 Solaris线程库内部使用的信号
SIGCHLD 子进程状态改变 忽略 在一个进程终止或停止时,将SIGCHLD信号发送给其父进程。
SIGCONT 使暂停进程继续 继续/忽略 此作业控制信号被发送给需要继续运行,但当前处于停止状态的进程。
SIGEMT 硬件故障 终止+core 指示一个实现定义的 硬件故障。(emt = emulator trap)。
SIGFPE 算术异常 终止+core 此信号表示算术运算异常,例如除以0,浮点溢出等。
SIGFREEZE 检查点冻结 忽略 仅由Solaris定义。用于通知进程在冻结系统状态之前需要采取特定的动作。
SIGHUP 连接断开 终止 如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)
SIGILL 非法硬件指令 终止+core 此信号指示进程已执行一条非法硬件指令。
SIGINFO 键盘状态请求 忽略 BSD信号
SIGINT 终端中断符 终止 当用户按中断键(一般采用DELETE或Ctrl+C)时,终端驱动程序产生此号并送至前台进程组中的每一个进程。
SIGIO 异步IO 终止/忽略 指示一个异步IO事件。
SIGIOT 硬件异常 终止+core 指示一个实现定义的 硬件故障 (iot = input/output trap)
SIGKILL 终止 终止 这是两个不能被捕捉或忽略的信号之一。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
SIGLWP 线程库内部使用 忽略 Solaris线程库内部使用
SIGPIPE 写至无读进程的管道 终止 如果在写到管道时读进程已终止,则产生此信号。
SIGPOLL 可轮询事件 终止 当在一个 可轮询设备 上发生 一特定事件 时产生此信号。
SIGPROF 梗概时间超时(setitimer) 终止 当setitimer(2)函数设置的梗概统计间隔计时器已到期时产生此信号。
SIGPWR 电源失效/重启 终止/忽略 依赖于系统。主要用于具有不间断电源(UPS)的系统。如果电源失效,则UPS起作用,而且通常软件会接到通知。
SIGQUIT 终端退出符 终止+core 当用户在终端上按退出键(一般是Ctrl+)时,产生此信号,并送至前台进程组中的所有进程。
SIGSEGV 无效的内存引用 终止+core 此信号指示进程进行了一次无效的内存引用 (segv = segmentation violation)
SIGSTKFLT 协处理器栈故障 终止 此信号仅由Linux定义。
SIGSTOP 停止 暂停进程 作业控制信号,用于停止一个进程。类似于交互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。
SIGSYS 无效系统调用 终止+core 该信号指示一个无效的系统调用。
SIGTERM 终止 终止 这是由kill(1)命令发送的系统默认终止信号。
SIGTHAW 检查点解冻 忽略 此信号仅由Solaris定义。
SIGTRAP 硬件故障 终止+core 指示一个实现定义的 硬件故障 。当执行断点指令时,实现常用此信号将控制转移至调试程序
SIGTSTP 终端停止符 暂停进程 交互式停止指令。Ctrl+Z
SIGTTIN 后台读控制tty 暂停进程 当一个后台进程组中进程试图读其控制终端时,终端驱动程序产生此信号。有两种情况不产生。
SIGTTOU 后台写至控制tty 暂停进程 当一个后台进程组中的进程试图写到其控制终端时。一个进程可以允许后台进程写控制终端,不允许时也有两种情况不产生此信号
SIGURG 紧急情况(套接字) 忽略 此信号通知进程已经发生了一个紧急情况。在网络连接上接收到带外的数据时,可选择产生此信号。
SIGUSR1 用户定义的信号 终止 用户定义的信号,可用于应用程序。
SIGUSR2 用户定义的信号 终止 用户定义的信号,可用于应用程序。
SIGVTALRM 虚拟时间闹钟(setitimer) 终止 当一个由setitimer(2)函数设置的虚拟间隔时间到期时产生此信号。
SIGWAITING 线程库内部使用 忽略 此信号由Solaris线程库内部使用
SIGWINCH 终端窗口大小改变 忽略 内核维持与每个终端或伪终端相关联的窗口大小。进程可以用ioctl函数得到或设置窗口的大小。
SIGXCPU 超过CPU限制(setrlimit) 终止+core/忽略 如果进程超过了其软CPU时间限制,则产生SIGXCPU信号。
SIGXFSZ 超过文件长度限制(setrlimit) 终止+core/忽略 如果进程超过了其软文件长度限制,则产生此限制。
SIGXRES 超过资源限制 忽略 此信号仅由Solaris定义。

在下列条件下并不会产生core文件:

  • 进程是设置用户ID的,而且当前用户并非程序文件的所有者。
  • 进程是设置组ID的,而且当前用户并非该程序的组所有者。
  • 用户没有写当前工作目录的权限。
  • 该文件已存在,而且用户对该文件设有写权限。
  • 该文件太大。

signal包

signal包负责在Python程序内部处理信号,典型的操作包括预设信号处理函数,暂停并等待信号,以及定时发出SIGALRM等。要注意,signal包主要是针对UNIX平台(比如Linux, MAC OS),而Windows内核中由于对信号机制的支持不充分,所以在Windows上的Python不能发挥信号系统的功能。

捕捉信号

signal包的核心是使用signal.signal()函数来预设(register)信号处理函数,如下所示:

singnal.signal(signalnum, handler)

signalnum为某个信号,handler为该信号的处理函数。我们在信号基础里提到,进程可以无视信号,可以采取默认操作,还可以自定义操作。当handler为signal.SIG_IGN时,信号被无视(ignore)。当handler为singal.SIG_DFL,进程采取默认操作(default)。当handler为一个函数名时,进程采取函数中定义的操作。

示例代码s1.py:

#coding: utf-8
import signal
# Define signal handler function
def myHandler(signum, frame):
    print('I received: ', signum)

# register signal.SIGTSTP’s handler
signal.signal(signal.SIGTSTP, myHandler)
signal.pause()#等待信号产生,是任意信号。
print('End of Signal Demo')

执行过程1:

执行 python s1.py
键入Ctrl+Z

输出结果:

Z(‘I received: ‘, 18)
End of Signal Demo

执行过程2:

执行 python s1.py
键入Ctrl+C

输出结果:

CTraceback (most recent call last):
File “s1.py”, line 9, in
signal.pause()
KeyboardInterrupt

因为Ctrl+Z产生的SIGTSTP由自定义函数处理了。打印完返回原来代码执行处,继续打印’End of Signal Demo‘后程序退出。
而Ctrl+C产生的SIGINT信号没有设置捕捉函数,按系统默认操作执行,终止程序运行。

发送信号

signal.alarm(),它被用于在一定时间之后,向进程自身发送SIGALRM信号:

import signal
import time
# Define signal handler function
def myHandler(signum, frame):
    print("Now, it’s the time ", time.ctime())
    exit()

# register signal.SIGALRM’s handler
signal.signal(signal.SIGALRM, myHandler)
signal.alarm(1)
while True:
    print('not yet')

输出结果:

not yet
not yet

not yet
Now, it’s the time Thu Feb 9 11:49:05 2017

os包的kill发送信号

signal只有alarm可以发送SIGALRM信号。其它信号需要借助os包。

os.kill(pid, sid) #向进程pid发送信号sid

os.killpg(pgid, sid)#向进程组pgid发送信号sid

其他注意事项

进程捕捉到信号并对其进行处理时,进程正在执行的指令会被临时中断,转而去处理该信号处理程序中的指令,返回继续处理之前被中断的指令。
但是之前被中断的指令可能是malloc,getpwnam等,而信号处理函数也可能调用malloc,getpwnam,结果导致继续执行之前的malloc,getpwnam出错。malloc\getpwnam就是不可重入函数。
相对应的就是可重入函数。

Single UNIX Specification说明了保证可重入函数。一共有117个。
不可重入的原因有:

  • 已知它们使用静态数据结构
  • 它们调用malloc或free
  • 它们是标准I/O函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

应当了解,即使使用可重入函数,但是由于每个线程只有一个errno变量,所以信号处理程序可能会修改其原先值。
因此,作为一个通用的规则,当在信号处理程序中调用可重入函数时,应当在其前保存,在其后恢复errno。

若在信号处理程序中调用一个不可重入函数,则其结果是不可预见的。

#coding: utf-8
import signal
# Define signal handler function
i = 0
def myHandler(signum, frame):
    global i;
    i += 1;
    print('I received: ', signum)

# register signal.SIGTSTP’s handler
signal.signal(signal.SIGTSTP, myHandler)
signal.pause()
i += 1;
print('End of Signal Demo, i=',i)

输出结果:

python s1.py
Z(‘I received: ‘, 18)
(‘End of Signal Demo, i=’, 2)

这个例子比较简单。但是在更复杂的环境下,i被信号捕捉函数修改过并不是显而易见的。

mac下python编辑器

mac下python编辑器

前言

选择一款合适的编辑器有助于快速开发。
本文将会介绍3个。PyCharm、vim、emacs。
笔者在mac osx下这三个都用过,依次接触的vim,emacs,PyCharm。
后面两个是资深党用的,不建议初学者玩。
我个人是打算用emacs,然后用vim的编辑模式。

PyCharm

代码补全,语法错误提示,直接运行,Debug,底下还有Python Console,还有Terminal,版本控制工具。
一直用的比较顺,但是在使用过程中遇到几个问题。

如下代码

threads = []
t1 = threading.Thread(target=music,args=('爱情买卖',))
threads.append(t1)
t2 = threading.Thread(target=move,args=('阿凡达',))
threads.append(t2)

for t in threads:
    t.#这里没有自动补全了,在类似的场合都一样。痛苦。。

另外在看了Python高手之路后,想对代码进行语法规范检查,让期遵循pep8标准。也没有找到插件。

因为个人在使用过程中,发现vim的编辑效率是最高的。故安装了个vim插件。插件名叫IdeaVim。

另外它还支持virtualenv。

因考虑到有时候为了方便,会直接在服务器上写代码。故还得会一个命令行编辑器。于是下面两个出场了。

vim编辑器之神

mac自带vim7。
vim --versions

VIM – Vi IMproved 7.3 (2010 Aug 15, compiled Jun 14 2016 16:06:49)
Unknown option argument: “–versions”
More info with: “vim -h”

偷懒的配置方法可以参考两个命令把 Vim 打造成 Python IDE

自己动手配置参见下面的文章。

配置前准备

在配置python开发环境前,需要确保两点:

  1. Vim编辑版本应该大于7.3。
  2. 支持Python语言。在所选编辑器的功能中,确保你看到了+python。 如没有,可参考mac vim写wordpress 如果满足上述要求,接下来可以安装Vim扩展了。如果不满足,则需要安装/升级。

验证安装

确保你已经安装了7.3版本以上、支持Python的Vim编辑器。你可以再次运行vim –version进行确认。如果你想知道Vim中使用的Python版本,你可以在编辑器中运行:python import sys; print(sys.version)。

2.7.6 (default, Sep 9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)]
这行命令会输出你的编辑器当前的Python版本。如果报错,那么你的编辑器就不支持Python语言,需要重装或重新编译。

安装Vundle

Vim有多个扩展管理器,但是我们强烈推荐Vundle。你可以把它想象成Vim的pip。有了Vundle,安装和更新包这种事情不费吹灰之力。

我们现在来安装Vundle:

git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim

该命令将下载Vundle插件管理器,并将它放置在你的Vim编辑器bundles文件夹中。现在,你可以通过.vimrc配置文件来管理所有扩展了。

将配置文件添加到你的用户的home文件夹中:

touch ~/.vimrc
接下来,把下来的Vundle配置添加到配置文件的顶部:

set nocompatible              " required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" alternatively, pass a path where Vundle should install plugins
"call vundle#begin('~/some/path/here')

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'

" Add all your plugins here (note older versions of Vundle used Bundle instead of Plugin)

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

这样,你就完成了使用Vundle前的设置。之后,你就可以在配置文件中添加希望安装的插件,然后打开Vim编辑器,运行下面的命令:
:PluginInstall
这个命令告诉Vundle施展它的魔法——自动下载所有的插件,并为你进行安装和更新。

打造成IDE

扔掉鼠标

学会扔掉鼠标,习惯使用vim快捷键,你会发现编辑的效率大大提升。
硬件效率越来越高。摆在软件工程上的问题是人的效率问题。
vim编辑快,python代码量小。所以人生苦短,我用python。

Split Layouts

使用:sv 命令打开一个文件,你可以纵向分割布局(新文件会在当前文件下方界面打开),使用相反的命令:vs , 你可以得到横向分割布局(新文件会在当前文件右侧界面打开)。

你还可以嵌套分割布局,所以你可以在分割布局内容再进行分割,纵向或横向都可以,直到你满意为止。众所周知,我们开发时经常需要同时查看多个文件。

专业贴士:记得在输入完:sv后,利用tab补全功能,快速查找文件。

专业贴士:你还可以指定屏幕上可以进行分割布局的区域,只要在.vimrc文件中添加下面的代码即可:

set splitbelow
set splitright

专业贴士:想要不使用鼠标就切换分割布局吗?只要将下面的代码添加到.vimrc文件中,你就可以通过快捷组合键进行切换。

"split navigations
nnoremap <C-J> <C-W><C-J>
nnoremap <C-K> <C-W><C-K>
nnoremap <C-L> <C-W><C-L>
nnoremap <C-H> <C-W><C-H>

组合快捷键:

  • Ctrl-j 切换到下方的分割窗口
  • Ctrl-k 切换到上方的分割窗口
  • Ctrl-l 切换到右侧的分割窗口
  • Ctrl-h 切换到左侧的分割窗口 换句话说, 按Ctrl+Vim的标准移动键,就可以切换到指定窗口。

等等,nnoremap是什么意思?——简单来说,nnoremap将一个组合快捷键映射为另一个快捷键。 nnoremap = n(normal模式)no(非)re(递归)map(映射)。一开始的n,指的是在Vim的正常模式(Normal Mode)下,而不是可视模式下重新映射。基本上,nnoremap 就是说,当我在正常模式按下时,进行操作。更多信息请看这里

缓冲区(Buffers)

虽然Vim支持tab操作,仍有很多人更喜欢缓冲区和分割布局。你可以把缓冲区想象成最近打开的一个文件。Vim提供了方便访问近期缓冲区的方式,只需要输入:b <buffer name or number>,就可以切换到一个已经开启的缓冲区(此处也可使用自动补全功能)。你还可以通过ls命令查看所有的缓冲区。

专业贴士: 在:ls命令输出的最后,Vim会提示“敲击Enter继续查看”,这时你可以直接输入:b <buffer name>,立即选择缓冲区。这样可以省掉一个按键操作,也不必去记忆缓冲区的名字。

代码折叠(Code Folding)

大多数“现代”集成开发环境(IDE)都提供对方法(methods)或类(classes)进行折叠的手段,只显示类或方法的定义部分,而不是全部的代码。

你可以在.vimrc中添加下面的代码开启该功能:

" Enable folding
set foldmethod=indent
set foldlevel=99

这样就可以实现,但是你必须手动输入za来折叠(和取消折叠)。使用空格键会是更好的选择。所以在你的配置文件中加上这一行命令吧:

" Enable folding with the spacebar
nnoremap <space> za

现在你可以轻松地隐藏掉那些当前工作时不需要关注的代码了。

第一个命令,set foldmethod=ident会根据每行的缩进开启折叠。但是这样做会出现超过你所希望的折叠数目。但是别怕,有几个扩展就是专门解决这个问题的。在这里,我们推荐SimplyFold。在.vimrc中加入下面这行代码,通过Vundle进行安装:

Plugin 'tmhedberg/SimpylFold'

不要忘记执行安装命令::PluginInstall

专业贴士: 希望看到折叠代码的文档字符串?

let g:SimpylFold_docstring_preview=1

Python代码缩进

当然,想要代码折叠功能根据缩进情况正常工作,那么你就会希望自己的缩进是正确的。这里,Vim的自带功能无法满足,因为它实现不了定义函数之后的自动缩进。我们希望Vim中的缩进能做到以下两点:

  • 首先,缩进要符合PEP8标准。
  • 其次,更好地处理自动缩进。

PEP8

要支持PEP8风格的缩进,请在.vimrc文件中添加下面的代码:

au BufNewFile,BufRead *.py
\ set tabstop=4 |
\ set softtabstop=4 |
\ set shiftwidth=4 |
\ set textwidth=79 |
\ set expandtab |
\ set autoindent |
\ set fileformat=unix

这些设置将让Vim中的Tab键就相当于4个标准的空格符,确保每行代码长度不超过80个字符,并且会以unix格式储存文件,避免在推送到Github或分享给其他用户时出现文件转换问题。

另外,对于全栈开发,你可以设置针对每种文件类型设置au命令:

au BufNewFile,BufRead *.js, *.html, *.css
\ set tabstop=2 |
\ set softtabstop=2 |
\ set shiftwidth=2

自动缩进

自动缩进有用,但是在某些情况下(比如函数定义有多行的时候),并不总是会达到你想要的效果,尤其是在符合PEP8标准方面。我们可以利用indentpython.vim插件,来解决这个问题:

Plugin 'vim-scripts/indentpython.vim'

标示不必要的空白字符

我们希望避免出现多余的空白字符。可以让Vim帮我们标示出来,使其很容易发现并删除。

hi BadWhitespace guifg=gray guibg=red ctermfg=gray ctermbg=red
au BufRead,BufNewFile *.py,*.pyw,*.c,*.h match BadWhitespace /\s\+$/

这会将多余的空白字符标示出来,很可能会将它们变成红色突出。

支持UTF-8编码

大部分情况下,进行Python开发时你应该使用UTF-8编码,尤其是使用Python 3的时候。确保Vim设置文件中有下面的命令:

set encoding=utf-8

为了避免乱码,请加入下列配置:

set fileformats=unix,mac,dos
set fencs=utf-8,GB18030,ucs-bom,default,latin1

自动补全

支持Python自动补全的最好插件是YouCompleteMe。我们再次使用Vundle安装:

Plugin 'Valloric/YouCompleteMe'

YouCompleteMe插件其实底层使用了一些不同的自动补全组件(包括针对Python开发的Jedi),另外要安装一些C库才能正常工作。插件官方文档提供了很好的安装指南,我就不在这里重复了。切记跟随文档的步骤进行安装。

安装完成后,插件自带的设置效果就很好,但是我们还可以进行一些小的调整:

let g:ycm_autoclose_preview_window_after_completion=1
map <leader>g  :YcmCompleter GoToDefinitionElseDeclaration<CR>

上面的第一行确保了在你完成操作之后,自动补全窗口不会消失,第二行则定义了“转到定义”的快捷方式。

支持Virtualenv虚拟环境

上面“转到定义”功能的一个问题,就是默认情况下Vim不知道virtualenv虚拟环境的情况,所以你必须在配置文件中添加下面的代码,使得Vim和YouCompleteMe能够发现你的虚拟环境:

"python with virtualenv support
py << EOF
import os
import sys
if 'VIRTUAL_ENV' in os.environ:
project_base_dir = os.environ['VIRTUAL_ENV']
activate_this = os.path.join(project_base_dir, 'bin/activate_this.py')
execfile(activate_this, dict(file=activate_this))
EOF

这段代码会判断你目前是否在虚拟环境中编辑,然后切换到相应的虚拟环境,并设置好你的系统路径,确保YouCompleteMe能够找到相应的site packages文件夹。

语法检查/高亮

通过安装syntastic插件,每次保存文件时Vim都会检查代码的语法:

Plugin 'scrooloose/syntastic'

还可以通过这个小巧的插件,添加PEP8代码风格检查:

Plugin 'nvie/vim-flake8'

最后,让你的代码变得更漂亮:

let python_highlight_all=1
syntax on

配色方案

配色方案可以和你正在使用的基础配色共同使用。GUI模式可以尝试solarized方案, 终端模式可以尝试Zenburn方案:

Plugin 'jnurmine/Zenburn'
Plugin 'altercation/vim-colors-solarized'

接下来,只需要添加一点逻辑判断,确定什么模式下使用何种方案就可以了:

if has('gui_running')
  set background=dark
  colorscheme solarized
else
  colorscheme Zenburn
endif

Solarized方案同时提供了暗色调和轻色调两种主题。要支持切换主题功能(按F5)也非常简单,只需添加:

call togglebg#map("<F5>")

文件浏览

如果你想要一个不错的文件树形结构,那么NERDTree是不二之选。

Plugin 'scrooloose/nerdtree'

如果你想用tab键,可以利用vim-nerdtree-tabs插件实现:

Plugin 'jistr/vim-nerdtree-tabs'

还想隐藏.pyc文件?那么再添加下面这行代码吧:

let NERDTreeIgnore=['\.pyc$', '\~$'] "ignore files in NERDTree

超级搜索

想要在Vim中搜索任何文件?试试ctrlP插件吧:

Plugin 'kien/ctrlp.vim'

正如插件名,按Ctrl+P就可以进行搜索。如果你的检索词与想要查找的文件相匹配的话,这个插件就会帮你找到它。哦,对了——它不仅仅可以搜索文件,还能检索标签!更多信息,可以观看这个Youtube视频.

显示行号
开启显示行号:

set nu

Git集成

想要在Vim中执行基本的Git命令?vim-fugitive插件则是不二之选。

Plugin 'tpope/vim-fugitive'

请看Vimcasts的这部视频,了解更多情况。

Powerline状态栏

Powerline是一个状态栏插件,可以显示当前的虚拟环境、Git分支、正在编辑的文件等信息。

这个插件是用Python编写的,支持诸如zsh、bash、tmux和IPython等多种环境。

Plugin 'Lokaltog/powerline', {'rtp': 'powerline/bindings/vim/'}

请查阅插件的官方文档,了解配置选项。

系统剪贴板

通常Vim会忽视系统剪贴板,而使用自带的剪贴板。但是有时候你想从Vim之外的程序中剪切、复制、粘贴文本。在OS X平台上,你可以通过这行代码访问你的系统剪贴板:

set clipboard=unnamed

Shell开启Vim编辑模式

最后,当你熟练掌握了Vim和它的键盘快捷方式之后,你会发现自己经常因为shell中缺乏相同的快捷键而懊恼。没关系,大部分的shell程序都有Vi模式。在当前shell中开启Vi模式,你只需要在~/.inputrc文件中添加这行代码:

set editing-mode vi

现在,你不仅可以在shell中使用Vim组合快捷键,还可以在Python解释器以及任何利用GNU Readline程序的工具(例如,大多数的数据库shell)中使用。现在,你在什么地方都可以使用Vim啦!

参考阅读

vim基本命令
vim进阶命令
解决复制粘贴代码自动缩进问题
vim讲解生动篇
把vim打造成ide
Vim/Vi编程提升编写速度技巧

emacs神的编辑器

安装略 ,配置emacs可以参考https://github.com/redguardtoo/emacs.d

配置python开发环境可以参考

总结

特性 PyCharm VIM Emacs
代码自动补全
语法错误提示
语法规范检查pep8 x
运行代码
Debug
git
virtualenv
代码跳转

解决pip安装慢

解决pip安装慢

在学习Python的过程中,发现pip安装慢,经常timeout,因为国外镜像太慢了。

网上找了下解决方案。有两个。都是使用国内镜像。

  1. 临时解决,加-i参数。pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 软件包
  2. 永久解决。配置环境。 编辑 ~/.pip/pip.conf文件(如没有,请创建)
[global]
timeout = 60
https://pypi.tuna.tsinghua.edu.cn/simple
download-cache = /Users/maynard/software/pip_cache

download-cache是用来缓存下载的软件包的。这样就可以在多个虚拟环境里复用。
请把路径改成自己的。。

清华镜像:https://pypi.tuna.tsinghua.edu.cn/simple
豆瓣镜像:http://pypi.douban.com/simple

Python3标准库urllib

Python3标准库urllib

前言

做web开发的,和http请求打交道最多了,不得不熟悉的就是urllib。当然爬虫也经常用。并且有好的第三方库requests。
本文就介绍这些东东。
note:是在python3.5下测试运行。

urllib

urllib有4个库。分别是:

  • urllib.request 打开和读url
  • urllib.error 包含由urllib.request抛出的异常。
  • urllib.parse 用来解析url
  • urllib.robotparser 用来解析robots.txt文件。

玩爬虫的同学请关注一下urllib.robotparser,做一个好程序员。-^

request

两行代码拿到页面内容。

get请求

from urllib.request import urlopen
print(urlopen("http://www.baidu.com").read())

urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
支持http、https、ftp协议。
urlopen返回 一个类文件对象,他提供了如下方法:

  • read() , readline() , readlines() , fileno() , close() :这些方法的使用方式与文件对象完全一样;
  • info():返回一个httplib.HTTPMessage 对象,表示远程服务器返回的头信息;
  • getcode():返回Http状态码。如果是http请求,200表示请求成功完成;404表示网址未找到;
  • geturl():返回请求的url;

参数说明:

  • url 可以是字符串,也可以是Request对象.
  • data 用于post请求时的数据。
  • timeout 超时时间
  • cafile & capath & cadefault 用于https的ca证书
  • context 同样是用于https请法庭。

需要注意的是这个方法使用的HTTP/1.1协议,并且在HTTP header里自动加入了Connection:close。

post请求

同样可以用urllib.request.urlopen实现。传入data即可。
第二种方式是传入的url是个Request对象。Request的构造函数中传入data即可。
如果没有数据传输,只能用第二第方式,只是Request对象的函数函数中需要明确指定method=’POST’。

完整的Request类定义如下:
urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)

参数说明:

  • url 字符串的网址
  • data 用于post的数据。如果有,则method自动为’POST’
  • headers http头。
  • origin_req_host 原始请求host。
  • unverifiable 用于指示请求是否是不可证实的。
  • method http方法

示例如下:

from urllib import request
from urllib import parse
#从这可以看出,前端的校验是给小白用的.码农是可以直接发起http请求,绕过前端js校验的.
data = {'name':'test1','email':'test@qq.com','passwd':'123456'}
response  = request.urlopen('http://awesome.go2live.cn/api/users',parse.urlencode(data).encode('utf-8'))
print(response.getcode())
print(response.read())

自定义header

前文已经说了。在构造Request对象时,传入headers参数即可。也可以之后调用Request的add_header方法。
这个主要用来模拟User-Agent和HTTP_REFERER。因为这两个经常用来在后端屏蔽掉请求。

譬如我的博客站点就是有屏蔽功能的。

直接请求会失败。

from urllib.request import urlopen
print(urlopen("http://www.go2live.cn").read())

输出结果:

Traceback (most recent call last):

urllib.error.HTTPError: HTTP Error 403: Forbidden

通过构造合适的user-agent就可以正常访问了。

from urllib import request
req = request.Request('http://www.go2live.cn',headers={'User-Agent':'Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11'})
response  = request.urlopen(req)
print(response.getcode())

输出结果:

200

还有Referer是用来防盗链的。请求某些资源时,Referer必须是来自指定的网站才可以,否则403拒绝访问。
另一个是Content-Type。
在post请求,自动成了application/x-www-form-urlencoded。
但是在api请求时,现在好多都改成了json的。这个时候需要用application/json。
另外上传文件时也不一样:multipart/form-data。

增加cookie

这个主要用来处理需要登录才能访问的页面。
这个稍为麻烦点。修复修改下opener才能处理。
http.cookiejiar包是来保存cookie的。
request.HTTPCookieProcessor是用来处理cookie的。
完整的代码示例如下:

from urllib import request
from urllib import parse
import http.cookiejar
import hashlib

URL_ROOT = 'http://awesome.go2live.cn'

cookie = http.cookiejar.CookieJar()# 用来保存cookie的对象
handler = request.HTTPCookieProcessor(cookie) #处理cookie的工具
opener = request.build_opener((handler))
response = opener.open(URL_ROOT)
print("before login")
for item in cookie:
    print('Name={0}, Value={1}'.format(item.name, item.value))
    
data = {'email':'test@qq.com','passwd':'123456'}
data['passwd'] = data['email']+":"+data['passwd']
data['passwd'] = hashlib.sha1(data['passwd'].encode()).hexdigest()
req = request.Request(URL_ROOT+'/api/authenticate', parse.urlencode(data).encode())
response = opener.open(req)
print(response.read())
print("after login")
for item in cookie:
    print('Name={0}, Value={1}'.format(item.name, item.value))

输出结果:

before login
b'{“admin”: 0, “created_at”: 1486789465.7326, “id”: “001486789465697a2db999d99f84506a24a457bee0eab76000”, “name”: “test”, “email”: “test@qq.com“, “passwd”: “******”, “image”: “http://www.gravatar.com/avatar/bf58432148b643a8b4c41c3901b81d1b?d=mm&s=120″}
after login
Name=awesession, Value=001486789465697a2db999d99f84506a24a457bee0eab76000-1486887360-220a1b13969736bb0f868cfef9076023a7ea3b02

上传文件

用还是urlopen方法。只是data的构造真的好蛋疼。。

#!/usr/bin/env python
urllib.request
import urllib.parse
import random, string
import mimetypes

def random_string (length):
    return ''.join (random.choice (string.ascii_letters) for ii in range (length + 1))

def encode_multipart_data (data, files):

    def get_content_type (filename):
        return mimetypes.guess_type (filename)[0] or 'application/octet-stream'

    def encode_field (field_name):
        return ('-----------------------------96951961826872/r/n',
                'Content-Disposition: form-data; name="%s"' % field_name, '/r/n'
                '', str (data [field_name]))

    def encode_file (field_name):
        filename = files [field_name]
        return ('-----------------------------96951961826872/r/n',
                'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename), '/r/n'
                'Content-Type: %s' % get_content_type(filename), '/r/n/r/n'
                '', open (filename, 'rb').read ())

    lines = []
    for name in data:
        lines.extend (encode_field (name))
    for name in files:
        lines.extend (encode_file (name))
    lines.extend ('/r/n-----------------------------96951961826872--/r/n')
    body = b''
    for x in lines:
        if(type(x) == str):
            body += x.encode('ascii')
        else:
            body += x
    headers = {'Content-Type': 'multipart/form-data; boundary=---------------------------96951961826872',
               'Content-Length': str (len (body))}

    return body, headers

def main():
    url = 'http://awesome.go2live.cn'
    data = {}
    files = {'notePhoto': '01.jpg'}
    req = urllib.request.Request (url, *encode_multipart_data (data, files))
    response = urllib.request.urlopen(req)
if __name__ == '__main__':
    main()

下载文件

这个其实没啥。

from urllib import request
response = request.urlopen('http://stock.gtimg.cn/data/get_hs_xls.php?id=ranka&type=1&metric=chr')
with open('test1.xls', 'wb') as fd:
    fd.write(response.read())

Debug调试

这个有时候要看看具体的http请求是啥样的。
使用下面的方式会把收发包的内容在屏幕上打印出来。

from urllib import request

http_handler = request.HTTPHandler(debuglevel=1)
https_handler = request.HTTPSHandler(debuglevel=1)
opener = request.build_opener(http_handler,https_handler)
request.install_opener(opener)
response = request.urlopen('http://m.baidu.com')
print(response.read())

输出结果:

send: b’GET http://m.baidu.com HTTP/1.1\r\nAccept-Encoding…
reply: ‘HTTP/1.1 200 OK\r\n’
header: Cache-Control header: Content-Length header: Content-Type…

内容太长。打…省略。

requests库

相当强大的一个第三方库。支持以下特性:

  • International Domains and URLs
  • Keep-Alive & Connection Pooling
  • Sessions with Cookie Persistence
  • Browser-style SSL Verification
  • Basic/Digest Authentication
  • Elegant Key/Value Cookies
  • Automatic Decompression
  • Automatic Content Decoding
  • Unicode Response Bodies
  • Multipart File Uploads
  • HTTP(S) Proxy Support
  • Connection Timeouts
  • Streaming Downloads
  • .netrc Support
  • Chunked Requests
  • Thread-safety

官方文档

get请求

同样简单的很,两行代码即可。

import requests
print(requests.get('http://blog.go2live.cn').content)

post请求

requests的api好简单。get请求就调用get方法。post请求就调用post方法。

import requests

print(requests.post('http://awesome.go2live.cn/api/users',data={'name':'test1','email':'test@qq.com','passwd':'123456'}).content)

两行代码搞定。对比下urllib用了4行代码简单多了。。

而要传入json数据。再加个json参数就ok了。

自定义header

这个好简单,直接添加一个参数就可以了。

>>> url = 'https://api.github.com/some/endpoint'
>>> headers = {'user-agent': 'my-app/0.0.1'}

>>> r = requests.get(url, headers=headers)

处理cookie

cookie可以直接用响应对象的cookies变量拿到。

>>> url = 'http://example.com/some/cookie/setting/url'
>>> r = requests.get(url)

>>> r.cookies['example_cookie_name']
'example_cookie_value'

发送cookie可以加个cookies字典。

>>> url = 'http://httpbin.org/cookies'
>>> cookies = dict(cookies_are='working')

>>> r = requests.get(url, cookies=cookies)
>>> r.text
'{"cookies": {"cookies_are": "working"}}'

RequestsCookieJar可以设置得更复杂。

>>> jar = requests.cookies.RequestsCookieJar()
>>> jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies')
>>> jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere')
>>> url = 'http://httpbin.org/cookies'
>>> r = requests.get(url, cookies=jar)
>>> r.text
'{"cookies": {"tasty_cookie": "yum"}}'

上传文件

上传文件也简单。用post方法,传个files参数。

>>> url = 'http://httpbin.org/post'
>>> files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
    "files": {
        "file": "<censored...binary...data>"
    },
    ...
}

下载文件

感觉好简单。见下面。

import requests
url = 'http://stock.gtimg.cn/data/get_hs_xls.php?id=ranka&type=1&metric=chr'
r = requests.get(url) 
with open('test.xls', "wb") as code:
    code.write(r.content)

考虑到一次下载,可能文件过大,会消耗过多的内存。
requests推荐的下载方式如下:

import requests
r = requests.get('http://stock.gtimg.cn/data/get_hs_xls.php?id=ranka&type=1&metric=chr',stream=True)
with open('test.xls', 'wb') as fd:
    for chunk in r.iter_content(chunk_size=128):
            fd.write(chunk)

写在后面

对比了urllib和requests库。我决定以后都用requests库了。太方便了。
给个git地址,去fork下吧。

Python标准库abc介绍

Python标准库abc介绍

前言

很多书都没有提到abc。python本身并没有抽象类,抽象函数。它是能过标准库abc提供的。
以前也热衷于造轮子,现在越来越趋向于去熟悉有哪些轮子,能不造则不造,这样开发起来效率快多了。
用于写代码的时候越少,用于思考的时间就越多。
之前看到的几本书,<python标准库>,<python绝技:运用python成为顶级黑客>,<Head+First+Python(中文版)>,,都没有看到abc库。
直到<python学习手册>里才看到。这本书我也是极力推荐阅读的。

最近看到的<Python高手之路>更是极力推荐abc库,说是大多数人居然不知道abc库的存在,还自己去造轮子。

抽象超类的使用方式

Python2.6和Python3.0的抽象超类
方法1:在需要由子类重载的方法中用assert或者raise NotImplementedError异常来指明子类必须重载。
方法2:特殊语法
在Python3.0中,我们在一个class头部使用一个关键字参数,以及特殊的@装饰器语法。

>>> from abc import ABCMeta,abstractmethod
>>> class Super(metaclass=ABCMeta):
...     @abstractmethod
...     def method(self):
...             pass
...
>>>

在Python2.6中,我们使用了一个类属性:

>>> from abc import ABCMeta,abstractmethod
>>> class Super:
...     __metaclass__ = ABCMeta
...     @abstractmethod
...     def method(self):
...             pass
...
>>>

当子类没有重载抽象方法时不能实例化的。

我感觉python中最应该掌握的概念是装饰器。
像上面的@abstractmethod就是用的装饰器语法。
还有@staticmethod和@classmethod 也都是依赖装饰器语法才提供的静态方法和类方法更便捷的使用方法。

ABCMeta更多的使用方式

使用ABCMeta作为metaclass的类有以下方法:
register(subclass)
把subclass注册为这个ABC的抽象子类。例如:

from abc import ABCMeta

class MyABC(metaclass=ABCMeta):
    pass
    
MyABC.register(tuple)

assert issubclass(tuple, MyABC)
assert isinstance((), MyABC)

还有另一种方法把一个类变成一个ABC类的子类。
那就是重写这个ABC类的__subclasshook__(subclass)(这是个类方法)
通过这种方法可以把一个类只需要满足接口要求就自动成为这个ABC类的子类,而不需要调用register方法,又或者让该类继承自此ABC类。
此方法有3返回值:

  • True subclass 就是 这个ABC的子类。
  • False subclass不是这个ABC的子类。
  • NotImplemented. 继续按常规机制检查subclass是否是这个ABC的子类。
from abc import ABCMeta
import collections

class MyAbc(metaclass=ABCMeta):

    @classmethod
    def __subclasshook__(Class,Subclass):
        if Class is MyAbc:
            attributes = collections.ChainMap(*(Superclass.__dict__ for Superclass in Subclass.__mro__))
            methods = ("test","sayhello")
            if all(method in attributes and callable(method) for method in methods):
                return True
            else:
                return False
        return NotImplemented
class Test(MyAbc
    def saygoodbye(self):
        pass

assert(issubclass(Test, MyAbc))

有没有return False,assert结果不一样。
有return False, assert会失败。
没有return False,assert则成功。

Python包管理工具setuptools详解

Python包管理工具setuptools详解

0.什么是setuptools

setuptools是Python distutils增强版的集合,它可以帮助我们更简单的创建和分发Python包,尤其是拥有依赖关系的。用户在使用setuptools创建的包时,并不需要已安装setuptools,只要一个启动模块即可。

功能亮点:

  • 利用EasyInstall自动查找、下载、安装、升级依赖包
  • 创建Python Eggs
  • 包含包目录内的数据文件
  • 自动包含包目录内的所有的包,而不用在setup.py中列举
  • 自动包含包内和发布有关的所有相关文件,而不用创建一个MANIFEST.in文件
  • 自动生成经过包装的脚本或Windows执行文件
  • 支持Pyrex,即在可以setup.py中列出.pyx文件,而最终用户无须安装Pyrex
  • 支持上传到PyPI
  • 可以部署开发模式,使项目在sys.path中
  • 用新命令或setup()参数扩展distutils,为多个项目发布/重用扩展
  • 在项目setup()中简单声明entry points,创建可以自动发现扩展的应用和框架

总之,setuptools就是比distutils好用的多,基本满足大型项目的安装和发布

1.安装setuptools

1) 最简单安装,假定在ubuntu下

sudo apt-get install python-setuptools

2) 启动脚本安装

wget http://peak.telecommunity.com/dist/ez_setup.py
sudo python ez_setup.py

2.创建一个简单的包

有了setuptools后,创建一个包基本上是无脑操作

cd /tmp 
mkdir demo
cd demo

在demo中创建一个setup.py文件,写入

from setuptools import setup, find_packages
setup(
    name = "demo",
    version = "0.1",
    packages = find_packages(),
)

执行python setup.py bdist_egg即可打包一个test的包了。

demo
|-- build
|   `-- bdist.linux-x86_64
|-- demo.egg-info
|   |-- dependency_links.txt
|   |-- PKG-INFO
|   |-- SOURCES.txt
|   `-- top_level.txt
|-- dist
|   `-- demo-0.1-py2.7.egg
`-- setup.py

在dist中生成的是egg包

file dist/demo-0.1-py2.7.egg
dist/demo-0.1-py2.7.egg: Zip archive data, at least v2.0 to extract

看一下生成的.egg文件,是个zip包,解开看看先

upzip -l dist/demo-0.1-py2.7.egg

Archive:  dist/demo-0.1-py2.7.egg
  Length      Date    Time    Name
---------  ---------- -----   ----
        1  2013-06-07 22:03   EGG-INFO/dependency_links.txt
        1  2013-06-07 22:03   EGG-INFO/zip-safe
      120  2013-06-07 22:03   EGG-INFO/SOURCES.txt
        1  2013-06-07 22:03   EGG-INFO/top_level.txt
      176  2013-06-07 22:03   EGG-INFO/PKG-INFO
---------                     -------
      299                     5 files

我们可以看到,里面是一系列自动生成的文件。现在可以介绍一下刚刚setup()中的参数了

  • name 包名
  • version 版本号
  • packages 所包含的其他包

要想发布到PyPI中,需要增加别的参数,这个可以参考官方文档中的例子了。

3.给包增加内容

上面生成的egg中没有实质的内容,显然谁也用不了,现在我们稍微调色一下,增加一点内容。

在demo中执行mkdir demo,再创建一个目录,在这个demo目录中创建一个__init__.py的文件,表示这个目录是一个包,然后写入:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

def test():
    print "hello world!"  

if __name__ == '__main__':
    test()

现在的主目录结构为下:

demo
|-- demo
|   `-- __init__.py
`-- setup.py

再次执行python setup.py bdist_egg后,再看egg包

Archive:  dist/demo-0.1-py2.7.egg
  Length      Date    Time    Name
---------  ---------- -----   ----
        1  2013-06-07 22:23   EGG-INFO/dependency_links.txt
        1  2013-06-07 22:23   EGG-INFO/zip-safe
      137  2013-06-07 22:23   EGG-INFO/SOURCES.txt
        5  2013-06-07 22:23   EGG-INFO/top_level.txt
      176  2013-06-07 22:23   EGG-INFO/PKG-INFO
       95  2013-06-07 22:21   demo/__init__.py
      338  2013-06-07 22:23   demo/__init__.pyc
---------                     -------
      753                     7 files

这回包内多了demo目录,显然已经有了我们自己的东西了,安装体验一下。

python setup.py install

这个命令会讲我们创建的egg安装到python的dist-packages目录下,我这里的位置在

tree /usr/local/lib/python2.7/dist-packages/demo-0.1-py2.7.egg

查看一下它的结构:

/usr/local/lib/python2.7/dist-packages/demo-0.1-py2.7.egg
|-- demo
|   |-- __init__.py
|   `-- __init__.pyc
`-- EGG-INFO
    |-- dependency_links.txt
    |-- PKG-INFO
    |-- SOURCES.txt
    |-- top_level.txt
    `-- zip-safe

打开python终端或者ipython都行,直接导入我们的包

>>> import demo
>>> demo.test()
hello world!
>>>

好了,执行成功!

4.setuptools进阶

在上例中,在前两例中,我们基本都使用setup()的默认参数,这只能写一些简单的egg。一旦我们的project逐渐变大以后,维护起来就有点复杂了,下面是setup()的其他参数,我们可以学习一下

使用find_packages()

对于简单工程来说,手动增加packages参数很容易,刚刚我们用到了这个函数,它默认在和setup.py同一目录下搜索各个含有__init__.py的包。其实我们可以将包统一放在一个src目录中,另外,这个包内可能还有aaa.txt文件和data数据文件夹。

demo
├── setup.py
└── src
    └── demo
        ├── __init__.py
        ├── aaa.txt
        └── data
            ├── abc.dat
            └── abcd.dat

如果不加控制,则setuptools只会将__init__.py加入到egg中,想要将这些文件都添加,需要修改setup.py

from setuptools import setup, find_packages
setup(
    packages = find_packages('src'),  # 包含所有src中的包
    package_dir = {'':'src'},   # 告诉distutils包都在src下

    package_data = {
        # 任何包中含有.txt文件,都包含它
        '': ['*.txt'],
        # 包含demo包data文件夹中的 *.dat文件
        'demo': ['data/*.dat'],
    }
)

这样,在生成的egg中就包含了所需文件了。看看:

Archive:  dist/demo-0.0.1-py2.7.egg
  Length     Date   Time    Name
 --------    ----   ----    ----
       88  06-07-13 23:40   demo/__init__.py
      347  06-07-13 23:52   demo/__init__.pyc
        0  06-07-13 23:45   demo/aaa.txt
        0  06-07-13 23:46   demo/data/abc.dat
        0  06-07-13 23:46   demo/data/abcd.dat
        1  06-07-13 23:52   EGG-INFO/dependency_links.txt
      178  06-07-13 23:52   EGG-INFO/PKG-INFO
      157  06-07-13 23:52   EGG-INFO/SOURCES.txt
        5  06-07-13 23:52   EGG-INFO/top_level.txt
        1  06-07-13 23:52   EGG-INFO/zip-safe
 --------                   -------
      777                   10 files

另外,也可以排除一些特定的包,如果在src中再增加一个tests包,可以通过exclude来排除它,

find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"])

使用entry_points

一个字典,从entry point组名映射道一个表示entry point的字符串或字符串列表。Entry points是用来支持动态发现服务和插件的,也用来支持自动生成脚本。这个还是看例子比较好理解:

setup(
    entry_points = {
        'console_scripts': [
            'foo = demo:test',
            'bar = demo:test',
        ],
        'gui_scripts': [
            'baz = demo:test',
        ]
    }
)

修改setup.py增加以上内容以后,再次安装这个egg,可以发现在安装信息里头多了两行代码(Linux下):

Installing foo script to /usr/local/bin
Installing bar script to /usr/local/bin

查看/usr/local/bin/foo内容

#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'demo==0.1','console_scripts','foo'
__requires__ = 'demo==0.1'
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.exit(
        load_entry_point('demo==0.1', 'console_scripts', 'foo')()
    )

这个内容其实显示的意思是,foo将执行console_scripts中定义的foo所代表的函数。执行foo,发现打出了hello world!,和预期结果一样。

使用Eggsecutable Scripts

从字面上来理解这个词,Eggsecutable是Eggs和executable合成词,翻译过来就是另eggs可执行。也就是说定义好一个参数以后,可以另你生成的.egg文件可以被直接执行,貌似Java的.jar也有这机制?不很清楚,下面是使用方法:

setup(
    # other arguments here...
    entry_points = {
        'setuptools.installation': [
            'eggsecutable = demo:test',
        ]
    }
)

这么写意味着在执行python *.egg时,会执行我的test()函数,在文档中说需要将.egg放到PATH路径中。

包含数据文件

在3中我们已经列举了如何包含数据文件,其实setuptools提供的不只这么一种方法,下面是另外两种

1)包含所有包内文件

这种方法中包内所有文件指的是受版本控制(CVS/SVN/GIT等)的文件,或者通过MANIFEST.in声明的

from setuptools import setup, find_packages
setup(
    ...
    include_package_data = True
)

2)包含一部分,排除一部分

from setuptools import setup, find_packages
setup(
    ...
    packages = find_packages('src'),  
    package_dir = {'':'src'},   

    include_package_data = True,    

    # 排除所有 README.txt
    exclude_package_data = { '': ['README.txt'] },
)

如果没有使用版本控制的话,可以还是使用3中提到的包含方法

可扩展的框架和应用

setuptools可以帮助你将应用变成插件模式,供别的应用使用。官网举例是一个帮助博客更改输出类型的插件,一个博客可能想要输出不同类型的文章,但是总自己写输出格式化代码太繁琐,可以借助一个已经写好的应用,在编写博客程序的时候动态调用其中的代码。

通过entry_points可以定义一系列接口,供别的应用或者自己调用,例如:

setup(
    entry_points = {'blogtool.parsers': '.rst = some_module:SomeClass'}
)

setup(
    entry_points = {'blogtool.parsers': ['.rst = some_module:a_func']}
)

setup(
    entry_points = """
        [blogtool.parsers]
        .rst = some.nested.module:SomeClass.some_classmethod [reST]
    """,
    extras_require = dict(reST = "Docutils>=0.3.5")
)

上面列举了三中定义方式,即我们将我们some_module中的函数,以名字为blogtool.parsers的借口共享给别的应用。

别的应用使用的方法是通过pkg_resources.require()来导入这些模块。

另外,一个名叫stevedore的库将这个方式做了封装,更加方便进行应用的扩展。

5. 以后增加

以上内容大部分来自于官方文档,需要额外学习的以后再增加

转载自http://blog.csdn.net/pfm685757/article/details/48651389

Python虚拟环境virtualenv

前言

实际工作中会遇到这样的问题。
1是安装了多个python版本。因为在python2.x和python3.x同时存在的时代,有应用只支持到python2.x,有应用用的新的python3.x。这不可避免。针对这个已经有了pyenv做这个事,管理多个python版本。

但同时还有一种情况,同一服务器上多个应用用到了同一个包的多个版本。怎么办呢?
如果都装到系统目录,显然是不可行的。virtualenv就正是处理这个问题的。
他为python提供独立的虚拟环境。

使用

使用步骤:

  1. 安装virtualenv pip install virtualenv
  2. 在应用目录内运行virtualenv venv创建虚拟环境。(venv是虚拟环境目录,你可以自己选择自己喜欢的名字) 进行虚拟环境后,shell提示符会变成类似下面这个。
(venv) ➜  mysite vim ~/.pip/pip.conf
  1. 启动虚拟环境source venv/bin/activate。然后做开发、测试、运行。
  2. 如果要退出虚拟环境。运行deactivate

一切就是如此简单。
如此你想在虚拟环境中访问系统库。则在创建虚拟环境时加上--system-site-packages参数即可。
更多参数可以通过virtualenv --help了解,无需去记忆。

后记

值得一提的是因为虚拟环境这么有用。已经有名为tox的工具专门解决这一问题。
另外在Python3.3之后已经内容了venv模块。只是它并不是那么完美。
所以建议不审用virtualenv即可。

Python单元测试

前言

因为Python是动态语言。非常动态,不写单元测试真心没法确认自己的代码是正确的。
即使有单元测试,也只证实测试过的代码是可靠的。本文就是来系统介绍下Python中的单元测试。

基础知识

  1. 最简单的方法是调用assert函数。并有nose包支持。 有个现成的包nose,安装之后,将提供nosetests命令,该命令会加载所有以test_开头的文件,然后执行其中所有以test_开头的函数。

nosetests -v

这种方法尽管简单,但却在很多小的项目中广泛使用且工作良好。除了nose,它们不需要其他工具或库,而且只依赖assert就足够了。
2. Python标准库unittest
用起来也比较简单。只需要创建继承自unittest.TestCase的类,并且写一个运行测试的方法。

import unittest

class TestKey(unittest.TestCase):
    def test_key(self):
        a = ['a','b']
        b = ['b']
        self.assertEqual(a,b) 

有两种运行方法:
1. 同上面的nosetests,但是要求文件名是test_开头。
2. python -m unittest module_name

unittest更多介绍

unittest有很多以assert开头的方法,用来特化测试。如asertDictEqual、assertEqual等。
也可以使用fail(msg)方法有意让某个测试立刻失败。

另外还有如unittest.skip装饰器和unittest.TestCase.skipTest()可以忽略一些测试。

import unittest

try:
    import mylib
except ImportError:
    mylib = None
        
class TestSkipped(unittest.TestCase):
    @unittest.skip("Do not run this")
    def test_fail(self):
        self.fail("This should not be run")

    @unittest.skipIf(mylib is None, "mylib is not available")
    def test_mylib(self):
        self.assertEqual(mylib.foobar(0, 42))

    def test_skip_at_runtime(self):
        if True:
            self.skipTest("Finally I don't want to run it")

输出结果:

python -m unittest -v test_skip
test_fail (test_skip.TestSkipped) … skipped ‘Do not run this’
test_mylib (test_skip.TestSkipped) … skipped ‘mylib is not available’
test_skip_at_runtime (test_skip.TestSkipped) … skipped “Finally I don’t want to run it”


Ran 3 tests in 0.000s

OK (skipped=3)

在许多场景中,需要在运行某个测试前后执行一组通用的操作。(这在web开发中,测试数据库很常见)。
unittest提供了两个特殊的方法setUp和tearDown,它们会在类的每个测试方法调用前后执行一次。

如下例所求。

import unittest

class TestMe(unittest.TestCase):
    def setUp(self):
        self.list = [1,2,3]

    def test_length(self):
        self.list.append(4)
        self.assertEqual(len(self.list),4)


    def test_has_one(self):
        self.assertEqual(len(self.list),3)
        self.assertIn(1,self.list)

    def tearDown(self):
        self.list = None

这样在执行每个测试前会先执行setUp方法。执行完测试后,再执行tearDown方法。

fixture

在单元测试中,fixture表示”测试前创建,测试后销毁”的(辅助性)组件。
unittest只提供了setUp和tearDown函数。不过,是有机制可以hook这两个函数的。
fixturesPython模块提供了一种简单的创建fixture类和对象的机制,如useFixture方法。

fixtures模块提供了一些内置的fixture,如fixtures.EnviromentVariable,对于在os.environ中添加或修改变量很有用,并且变量会在测试退出后重置,如下所求:

import fixtures
import os

class TestEnviron(fixtures.TestWithFixtures):

    def test_environ(self):
        fixture = self.useFixture(fixtures.EnvironmentVariable("FOOBAR","42"))
        self.assertEqual(os.environ.get("FOOBAR"),"42")

    def test_environ_no_fixture(self):
        self.assertEqual(os.environ.get("FOOBAR"),None)

fixtures.TestWithFixtures是unittest.testCase的子类。
大概看了下源码。unittest.testCase有个cleanUp的列表。这个列表里的东西会在tearDown()之后执行。
以达到执行完测试之后,把环境变量还原的目的。

模拟(mocking)

如果正在开发一个HTTP客户端,要想部署HTTP服务器并测试所有场景,令其返回所有可能值,几乎是不可能的。(至少会非常复杂)。此外,测试所有失败场景也是极其困难的。
一种更简单的方式是创建一组根据这些特定场景进行建模的mock对象(模拟对象),并利用它们作为测试环境对代码进行测试。

Python标准库中用来创建mock对象的库名为mock
从Python3.3开始,被命名为unit.mock,合并到Python标准库。因此为兼容,可以用下面的代码。

try:
    from unittest import mock
except ImportError:
    import mock

mock的基本用法。

>>> from unittest import mock
>>> m = mock.Mock()
>>> m.some_method.return_value = 42
>>> m.some_method()
42
>>> def print_hello():
...     print("hello world!")
... 
>>> m.some_method.side_effect = print_hello
>>> m.some_method()
hello world!
>>> def print_hello():
...     print("hello world!")
...     return 43
... 
>>> m.some_method.side_effect = print_hello
>>> m.some_method()
hello world!
43
>>> m.some_method.call_count
3

模拟使用动作/断言模式,也就是说一旦测试运行,必须确保模拟的动作被正确地执行。如下所示:

>>> from unittest import mock
>>> m = mock.Mock()
>>> m.some_method('foo','bar')#方法调用
<Mock name='mock.some_method()' id='4508225488'>
>>> m.some_method.assert_called_once_with('foo','bar')#断言
>>> m.some_method.assert_called_once_with('foo',mock.ANY)#断言
>>> m.some_method.assert_called_once_with('foo','baz')#断言
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/unittest/mock.py", line 803, in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
File "/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/unittest/mock.py", line 792, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: some_method('foo', 'baz')
Actual call: some_method('foo', 'bar')

有时可能需要来自外部模块的函数、方法或对象。mock库为此提供了一组补丁函数。
如下所示:

>>> from unittest import mock
>>> import os
>>> def fake_on_unlink(path):
...     raise IOError("Testing!")
... 
>>> with mock.patch('os.unlink',fake_on_unlink):
...     os.unlink('foobar')
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    File "<stdin>", line 2, in fake_on_unlink
    OSError: Testing!
    >>> 

另外有@mock.patch(origin_method,fake_method) 的装饰器语法,更方便使用。

关于mock,这里有篇更详细的文章Mock 在 Python 单元测试中的使用

场景测试

在进行单元测试时,对某个对象的不同版本运行一组测试是较常见的需求。你也可能想对一组不同的对象运行同一个错误处理测试去触发这个错误,又或者想对不同的驱动执行整个测试集。

考虑下面的实例。
Ceilometer中提供了一个调用存储API的抽象类。任何驱动都可以实现这个抽象类,并将自己注册成为一个驱动。Ceilometer可以按需要加载被配置的存储驱动,并且利用实现的存储API保存和提供数据。这种情况下就需要对每个实现了存储API的驱动调用一类单元测试,以确保它们按照调用者的期望执行。

实现这一点的一种自然方式是使用混入类(mixin class):一方面你将拥有一个包含单元测试的类,另一方面这个类还会包含对特定驱动用法的设置。

import unittest

class MongoDBBaseTest(unittest.TestCase):
    def setUp(self):
        self.connection = connect_to_mongodb()

class MySQLBaseTest(unittest.TestCase):
    def setUp(self):
        self.connection = connect_to_mysql()
        
class TestDatabase(unittest.TestCase):
    def test_connected(self):
        self.assertTrue(self.connection.is_connected())


class TestMongoDB(TestDatabase,MongoDBBaseTest):
    pass
    
class TestMySQL(TestDatabase,MySQLBaseTest):
    pass

然而,从长期维护的角度看,这种方法的实用性和可扩展性都不好。(每增加一个类型的数据库,就需要增加2个类)。
更好的技术是有的,可以使用testscenarios。它提供了一种简单的方式针对一组实时生成的不同场景运行类测试。使用testscenarios重写上面的例子如下:

import testscenarios
from myapp import storage

class TestPythonErrorCode(testscenarios.TestWithScenarios):
    scenarios = [
    ('MongoDB',dict(driver=storage.MongoDBStorage())),
    ('SQL',dict(driver=storage.SQLStorage())),
    ('File',dict(driver=storage.FileStorage())),
    ]
    
    def test_storage(self):
        self.assertTrue(self.driver.store({'foo':'bar'}))
        
    def test_fetch(self):
        self.assertEqual(self.driver.fetch('foo'),'bar')

如上所示,为构建一个场景列表,我们需要的只是一个元组列表,其将场景名称作为第一个参数,并将针对此场景的属性字典作为第二个参数。
针对每个场景,都会运行一遍测试用例。

测试序列与并行

subunit是用来为测试结果提供流协议的一个Python模块。它支持很多有意思的功能,如聚合测试结果或者对测试的运行进行记录或者归档等。

使用subunit运行测试非常简单:
python -m subunit.run test_scenarios
这条命令的输出是二进制数据,好在subunit还支持一组将二进制流转换为其他易读格式的工具。
python -m subunit.run test_scenarios | subunit2pyunit
其他值得一提的工具还有subunit2csv、subunit2gtk和subunit2junitxml。
subunit还可以通过传入discover参数支持自动发现哪个测试要运行。
python -m subunit.run discover | subuint2pyunit
也可以通过传入参数–list只列出测试但不运行。要查看这一结果,可以使用subunit-ls
python -m subunit.run discover --list | subunit-ls --exists

备注:unittest本身也提供了自动发现测试用例的方法。

tests = unittest.TestLoader().discover('tests') #tests参数是指tests目录。
unittest.TextTestRunner(verbosity=2).run(tests)

在大型应用程序中,测试用例的数据可能会多到难以应付,因此让程序处理测试结果序列是非常有用的。testrepository包目的就是解决这一问题,它提供了testr程序,可以用来处理要运行的测试数据库。

$testr init
$ touch .testr.conf
$ python -m subunit.run test_scenarios | testr load

一旦subunit的测试流被运行并加载到testrepository,接下来就很容易使用testr命令了。
通过.testr.conf文件可以实现自动化测试。

[DEFAULT]
test_command=python -m subunit.run discover . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

现在只需要运行testr run就可以将测试加载到testrepository中并执行。
另外可以通过增加–parallel实现并发测试。

测试覆盖

测试覆盖是完善单元测试的工具。它通过代码分析工具和跟踪钩子来判断代码的哪些部分被执行了。在单元测试期间使用时,它可以用来展示代码的哪些部分被测试所覆盖而哪些没有。

安装Python的coverage模块后,就可以通过shell使用coverage程序。
单独使用coverage非常简单且有用,它可以提出程序的哪些部分从来没有被运行时,以及哪些可能是”僵尸代码”。
此外,在单元测试中使用的好处也显而易见,可以知道代码的哪些部分没有被测试过。前面谈到的测试工具都可以和coverage集成。

  1. nosetests和coverage nosetests --cover-package=ceilometer --with-coverage tests/test_pipeline.py
  2. 使用coverage和testrepository python setup.py testr --coverage

使用虚拟环境和tox

tox的目标是自动化和标准化Python中运行测试的方式。基于这一目标,它提供了在一个干净的虚拟环境中运行整个测试集的所有功能,并安装被测试的应用程序以检查其安装是否正常。

使用tox之前,需要提供一个配置文件。这个文件名为tox.ini,需要放在被测试项目的根目录,与setup.py同级。
$ touch tox.ini
现在就可以成功运行tox:

$ tox
...

通过编辑tox.ini可以改变默认行为

[testenv]
deps=nose
     -rrequirements.txt
commands=nosetests

如上配置可运行nosetests命令,并且会自动安装依赖的nose包以及requirements.txt文件中的包。

可以配置多个环境,然后通过tox -e参数指定。

[testenv]
deps=nose
     -rrequirements.txt
commands=nosetests

[testenv:py21]
basepython=python2.1

以上配置就可以通过tox -e py21来测试python2.1版本下的表现。

测试策略

无论你的代码托管在哪里,都应该尽可能实现软件测试的自动化,进而保证项目不断向前推进而不是引入更多Bug而倒退。

更多测试话题

除了nose还有pytest。

在测试出问题的时候,可以通过pdb单步调试来发现问题在哪。pdg可能还是pycharm这类ide工具方便点。

另外推荐看下Flask Web开发 基于Python的Web应用开发实战
这里面讲了好多测试用例的东西,例子丰富。
而且也涵盖了测试覆盖率,测试报告,自动测试,测试客户端,使用Selenium进行端到端的测试。

在FlaskWeb开发中。
1. 针对模型类写了单元测试。
2. 测试客户端,针对网页内容做了测试。
3. 测试API服务。
4. 利用selenium完成测试端到端的服务。(可以测试交互功能,会依赖于js)

Python方法和装饰器

前言

装饰器真的很重要,再怎么强调都不为过。

装饰器

装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。
关键就是修改这块,可以做一些通用处理,以扩大原函数的功能。感觉有点类似java中的切片。

装饰是为函数和类指定管理代码的一种方式。装饰器本身的形式是处理其他的可调用对象的可调用对象(如函数)。
装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码。

通过针对随后的调用安装包装器对象可以实现:

  1. 函数装饰器安装包装器对象,以在需要的时候拦截随后的函数调用并处理它们。
  2. 类装饰器安装包装器对象,以在需要的时候拦截随后的实例创建调用并处理它们。

为什么使用装饰器?

  1. 装饰器有一种非常明确的语法,这使得它们比那些可能任意地远离主体函数或类的辅助函数调用更容易为人们发现。
  2. 当主体函数或类定义的时候,装饰器应用一次;在对类或函数的每次调用的时候,不必添加额外的代码。
  3. 由于前面两点,装饰器使得一个API的用户不太可能忘记根据API需要扩展一个函数或类。

装饰器本质

函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明。
装饰器在紧挨着定义一个函数或方法的def语句之前的一行编写,并且它由@符号以及紧随其后的对于元函数的一个引用组成–这是管理另一个函数的一个函数。

在编码方面,函数装饰器自动将如下的语法:

@decorator #Decorate function
def F(arg):
    ...
    F(99) #调用函数

映射为这一对等的形式,其中装饰器是一个单参数的可调用对象,它返回与F具有相同数目的参数的一个可调用对象:

def F(arg):
    ...
    
F = decorator(F) #rebind function name to decorator result
F(99) # Essentially calls decorator(F)(99)

这一自动名称重绑定在def语句上有效,不管它针对一个简单的函数或是类中的一个方法。当随后调用F函数的时候,它自动调用装饰器所返回的对象,该对象可能是实现了所需的包装逻辑的另一个对象,或者是最初的函数本身。

装饰器自身是一个返回可调用对象的可调用对象

有一种常用的编码模式–装饰器返回了一个包装器,包装器把最初的函数保持到一个封闭的作用域中:

def decorator(F):
    def wrapper(*args, **kwargs):
        #使用F和参数做一些扩展功能,如权限判断等
        #调用原函数
    return wrapper

@decorator
def func(x,y):
    ...
    
func(6,7)

当随后调用名称func的时候,它硬实调用装饰器所返回的包装器函数;随后包装器函数可能会运行最初的func,因为它在一个封闭的作用域中仍然可以使用。当以这种方式编码的时候,每个装饰器的函数都会产生一个新的作用域来保持状态。

functools和inspect

但这样因为改了函数签名等,所以有了functools提供一个wraps装饰器来帮忙保持原函数的函数名和文档字符串。
看看源代码:

WRAPPER_ASSIGNMENTS = ('__module__','__name__','__qualname__','__doc__','__annotations__')
WRAPPER_UPDATES = ('__dict__')
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    wrapper.__wrapped__ = wrapped
    for attr in assigned:
        try:
            value = getattr(wrapped,attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

使用@functools_wraps时实际就是用的这个函数。

inspect模块可以提取函数的签名,把位置参数和关键字参数统一成一个key/value的字典。
从而方便使用,而不必关心到底是位置参数还是关键字参数。

示例如下:

import functools
import inspect

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(f,*args,**kwargs)
        if func_args.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args,**kwargs)
    return wrapper
        

使用类实现装饰器

我们也可以通过对类来重载call方法,从而把类转成一个可调用对象,并且使用实例属性而不是封闭的作用域:

class decorator:
    def __init__(self,func):
        self.func = func
    
    def __call__(self,*args):
        #使用self.func和args来做扩展功能
        #self.func(*args)调用原来的函数
        
@decorator
def func(x,y):
    ...
    
func(6,7)

有一点需要注意,通过类实现的装饰器对象并不能工作在类方法上

因为:当一个方法名绑定只是绑定到一个简单的函数时,Python向self传递了隐含的主体实例;当它是一个可调用类的实例的时候,就传递这个类的实例。
从技术上讲,当方法是一个简单函数的时候,Python只是创建了一个绑定的方法对象,其中包含了主体实例。
反而是利用封闭作用域的嵌套函数工作的更好,既能支持简单函数,也能支持实例方法。

类装饰器

类装饰器和函数装饰器很类似,只不过管理的是类。
通过函数实现,返回了一个包装器类。

def decorator(cls):
    class Wrapper:
        def __init__(self, *args):
            self.wrapped = cls(*args)
        
        def __getattr__(self, name):
            return getattr(self.wrapped, name)
    return Wrapper

@decorator
class C:
    def __init__(self,x,y):
        self.attr = 'spam'

x = C(6,7)
print(x.attr)

每个被装饰的类都创建一个新的作用域,它记住了最初的类。

工厂函数通常在封闭的作用域引用中保持状态,类通常在属性中保持状态。

需要注意通过类实现的类装饰器,看如下的错误示例:

class Decorator:
    def __init__(self, C):
        self.C = C
    
    def __call__(self,*args):
        self.wrapped = self.C(*args)
        return self
    
    def __getattr__(self, attrname):
        return getattr(self.wrapped, attrname)

@Decorator
class C:... #class C实际变成了Decorator的一个实例,只是通过属性保留了原来的C。

x = C() #调用Decorator实例,其实就是调用__call__方法。
y = C()#同上

每个被装饰的类都返回了一个Decorator的实例。
但是对给定的类创建多个实例时出问题了—会对一个Decorator实例反复调用call方法,从而后面的的实例创建调用都覆盖了前面保存的实例。。(也许我们可以利用这个特性来实现单例模式??)

装饰器嵌套

为了支持多步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。
这种形式的装饰器语法:

@A
@B
@C 
def f(...):
    ...

如下这样运行:

def f(...):
    ...
    
f = A(B(C(f)))

类装饰器类似。。

装饰器参数

函数装饰器和类装饰器似乎都能接受参数,尽管实际上这些参数传递给了真正返回装饰器的一个可调用对象,而装饰器反过来又返回了一个可调用对象。例如,如下代码:

@decorator(A,B)
def F(arg):
    ...
F(99)

自动地映射到其对等的形式,其中装饰器是一个可调用对象,它返回实际的装饰器。返回的装饰器反过来返回可调用的对象,这个对象随后运行以调用最初的函数名:

def F(arg):
    ...
F = decorator(A,B)(F) #Rebind F to result of decorator's return value
F(99) #Essentially calls decorator(A,B)(F)(99)

装饰器参数在装饰发生之前就解析了,并且它们通常用来保持状态信息供随后的调用使用。
例如,这个例子中的装饰器函数,可能采用如下的形式:

def decorator(A,B):
    #save or use A,B
    def actualDecorator(F):
        #Save or use function F
        #Return a callable:nested def, class with __call__, etc.
        return callable
    return actualDecorator

换句话说,装饰器参数往往意味着可调用对象的3个层级:
1. 接受装饰器参数的一个可调用对象,它返回一个可调用对象以作装饰器,
2. 实际的装饰器,
3. 该装饰器返回一个可调用对象来处理对最初的函数或类的调用。
这3个层级的每一个都可能是一个函数或类,并且可能以作用域或类属性的形式保存了状态。

##装饰器管理函数和类
装饰器不光可以管理随后对函数和类的调用,还能管理函数和类本身。如下所示,返回函数和类本身:

def decorator(o):
    #Save or augment function or class o
    return o
             
@decorator
def F():... #F=decorator(F)
             
@decorator
class C:... #C = decorator(C)
 ```
 
 函数装饰器有几种办法来保持装饰的时候所提供的状态信息,以便在实际函数调用过程中使用:
 1. 实例属性。
 
 ```python
 class tracer:
     def __init__(self,func):
         self.calls = 0
         self.func = func
     
     def __call__(self, *args, **kwargs):
         self.calls += 1
         print('call %s to %s' % (self.calls, self.func.__name__))
         
 @tracer
 def spam(a,b,c):
     print(a+b+c)
     
 @tracer
 def eggs(x,y):
     print(x ** y)
     
 spam(1,2,3)
 spam(a=4,b=5,c=6)
 
 eggs(2,16)
 eggs(4,y=4)
 
 ```
 
 2. 全局变量
 
 ```
 calls = 0
 def tracer(func):
     def wrapper(*args, **kwargs):
         global calls
         calls += 1
         print('call %s to %s' % (calls, func.__name__))
         return func(*args,**kwargs)
     return wrapper
     
 @tracer
 def spam(a,b,c):
     print(a+b+c)

 spam(1,2,3)
 ```

3. 非局部变量

```python
 def tracer(func):
     calls = 0
     def wrapper(*args, **kwargs):
         nonlocal calls
         calls += 1
         print('call %s to %s' % (calls, func.__name__))
         return func(*args,**kwargs)
     return wrapper
     
 @tracer
 def spam(a,b,c):
     print(a+b+c)

 spam(1,2,3)
 spam(a=4,b=5,c=6)
 ```

4. 函数属性

```python
def tracer(func):
     def wrapper(*args, **kwargs):
         wrapper.calls += 1
         print('call %s to %s' % (wrapper.calls, func.__name__))
         return func(*args,**kwargs)
     wrapper.calls = 0
     return wrapper

在运用描述符的情况下,我们也能把通过类实现的装饰器运用到 类方法上,只是有点复杂,如下所示:

class tracer(object):
    def __init__(self,func):
        self.calls = 0
        self.func = func
    
    def __call__(self, *args, **kwargs):
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    
    def __get__(self,instance,owner):
        return wrapper(self, instance)
        
class wrapper:
    def __init__(self, desc, subj):
        self.desc = desc
        self.subj = subj
    
    def __call__(self, *args, **kwargs):
        return self.desc(self.subj, *args, **kwargs)
    
@tracer
def spam(a,b,c):  #spam = tracer(spam), 返回的tracer实例。当调用spam方法时,调用的就是tracer的__call__方法
    ...same as prior...
    
class Person:
    @tracer
    def giveRaise(self,percent): # giveRaise = tracer(giverRaise)
        ...same as prior...

经常装饰器后giveRaise变成了描述符对象。当person实际调用giveRaise的时候,当是获取giveRaise属性会触发描述符tracer的get调用。get返回了wrapper对象。而wrapper对象又保持了tracer实例和person实例。
当调用giveRaise(此时变成了wrapper对象)时,其实是调用的wrapper实例的call方法。wrapper实例的call方法又回调 tracer的call方法,利用wrapper保持的person实例把person实例当成参数也传了回去。
调用顺序如下:

person.giveRaise()->wrapper.__call__()->tracer.__call__()

这个例子中把wrapper类改成嵌套的函数也可以,而且代码量更少,如下:

class tracer(object):
    def __init__(self,func):
        self.calls = 0
        self.func = func
    
    def __call__(self, *args, **kwargs):
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    
    def __get__(self,instance,owner):
        def wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)
        return wrapper

类装饰器(函数装饰器)的两个潜在缺陷:

  1. 类型修改。当插入包装器的时候,一个装饰器函数或类不会保持其最初的类型–其名称重新绑定到一个包装器对象,在使用对象名称或测试对象类型的程序中,这可能会很重要。
  2. 额外调用。通过装饰添加一个包装层,在每次调用装饰对象的时候,会引发一次额外调用所需要的额外性能成本–调用是相对耗费时间的操作。

Python中方法的运行机制

方法是作为类属性保存的函数。

看下面的例子。

>>> class Pizza():
...     def __init__(self,size):
...         self.size = size
...     def get_size(self):
...         return self.size
... 
>>> Pizza.get_size
<function Pizza.get_size at 0x10a108488>

在python2中是unbound method,而在python3只已经完全删除了未绑定方法这个概念,它会提示get_size是一个函数。

>>>Pizza.get_size()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  TypeError: get_size() missing 1 required positional argument: 'self'

当调用的时候,都会报错。因为少了声明时的self参数。

你明确传一个参数是可以的。

>>> Pizza.get_size(Pizza(42))
42

这个做法在__init__中是最常用的。调用父类的初始化方法。
实际上上述代码等同于下面的代码。

Pizza(42).get_size()

这次没有传参,是因为Python会把Pizza(42)这个对象自动传给get_size的self参数。
这也是Python2.x时有绑定方法的原因。因为方法和某个对象实例绑定起来了,即self参数会自动变成绑定的对象实例。

静态方法

静态方法是属于类的方法,但实际上并非运行在类的实例上。

class Pizza(object):
    @staticmethod
    def mix_ingredients(x,y):
        return x+y

装饰器@staticmethod提供了以下几种功能:

  • Python不必为我们创建的每个Pizza对象实例化一个绑定方法。
  • 提高代码的可读性。当看到@staticmethod时,就知道这个方法不依赖于对象的状态。
  • 可以在子类中覆盖静态方法。

类方法

类方法是直接绑定到类而非它的实例的方法:

class Pizza(object):
    radius = 42
    
    @classmethod
    def get_radius(cls):
        return cls.radius

因为第一个参数要求的是类实例,所以Pizza.get_radius就成为绑定方法了。

抽象方法

最朴素的抽象方法其实是父类抛出异常,让子类去重写。
标准库里提供了abc,请参考Python标准库abc介绍

混合使用静态方法、类方法和抽象方法

从Python3开始,已经支持在@abstractmethod之上使用@staticmethod和@classmethod

不过在基类中声明为抽象方法为类方法并不会强迫其子类也将其定义为类方法。
将其定义为静态方法也一样,没有办法强迫子类将抽象方法实现为某种特定类型的方法。

另外,在抽象方法中是可以有实现代码的,并且子类可以通过super引用到父类的实现。

关于super的真相

python是支持多继承的。那么super到底是谁?

看下面代码:

>>> def parent():
...     return object
... 
>>> class A(parent()):
...     pass
... 
>>> A.mro()
[<class '__main__.A'>, <class 'object'>]

不出所料,可以正常运行:类A继承自父类object。类方法mro()返回方法解析顺序用于解析属性。

super()函数实际上是一个构造器,每次调用它都会实例化一个super对象。它接收一个或两个参数,第一个参数是一个类,第二个参数是一个子类或第一个参数的一个实例。

构造器返回的对象就像是第一个参数的父类的一个代理。它有自己的__getattribute__方法去遍历MRO列表中的类并返回第一个满足条件的属性:

>>> class A(object):
...     bar = 42
...     def foo(self):
...         pass
... 
>>> class B(object):
...     bar = 0
... 
>>> class C(A,B):
...     xyz = 'abc'
... 
>>> C.mro()
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
>>> super(C,C()).bar
42
>>> super(C,C()).foo
<bound method A.foo of <__main__.C object at 0x10a10fbe0>>
>>> super(B).__self__
>>> super(B,B()).__self__
<__main__.B object at 0x10a10fc18>
>>> 

其实super就是利用的MRO嘛。
在Python3中,super()变得更加神奇:可以在一个方法中不传入任何参数调用它。但没有参数传给super()时,它会为它们自动搜索栈框架:

class B(A):
    def foo(self):
        super().foo()

Python函数式编程

函数式编程

在以函数式风格写代码时,函数应该设计成没有其他副作用。也就是说,函数接收参数并生成输出而不保留任何状态或修改任何不反映在返回值中的内容。遵循这种理想方式的函数可以被看成纯函数式函数

举例:

一个非纯函数:

def remove_list(mylist):
    mylist.pop(-1) #修改了参数mylist。使用者在不看代码源码的情况下,并不知道有被修改的副作用。

一个纯函数:

def butlast(mylist):
    return mylist[:-1]

函数式编程具有以下实用的特点。

  • 可形式化证明。
  • 模块化。模块化编码能够在一定程度上强制对问题进行分治解决并简化在其他场景下的重用。
  • 简洁。 函数式编程通常比其他范型更为简洁。
  • 并发。 纯函数式函数是线程安全的并且可以并行运行。
  • 可测性。测试一个函数式程序是非常简单的:所有需要做的仅仅是一组输入和一组期望的输出。而且是幂等的。

生成器

生成器适合运行时计算。从而不用占用那么多内存。
生成器(generator)是这样一种对象:在每次调用它的next()方法时返回一个值(yield返回的),直到它抛出StopIteration。

要创建一个生成器所需要做的只是写一个普通的包含yield语句的Python函数。Python会检测对yield的使用并将这个函数标识为一个生成器。当函数执行到yield语句时,它会像return语句那样返回一个值,但一个明显不同的在于:解释器会保存对栈的引用,它将被用来在下一次调用next函数时恢复函数的执行。

创建一个生成器:

def mygenerator():
    yield 1
    yield 2
    yield 'a'

可以通过inspect.isgeneratorfunction来检查一个函数是否是生成器。

def isgeneratorfunction(object):
    return bool((isfunction(object) or ismethod(object)) and object.func_code.co_flags & CO_GENERATOR)

Python3中提供了另一个有用的函数inspect.getgeneratorstate
看例子可以看到它的作用。

>>> import inspect
>>> def mygenerator():
...     yield 1
... 
>>> gen = mygenerator()
>>> gen
<generator object mygenerator at 0x104f2e468>
>>> inspect.getgeneratorstate(gen)
'GEN_CREATED'
>>> next(gen)
1
>>> inspect.getgeneratorstate(gen)
'GEN_SUSPENDED'
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  StopIteration
  >>> inspect.getgeneratorstate(gen)
  'GEN_CLOSED'
  >>> 

在Python中,生成器的构建是通过当函数产生某对象时保持一个地栈的引用来实现的,并在需要时恢复这个栈,例如,当调用next()时会再次执行。

yield还有一个不太常用的功能:它可以像函数调用一样返回值。这允许通过调用它的send()函数来向生成器传入一个值。

示例:通过yield返回值:

def h():
    print('Wen Chuan')
    m = yield 5
    print(m)
    d = yield 12
    print('We are together!')
                    
c = h()
r = next(c)
print("call 1",r)
r = c.send('Fighting!')
print("call 2",r)

输出结果:

Wen Chuan
call 1 5
Fighting!
call 2 12

从上例可看出。next和send的返回值是yield右边的表达式。
而send 影响的是yield的返回值,即左值。
第一次执行next()时,执行到yield 5, 5即next()的返回值。
再执行send(‘Fighting’)时,执行 m = ‘Fighting’

PEP 289引入了生成器表达式。通过使用类似列表解析的语法可以构建单行生成器。

>>> (x.upper() for x in ['hello','world'])
<generator object <genexpr> at 0x10aefddb0>
>>> gen = (x.upper() for x in ['hello','world'])
>>> list(gen)
['HELLO', 'WORLD']
>>> 

列表解析

列表解析(list comprehension, 简称listcomp)让你可以通过声明在单行内构造列表的内容。
在Python用列表解析比用for循环用的多。

>>> [pow(i,2) for i in (1,2,3)]
[1, 4, 9]

列表解析里可以加if过滤

>>> w(i,2) for i in (1,2,3) if i>=2]
[4, 9]

列表解析可以嵌套

>>> x = [word.capitalize()
... for line in ("hello world?", "world!", "or not")
... for word in line.split()
... if not word.startswith("or")]
>>> x
['Hello', 'World?', 'World!', 'Not']
>>> 

函数式,函数的,函数化

Python包括很多会对函数式编程的工具。这些内置的函数涵盖了以下这些基本部分。

  • map(function, iterable) 对iterable中的每一个元素应用function,并在Py2中返回一个列表,或者在py3中返回可迭代的map对象。
  • filter(function or None, iterable)地iterable中的元素应用function对返回结果进行过滤,并在py2中返回一个列表,或者在py3中返回可迭代的filter对象。
  • enumerate(iterable[,start]) 返回一个可迭代的enumerate对象,它生成一个元组序列,每个元组包括一个整形索引(如果提供了的话,则从start开始)和iterable中对应的元素。当需要参考数组的索引编写代码时这是很有用的。
  • sorted(iterable, key=None, reverse=False)返回iterable的一个已排序版本。通过参数key可以提供一个返回要排序的值的函数。
  • any(iterable)和all(iterable) 都返回一个依赖于iterable返回的值的布尔值。any有一个为真则为值。all,全为值 则为真。
  • zip(iter1[, iter2 […]])接收多个序列并将它们组合成元组。它在将一组键和一组值组合成字典时很有用。

在py2中是返回列表,而不是可迭代对象,从而在内存上面的利用不那么高效。要想也像py3一样返回可迭代对象,可以使用标准库的itertools模块,其提供了许多这些函数的迭代器版本(itertools.izip、itertools.imap、itertools.ifilter等)

有个first模块提供了从一个列表中选择首个满足条件的项。

>>> from first import first
>>> first([0,False,None,[],42])
42
>>> first([-1,0,1,2], key=lambda x: x>0)
1

lambda在单行函数时很在效,可以避免定义单行函数,而直接以内联的方式使用。
但在超过一行的函数时,则不管用了。

functools.partial是以更为灵活的方案替代lambda的第一步。它允许通过一种反转的方式创建一个包装器函数:它修改收到的参数而不是修改函数的行为。

from functools import partial
from first import first

def greater_than(number, min=0):
    return number > min
    
first([-1, 0, 1, 2], key=partial(greater_than, min=42))

Python标准库中的itertools模块也提供了一组非常有用的函数,也很有必要记住。

  • chain(*iterables)依次迭代多个iterables但并不会构造包含所有元素的中间列表。
  • combinations(iterable, r)从给定的iterable中生成所有长度为r的组合。
  • compress(data, selectors)对data应用来自selectors的布尔掩码并从data中返回selectors中对应为真的元素。
  • count(start, step)创建一个无限的值的序列,从start开始,步长为step。
  • cycle(iterable)重复的遍历iterable中的值。
  • dropwhile(predicate, iterable)过滤iterable中的元素,丢弃符合predicate描述的那些元素。
  • groupby(iterable, keyfunc)根据keyfunc函数返回的结果对元素进行分组并返回一个迭代器。
  • permutations(iterable[, r])返回iterable中r个元素的所有组合。
  • product(*iterables)返回iterables的笛卡尔积的可迭代对象,但不使用嵌套的for循环。
  • takewhile(predicate, iterable)返回满足predicate条件的iterable中的元素。

这些函数在和operator模块组合在一起时特别有用。当一起使用时,itertools和operator能够覆盖通常程序员依赖lambda表达式的大部分场景。

示例 结合itertools.groupby使用operator模块

>>> import itertools
>>> a = [{'foo':'bar'},{'foo':'bar','x':42},{'foo':'baz','y':43}]
>>> import operator
>>> list(itertools.groupby(a, operator.itemgetter('foo')))
[('bar', <itertools._grouper object at 0x10af13f28>), ('baz', <itertools._grouper object at 0x10af27198>)]
>>> [(key,list(group)) for key, group in list(itertools.groupby(a, operator.itemgetter('foo')))]
[('bar', []), ('baz', [{'y': 43, 'foo': 'baz'}])]
>>>