更新:现在可以使用python-yapf.el更好地格式化Python代码。


前些日子下载了PyCharm试用1,想着自己也可以慢慢接受一款IDE,看到王垠的一篇关于编辑器与IDE的文章:一种语言的“IDE”最能“理解”那种语言。对于一款工具,“专而精”是好的,我应该试着消除自己对IDE的一些偏见。但有的时候,个人对于一款工具的选择或许不会考虑那么多,而是更在乎工具是否“顺手”,是否能提高个人的开发效率。同时,我也期待工具的良好的拓展性,从某些方面来说,拥有良好的拓展性更加人性化——你有可能以自己的方式来做你喜欢做的事情而不必遵守过多的限制,比如哪些功能应该有,某种操作应该是什么方式……虽然对工具进行配置、拓展的过程可能比较麻烦,但是能亲手为自己打造一款“更适合”自己的工具,其中的乐趣也只有自己才能够体会。或者换一种想法,选择IDE还是编辑器,两者并不冲突,可以都试一试。

Emacs用来做Python开发,还是特别让人舒服的。比如PyCharm中的一些特性,像 code completionsnippetscode foldingerror highlightingcode refactoringtestingversion control integrationdebugger 等,都可以在Emacs中通过配置和拓展来实现。2PyCharm有一个功能,可以通过 Code -> Reformat Code… (⌥⌘L) 来重新格式化Python文件,对代码缩进等进行调整,使其符合PEP8规范。虽然Emacs的Python Mode对代码缩进的控制已经比较好了,但是还是有一些情况需要能够对代码进行重新缩进:好多Python文件使用两个空格来缩进代码,而PEP8规范要求是四个,对于有强迫症的程序员来说,这一点让人不可忍受;有的编辑器默认不会使用空格来替代TAB进行缩进,代码被不同的编辑器修改后容易造成缩进混乱。下面的代码正是为了解决这类问题,通过将 Tim Peters 的Python Reindent 脚本集成到Emacs中,为Emacs也添加上PyCharm重新格式化代码的功能,实现对Python代码的重新缩进。

(defun revert-buffer-keep-undo (&rest -)
  "Revert buffer but keep undo history."
  (interactive)
  (let ((inhibit-read-only t))
    (clear-visited-file-modtime)
    (erase-buffer)
    (insert-file-contents (buffer-file-name))
    (set-visited-file-modtime)
    (set-buffer-modified-p nil)))

(defun revert-python-buffers ()
  "Refresh all opened buffers of python files."
  (interactive)
  (dolist (buf (buffer-list))
    (with-current-buffer buf
      (when (and (buffer-file-name)
                 (string-match "\\.\\(py\\|pyw\\)$"
                               (file-name-nondirectory (buffer-file-name)))
                 (not (buffer-modified-p)))
        (revert-buffer-keep-undo t t t))))
  (message "Refreshed opened python files."))

(defcustom python-reindent-command "reindent -v"
  "Command used to reindent a python file.

Change Python (.py) files to use 4-space indents and no hard tab characters.
Also trim excess spaces and tabs from ends of lines, and remove empty lines
at the end of files.  Also ensure the last line ends with a newline.

One or more file and/or directory paths can be passed as the arguments
of the command, reindent overwrites files in place. If backups are
required, it will rename the originals with a .bak extension.

If it finds nothing to change, the file is left alone.

If reindent does change a file, the changed file is a fixed-point for
future runs (i.e., running reindent on the resulting .py file won't
change it again)."
  :type 'string
  :group 'python)

(defun python-reindent-directory (dir &optional backup-p recurse-p)
  "Search and reindent .py files in a directory.

If BACKUP-P is set to non-nil, backup files with .bak extension will
be generated.

All .py files within the directory will be examined, and, if RECURSE-P
is set to non-nil, subdirectories will be recursively searched.

Check `python-reindent-command' for what the indentation action will do."
  (interactive
   (let ((directory-name
          (ido-read-directory-name "Reindent directory: "))
         (recurse (y-or-n-p "Search recursively for all .py files?"))
         (backup (y-or-n-p "Before reindentation, backup the files?")))
     (list directory-name backup recurse)))
  (save-some-buffers (not compilation-ask-about-save) nil)
  (shell-command (concat python-reindent-command " "
                         (if (not backup-p)
                             "--nobackup ")
                         (if recurse-p
                             "--recurse ")
                         dir))
  (revert-python-buffers)
  (message
   (concat "Reindent files done!"
           (if backup-p
               " Backup files with '.bak' extension generated."))))

(defun python-reindent-file (file &optional backup-p)
  "Reindent a file(by default the one opened in current buffer).

If BACKUP-P is set to non-nil, a backup file with .bak extension will
be generated.

Check `python-reindent-command' for what the indentation action will do."
  (interactive
   (let* ((file-name
           (ido-read-file-name
            "Reindent file: " nil
            (file-name-nondirectory (buffer-file-name))))
          (backup (y-or-n-p "Before reindentation, backup the file?")))
     (list file-name backup)))
  (save-some-buffers (not compilation-ask-about-save) nil)
  (shell-command (concat python-reindent-command " "
                         (if (not backup-p)
                             "--nobackup ")
                         file))
  (revert-python-buffers)
  (message
   (concat "Reindent file done!"
           (if backup-p
               " A .bak file has been generated."))))

(defun python-reindent-region (beg end)
  "Reindent the code of the region or the buffer if no region selected."
  (interactive
   (if (or (null transient-mark-mode)
           mark-active)
       (list (region-beginning) (region-end))
     (list (point-min) (point-max))))
  (let ((output (with-output-to-string
                  (shell-command-on-region
                   beg end
                   python-reindent-command
                   standard-output nil)))
        (error-buffer "*Shell Output Error*"))
    (if (and output
             (not (string-match "^Traceback\\|^\\w+Error:" output)))
        ;; no error
        (progn
          (goto-char beg)
          (kill-region beg end)
          (insert output)
          (message "Code has been reindented!"))
      (progn
        (set-buffer (get-buffer-create error-buffer))
        (insert output)
        (display-buffer error-buffer)
        (message "Error occurred, please check!")))))

下面是简单的使用说明:

  • 首先,使用 pip 安装Python Reindent:

    pip install -U Reindent
    
  • 然后,从这里下载gist文件,将其添加进Emacs的 load-path ,并在Emacs初始化文件中添加 (require 'python-reindent)3
  • 通过 M-x python-reindent-* RET 执行相应的命令即可对Python代码进行重新缩进。为了方便,可以根据个人喜好为 python-reindent-* 等命令设置快捷键。

需要注意的是, python-reindent-region 命令会重新缩进选中的代码区域,如果没有代码被选择,则会对整个buffer进行操作。

关于 Python Reindent 的详细说明,请参考其代码。

Footnotes:

1

JetBrains开发工具在OSChina上做三折促销。

3

或者,可以简单地直接将上面的代码添加到Emacs初始化文件中。