ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

Emacs 的 Python3 开发环境配置

基础配置

基础配置就没啥好说的了,就是缩进宽度啦,各种 minor-mode 的添加啦之类的,直接上配置

(defun my-python-mode-config ()
  (setq python-indent-offset 4
        python-indent 4
        indent-tabs-mode nil
        default-tab-width 4

        ;; 设置 run-python 的参数
        python-shell-interpreter "ipython"
        python-shell-interpreter-args "-i"
        python-shell-prompt-regexp "In \\[[0-9]+\\]: "
        python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: "
        python-shell-completion-setup-code "from IPython.core.completerlib import module_completion"
        python-shell-completion-module-string-code "';'.join(module_completion('''%s'''))\n"
        python-shell-completion-string-code "';'.join(get_ipython().Completer.all_completions('''%s'''))\n")

  (add-to-list 'auto-mode-alist '("\\.py\\'" . python-mode))
  (hs-minor-mode t)                     ;开启 hs-minor-mode 以支持代码折叠
  (auto-fill-mode 0)                    ;关闭 auto-fill-mode,拒绝自动折行
  (whitespace-mode t)                   ;开启 whitespace-mode 对制表符和行为空格高亮
  (hl-line-mode t)                      ;开启 hl-line-mode 对当前行进行高亮
  (pretty-symbols-mode t)               ;开启 pretty-symbols-mode 将 lambda 显示成希腊字符 λ
  (set (make-local-variable 'electric-indent-mode) nil)) ;关闭自动缩进

(add-hook 'python-mode-hook 'my-python-mode-config)

另外如果是 Emacs 25.1 的话,有一个已知 bug,会导致执行 run-python 的时候,python shell 里显示一堆乱码,下面的方法能够解决

(setenv "IPY_TEST_SIMPLE_PROMPT" "1")

Emacs + Python3 的问题

从去年开始,因为工作的原因,日常的开发环境从 Python2 切换成了 Python3,一开始还是有一点不太习惯的,其中 Python 本身的语法差异倒真没有带来太多的不适应,一开始的抗拒主要还是因为不少 Python 的库在对 Python3 的支持上多少有点问题。

当然,因为公司是用 Python3 的,碰到上述问题的时候就会去找替代方案了,加上主流的一些库也有了对 Python3 的支持,所以现在已经习惯了用 Python3,而且本身是从事 NLP 相关的工作,读写文本的时候不用每次都 encode/decode,还是挺舒服的。

Python2 还是 Python3 这个就不想讨论了,网上相关的讨论也不少了。我这边的问题主要是,切换成 Python3 后,原来 Python 的配置多少都有点问题,比如语法检查、自动补全等默认都是用系统的 Python 环境的,要处理 Python3 的代码就需要额外做点事情,我这个人实在是懒于是去掉了语法检查、自动补全这些功能,将就着用着最基础的一些功能,倒也不是不能过日子。

有时候也有考虑重新配置一下 Python 环境,但是一看到 elpy 啊 projectile 这些稍微复杂点的 package 就犯懒,倒是这阵子用一些更小的 package 一点一点地加新功能,貌似倒是已经解决了自己的需求了,加上有些时间没写东西了,想着写点东西,顺便分享下踩到的坑吧。

company + jedi-core 的 Python3 配置

首先是自动补全了,一开始是用 auto-complete 的,不过被 auto-complete 坑过太多次了, company 算是后起之秀,配置起来也挺方便的。

把 company 装上

(when (not (require 'company nil :noerror))
  (message "install company now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company))

然后在启动 Emacs 的时候开启全局的 company-mode

(add-hook 'after-init-hook 'global-company-mode)

company-mode 默认已经配置好了多个语言的 backends,基本上是开箱即用的,查看变量 company-backends 可以看它当前使用的 backends,默认是

(company-bbdb
 company-nxml
 company-css
 company-eclim
 company-semantic
 company-clang
 company-xcode
 company-cmake
 company-capf
 company-files
 (company-dabbrev-code company-gtags company-etags company-keywords)
 company-oddmuse company-dabbrev)

这些应付一些简单的场景足够用了。

然后是安装 company-jedi

(when (not (require 'company-jedi nil :noerror))
  (message "install company-jedi now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company-jedi))

company-jedi 是 company 的一个 backend,使用 jedi 这个 Python 的自动补全和静态分析工具。需要注意的是,使用 package-install 安装 company-jedi 就好了,它会安装 jedi-core 这个 package,里面有对 jedi 的封装。说这个是因为用户如果没有看 company-jedi 的说明,有可能会去安装 jedi 这个 Emacs package,但实际上这个 package 是一个 auto-complete 的后端,完全不用。

到目前为止的操作都是通用的,和 Python2/Python3 都没有关系,但要知道,jedi 的工作原理是根据一个 Python 环境里的标准库及安装的非标准库来进行补全的,也就是说,它需要依赖一个外部的 Python 环境,如果去看 emacs-jedi 的文档,会看到要求用户执行 jedi:install-server 来建立一个 Python 环境,而这个命令实际上会在 ~/.emacs.d/.python-environments 这个目录下建立一个 virtualenv 环境,默认用的是 Python2.7。

所以如果想为 Python3 配置 jedi,请注意 不要使用 jedi:install-server 这种方式

既然知道了 emacs-jedi 的工作原理,那就好办了,那我就自己在 ~/.emacs.d/.python-environments 这个目录下建立一个 Python3 的 virtualenv 环境呗。

首先建立 ~/.emacs.d/.python-environments/ 这个目录

mkdir -p ~/.emacs.d/.python-environments/

然后在其中创建 virtualenv 环境,下面的示例中为这个 virtualenv 环境命名为 jedi,取别的名字都可以的

cd ~/.emacs.d/.python-environments/
virtualenv -p /usr/bin/python3  --prompt="<venv:jedi>" jedi

然后在这个 virtualenv 环境中安装需要的 Python 依赖,依赖分两部分,一部分是 jedi 相关的几个 Python 包,是自动补全必须的,这些东西都在 jedi-core 这个 Emacs package 里的 setup.py 里写好了,其内容如下

setup(
    name='jediepcserver',
    version='0.2.7',
    py_modules=['jediepcserver'],
    install_requires=[
        "jedi>=0.8.1",
        "epc>=0.0.4",
        "argparse",
    ],
    entry_points={
        'console_scripts': ['jediepcserver = jediepcserver:main'],
    },
    **args
)

可以看到,依赖的是 jedi 和 epc 两个 Python 包,我们可以手动安装它们

~/.emacs.d/.python-environments/jedi/bin/pip install jedi>=0.8.1 epc>=0.0.4 argparse

也可以直接使用这个 setup.py 来安装

~/.emacs.d/.python-environments/jedi/bin/pip install --upgrade ~/.emacs.d/elpa/jedi-core-20170319.2107/

其次是需要用于补全的 Python 的非标准库,比如说我经常用 sklearn、tensorflow 之类的工具,我想在写相关的代码的时候能补全,那么要在我们刚才建立好的 virtualenv 环境里安装好这些 Python 包。

~/.emacs.d/.python-environments/jedi/bin/pip install tensorflow==1.3.0 scipy==0.19.1 numy==1.13.1 scikit-learn==0.19.0

至此外部的设置都已经好了,然后就是要在 Emacs 里设置来使用我们刚才建立好的这个 virtualenv 环境

(setq jedi:environment-root "jedi")
(setq jedi:server-command (jedi:-env-server-command))

然后设置当打开 Python 代码文件的时候,启动 jedi

(defun config/enable-jedi ()
  (add-to-list 'company-backends 'company-jedi))
(add-hook 'python-mode-hook 'jedi:setup)
(add-hook 'python-mode-hook 'config/enable-jedi)

还有一些补全的细节可以设置,如

  • 输入句点符号 "." 的时候自动弹出补全列表,这个主要是方便用来选择 Python package 的子模块或者方法

    (setq jedi:complete-on-dot t)
    
  • 补全时能识别简写,这个是说如果我写了 "import tensorflow as tf" ,那么我再输入 "tf." 的时候能自动补全

    (setq jedi:use-shortcuts t)
    
  • 设置补全时需要的最小字数(默认就是 3)

    (setq compandy-minimum-prefix-length 3)
    
  • 设置弹出的补全列表的外观

    让补全列表里的各项左右对齐

    (setq company-tooltip-align-annotations t)
    

    如果开启这个,那么补全列表会是下面这个样子

    company-aligned-tooltip.png

    默认是这个样子

    company-default-tooltip.png

    补全列表里的项按照使用的频次排序,这样经常使用到的会放在前面,减少按键次数

    (setq company-transformers '(company-sort-by-occurrence))
    

    在弹出的补全列表里移动时可以前后循环,默认如果移动到了最后一个是没有办法再往下移动的

    (setq company-selection-wrap-around t)
    
  • 对默认快捷键做一些修改

    默认使用 M-n 和 M-p 来在补全列表里移动,改成 C-n 和 C-p

    (define-key company-active-map (kbd "M-n") nil)
    (define-key company-active-map (kbd "M-p") nil)
    (define-key company-active-map (kbd "C-n") 'company-select-next)
    (define-key company-active-map (kbd "C-p") 'company-select-previous)
    

    设置让 TAB 也具备相同的功能

    (define-key company-active-map (kbd "TAB") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "<tab>") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "S-TAB") 'company-select-previous)
    (define-key company-active-map (kbd "<backtab>") 'company-select-previous)
    

结合 virtualenv 来使用 flycheck

首先我们要安装 flycheck 这个实时语法检查工具

(when (not (require 'flycheck nil :noerror))
  (message "install flycheck now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'flycheck))

在 python-mode 里启用也很简单

(defun config/enable-flycheck ()
  (flycheck-mode t))
(add-hook 'python-mode-hook 'config/enable-flycheck)

flycheck 使用 pylint 来对代码进行语法和代码规范的检查,实际上会使用 executable-find 这个方法来确定使用的 pylint

(defcustom flycheck-executable-find #'executable-find
  "Function to search for executables.

The value of this option is a function which is given the name or
path of an executable and shall return the full path to the
executable, or nil if the executable does not exit.

The default is the standard `executable-find' function which
searches `exec-path'.  You can customize this option to search
for checkers in other environments such as bundle or NixOS
sandboxes."
  :group 'flycheck
  :type '(choice (const :tag "Search executables in `exec-path'" executable-find)
                 (function :tag "Search executables with a custom function"))
  :package-version '(flycheck . "0.25")
  :risky t)

而 executable-find 的工作原理是从 exec-path 这个变量里包含的的路径下寻找对应的可执行程序

(defun executable-find (command)
  "Search for COMMAND in `exec-path' and return the absolute file name.
Return nil if COMMAND is not found anywhere in `exec-path'."
  ;; Use 1 rather than file-executable-p to better match the behavior of
  ;; call-process.
  (locate-file command exec-path exec-suffixes 1))

如果只是为了支持 Python3,那么我们可以自己建立一个 Python3 的 virtualenv,然后将其路径加到 exec-path 的最前面

(push "<YOUR PYTHON3 VENV>/bin/" exec-path)

当然记得在里面安装 pylint,不然还是会用系统环境也就是 Python2 环境里的 pylint。

这种方法可以 work,但是会有不方便的地方,比如说我有时候也有可能会写 Python2 代码,遇到 Python3 已经不兼容的语法,上述方法会导致 flycheck 认为是语法错误。另外一个就是,比较良好的开发习惯,是用 virtualenv 隔离开每个项目的依赖,不同项目的同一个依赖可能会版本不一样,这样的话 flycheck 如果只使用静态的环境就没有办法应付。

当然,上一节的自动补全用的是一个统一的 virtualenv 环境,也会有类似的问题,不过要改起来会麻烦一些,所以先略过。

flycheck 的这个问题倒是好解决,既然我每个项目都会有一个独立的 virtualenv,那么能不能做到我打开对应项目的代码的时候就使用对应的 virtualenv 环境呢,比如说将对应的路径添加到 exec-path 这个列表的前面?

答案是可以的,方法是使用 auto-virtualenvwrapper,这个 package 可以根据当前的文件寻找当前目录或者上级目录中的 virtualenv 环境,然后启用。

(when (not (require 'auto-virtualenvwrapper nil :noerror))
  (message "install auto-virtualenvwrapper now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'auto-virtualenvwrapper))

然后设置一下在 python-mode 里启用它

(add-hook 'python-mode-hook #'auto-virtualenvwrapper-activate)

可以做到切换 buffer 的时候自动切换对应的 virtualenv 环境

(add-hook 'window-configuration-change-hook #'auto-virtualenvwrapper-activate)

然后我们要保证 flycheck 会在这个 virtualenv 环境里去寻找 pylint,也就是说,我们要临时修改一下 exec-path 的值

(declare-function python-shell-calculate-exec-path "python")

(defun flycheck-virtualenv-executable-find (executable)
  "Find an EXECUTABLE in the current virtualenv if any."
  (if (bound-and-true-p python-shell-virtualenv-root)
      (let ((exec-path (python-shell-calculate-exec-path)))
        (executable-find executable))
    (executable-find executable)))

(defun flycheck-virtualenv-setup ()
  "Setup Flycheck for the current virtualenv."
  (setq-local flycheck-executable-find #'flycheck-virtualenv-executable-find))

注:上述代码来自lunaryorn 的配置

PEP8 的支持

上面配置好的 flycheck 所做的语法检查和静态分析,对于不符合 PEP8 规范的语句已经会做一些提示了,不过说实话,一些东西我们可能并不想在上面化太多精力,运算符前后一个空格啦、函数之间空两行啦、类内方法之间空一行啦之类的,其实可以靠 py-autopep8 来格式化代码自动完成。

安装相应的 Emacs package

(when (not (require 'py-autopep8 nil :noerror))
  (message "install autopep8 now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'py-autopep8))

当然它其实是使用的 Python 的 autopep8 这个外部工具,所以也需要安装它

pip install autopep8

然后在 python-mode 里启用就好了,下面的配置会让 Emacs 每次在保存 Python 文件的时候自动调用 autopep8 进行格式化

(add-hook 'python-mode-hook 'py-autopep8-enable-on-save)

当然我们也可以额外设置一些参数,比如默认的一个标准是每行最大字符数为 80,如果超过了,格式化的时候会将该行折行。下面的配置可以设置为 100

(setq py-autopep8-options '("--max-line-length=100"))