迁移到 Python 3

Thu 13 July 2017

前段时间(2017-06-07)我开始决定将公司现有的项目逐渐的迁移到 Python 3. 主要原因有一下几点:

促成我决定迁移到 Python 3 的主要原因是公司最大的项目的单元测试覆盖率经过一段时间的迭代终于达到了 80% 以上.

迁移方案

由于项目巨大任务艰巨无法短时间内就将项目迁移到 Python 3, 而且当前还有产品上的功能需要迭代. 所以迁移方案是同时兼容 Python 2 和 3, 并在迁移完成之后移除对 Python 2 的支持.

搭建 CI

由于之前本身就有持续集成服务(参见上一篇), 所以目前需要做的就是在现有的基础增加一套针对 Python 3 的持续集成服务. 并且在没有完全兼容 Python 3 之前针对 Python 3 的持续集成不影响最终构建结果, 仅作为参考. 这里根据不同的 CI 需要进行不同的操作不在本文讨论范围之内就不在深入.

兼容库的选择

针对一些 Python 2 和 3 的差异我不打算自己封装, 所以找了一些成熟的第三方库, 我对比了目前比较流行的两个兼容库:

future 看起来使用比较多的黑魔法, 好多兼容都是侵入式的修改. 所以我选择了更加清晰的 six.

兼容性问题及解决

可以从 __future__ 导入如下特性来解决一些常见的不兼容:

from __future__ import print_function 
from __future__ import division   # 解决除法行为不一致
from __future__ import unicode_literals   # 字符串字面量默认为 Unicode

标准库和内建函数不兼容

Python 3 中更改了一些标准库的名字和一些函数归属的模块, 同时也移除了一些关键字如 round. 此种类型的问题可以通过 six.moves 来进行解决:

from six.moves.urllib_parse as urlparse
from fix.moves.builtins import round

字符串

以我的感受解决 Python 2 和 3 之间的兼容性字符串是难度最大的问题之一, 因为 Python 3 重新实现了字符串相关, 并增加了一些限制, 主要体现在:

  1. 移除了 unicode 关键字
  2. 原有的 str(bytes) 变成了 unicode
  3. bytes 不在支持 format/decode 等方法
  4. 不支持 Unicode 和字节序直接拼接和对比(in 操作), 一些标准库也明确指定了接收的是字节序还是 Unicode.
  5. 通过字符串格式化拼接 Unicode 和字节序会产生不符合预期的结果

这里面第 1 条和第 2 条是比较容易, 可以通过批量的查找和替换来解决. 3-4 是比较难解决, 但是单元测试基本可以覆盖到.

5 会产生一些非常难以发现的 Bug, 因为出现像下面这样的结果:

>>> '{}'.format(b'test')
"b'test'"       # Python 2: "test"

如果一些单元测试没覆盖到的或者本身逻辑就是被 mock 掉就更难排查. 比如我们内部服务通信大量的使用了签名机制, 在进行单元测试时又对这部分逻辑进行了 mock, 所以在我们在测试环境用 Python 3 启动项目后主要就是解决此类问题.

迭代器

Python 3 中除了字符串这一改动难以兼容, 还有一个就是之前返回列表的一些函数或方法改为返回迭代器, 如

  • dict.keys/dict.values/dict.items
  • map/filter/zip

如果没有对一些迭代器进行展开而是当做列表使用就会产生异常, 或者对迭代器展开没有及时进行收集就会产生一些难以排查的 Bug, 考虑如下代码:

def foo():
    data = {"a": {"n": "1"}  "b": {"n": "2"}}
    digits = data.values()

    for item in digits:
        item["n"] = int(item["n"])
    return digits

上面代码在 Python 2 中可以达到预期的行为, 返回 [{"n": 1}, {"n": 2}], 但在 Python 3 会返回一个消耗完的迭代器, 转换之后结果为 [].

下面代码在 Python2 是可以正常工作的, 但是 Python 3 下不行:

digits = {"a": "1", "b": "2"}

for k in digits.keys():
    digits[k] = int(digits[k])

主要是 Python 2 该方法返回一个列表是对字典 key 的一次拷贝, 所以在更改字典原有值就不会有问题, 但是 Python 3 中该方法返回了一个迭代器, 是对字典 key 的引用. 如果这时候更新字典就会触发异常.

工具

pylint

pylint 有一个选项 --py3k 选项可以打开检测一些不兼容 Python 3 的地方, 比如 map/filter/zip 没有展开等.

pre-commit

准确的说应该是 pre-commit 下有一些工具可以帮助我们自动修复一些 Python 3 的兼容性问题, 并且可以方便的放在持续集成服务上运行监测, 这里列出一些我们用于帮助 Python 3 迁移的工具:

最后贴上我们项目所使用的 .pre-commit-config 文件

-   repo: git@github.com:pre-commit/pre-commit-hooks.git
    sha: v0.8.0
    hooks:
    -   id: flake8
    -   id: check-docstring-first
    -   id: debug-statements
-   repo: https://github.com/asottile/reorder_python_imports
    sha: v0.3.5
    hooks:
    -   id: reorder-python-imports
        language_version: python2.7
        args:
        - --separate-relative
        - --separate-from-import
        - --add-import
        - from __future__ import absolute_import
        - --add-import
        - from __future__ import division
        - --add-import
        - from __future__ import print_function
        - --add-import
        - from __future__ import unicode_literals
        - --remove-import
        - from __future__ import with_statement
-   repo: https://github.com/asottile/pyupgrade
    sha: v1.1.1
    hooks:
    -   id: pyupgrade
-   repo: git@github.com:coldnight/pre-commit-pylint.git
    sha: 630e2662aabf3236fc62460b163d613c4bd1cfbc
    hooks:
    -   id: pylint-py3k
    -   id: pylint-score-limit
        args:
        - --limit=8.5
        - --rcfile=./.pylintrc
        additional_dependencies:
        - enum34; python_version<='3.4'
        - mock
        - coverage

后记

目前我们已经完成了公司最大的一个项目的 Python 3 兼容, 并于今天在测试环境使用 Python 3 运行.

参考

Category: Python Tagged: 2to3

comments


Python github 私有项目通过 buildbot 进行 Review

Sun 22 May 2016

背景

随着公司开发团队的壮大, 团队中每个人的水平参差不齐, 为了保证项目质量我们打算对 提交的代码进行 review, 但是苦于一直没有好的 review 机制. 前段时间我在逛 Rust 社区是发现了他们有一个 review 机器人 Homu 非常不错, 研究一下后我将其应用到我们当前 Python 项目中来配合 review, 我感觉非常棒, 今天抽空就分享给大家.

技术栈

本文涉及的项目和技术有:

0. 隔离 Github 部署 Key

Github 可以添加部署 Key 来实现部署, 但是每个项目必须是不同的部署 key. 这就给 多个私有项目的可持续集成带来一定的困难, 因为 buildbot 是通过轮询来获取 git 分支 变更的, 并且 buildbot 不支持指定私钥 …

Category: Python Tagged: Python github 私有 可持续集成 homu buildbot review

comments

Read More

Python mock 使用心得

Sun 03 April 2016

好久没有更新博客, 趁着清明节小长假和我儿子正在睡觉更新一篇刷刷存在感. 近来变化很多, 儿子也有了, 工作上也有很多收获. 这篇博客就分享一下关于 mock 的使用的心得体会.

很长一段时间以来写单元测试都类似写执行脚本, 运行一下然后看一下结果. 这里面有一部分原因是因为无法规避外部的依赖组件, 比如:

  • 数据库操作
  • 外部接口调用
  • 外部其他不可控因素

这样写测试只关心当前测试的结果, 而不去管其他测试是否 passed.

后面随着团队开始进新人, 由于团队里每个人的标准和水平不同, 开始不得不重视整体项目的质量, 发现没有好的测试就没有统一的标准来衡量提交代码的质量, 当然说到代码质量还有另外一个和测试放在一起的标准就是代码风格, 这不是本文的主题所里这里就暂且不提.

为了能写好测试就不得不面对现实项目的复杂性, 诸如外部接口数据库操作等. 这时开始将目光转向 mock, 因为之前有听过类似概念, 但是还是有误解, 以为把要测的东西都模拟掉了还测试什么呢? 但是真正的了解 mock 之后才完整的理解了单元测试.

单元测试应该只针对当前单元进行测试, 所有的外部依赖应该是稳定的, 在别处进行测试过的. 使用 mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而隐藏外部组件的实现, 使得单元测试将焦点只放在当前的逻辑(当前单元),

安装

mock 在 Python3 中是内置的, 直接 import …

Category: Python Tagged: Python mock unittest

comments

Read More

Python 内存泄露实战分析

Mon 30 March 2015

引子

之前一直盲目的认为 Python 不会存在内存泄露, 但是眼看着上线的项目随着运行时间的增长 而越来越大的内存占用, 我意识到我写的程序在发生内存泄露, 之前 debug 过 logging 模块导致的内存泄露.

目前看来, 还有别的地方引起的内存泄露. 经过一天的奋战, 终于找到了内存泄露的地方, 目前项目 跑了很长时间, 在业务量较小的时候内存还是能回到刚启动的时候的内存占用.

什么情况下不用这么麻烦

如果你的程序只是跑一下就退出大可不必大费周章的去查找是否有内存泄露, 因为 Python 在退出时 会释放它所分配的所有内存, 如果你的程序需要连续跑很长时间那么就要仔细的查找是否 产生了内存泄露.

场景

如何产生的内存泄露呢, 项目是一个 TCP server, 每当有连接过来时都会创建一个连接实例来进行 管理, 每次断开时连接实例还被占用并没有释放. 没有被释放的原因肯定是因为有某个地方对连接 实例的引用没有释放, 所以随着时间的推移, 连接创建分配内存, 连接断开并没有释放掉内存, 所以 就会产生内存泄露.

调试方法

由于不知道具体是哪里引起的内存泄露, 所以要耐心的一点点调试.

由于知道了断开连接时没有释放, 所以我就不停的模拟创建连接然后发送一些包后断开连接, 然后通过下面一行 shell 来观察内存占用情况 …

Category: Python Tagged: Python 内存 泄露 引用 回收 交叉

comments

Read More

logging 模块误用导致的内存泄露

Sat 31 January 2015

首先介绍下怎么发现的吧, 线上的项目日志是通过 logging 模块打到 syslog 里, 跑了一段时间后发现 syslog 的 UDP 连接超过了 8W, 没错是 8 W. 主要是 logging 模块用的不对

我们之前有这么一个需求, 就是针对每一个连接日志输出当前连接的信息, 所以每一个 连接就创建了一个日志实例, 并分配一个 Formatter, 创建日志实例为了区分其他连接 所以我就简单粗暴的用了当前对象的 id 来作为日志名称:

import logging


class Connection(object):
    def __init__(self):
        self._logger_name = "Connection.{}".format(id(self))
        self.logger = logging.getLogger(self._logger_name)

当然测试环境是开 DEBUG …

Category: Python Tagged: Python logging 内存泄露

comments

Read More

基于 Python 生成器的 Tornado 协程异步

Fri 19 December 2014

Tornado 4.0 已经发布了很长一段时间了, 新版本广泛的应用了协程(Future)特性. 我们目前已经将 Tornado 升级到最新版本, 而且也大量的使用协程特性.

很长时间没有更新博客, 今天就简单介绍下 Tornado 协程实现原理, Tornado 的协程是基于 Python 的生成器实现的, 所以首先来回顾下生成器.

生成器

Python 的生成器可以保存执行状态 并在下次调用的时候恢复, 通过在函数体内使用 yield 关键字 来创建一个生成器, 通过内置函数 next 或生成器的 next 方法来恢复生成器的状态.

def test():
    yield 1

我们调用 test 函数, 此时并不会返回结果, 而是会返回一个生成器

>>> test()
<generator object test at 0x100b3b320>

我们调用其 next …

Category: Python Tagged: Python generator coroutine 协程 生成器 Tornado

comments

Read More

Python 入门指南

Fri 23 May 2014

引子

经常能在 Python 群里看到很多新人在问一些非常基础的问题, 基本每天都在重复的问这些问题, 在这里就总结一下这些问题.

首先声明, 本文不打算教会你 Python, 本文力图陈列一些新手容易遇到的问题, 并企图教会你 如何学习 Python, 在遇到问题的时候如何提问.

关于版本

学习 Python 的第一步需要选择版本, Python 3.x 和 2.x 的断层较大, 3.x 不向后兼容 2.x. Python 现在主流应该还是 Python 2.7, Python 2.7 将会是 Python 2.x 的最后一个版本, 并且 会支持到 2020 年. 但是 Python 3 …

Category: Python Tagged: Python 入门 指南 新手 错误 书籍 工具

comments

Read More

PyQt 中用 QtNetwork 异步发起HTTP请求

Wed 07 May 2014

引子

最近有需求要在 PyQt 中请求一个链接, 因为比较简单直接用 urllib2 处理了, 但是 urllib2 在 有延时的时候会造成 GUI 界面卡死. 所以今天研究研究 QtNetwork 模块.

QtNetwork 中的请求在 PyQt 中都是异步的.

简单的请求 QHttp

发起一个GET请求

PyQt4.QtNetwork.QHttp 可以发起一个简单请求, 需要注意的是这个对象需要通过调用 setHost 设置请求主机, 然后 调用 get/post 传入 path 才能正常使用.

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from PyQt4 import QtGui, QtCore, QtNetwork


class …

Category: PyQt Tagged: PyQt QtNetwork QHttp QNetworkAccessManager HTTP 网络 异步

comments

Read More

Tornado 多进程实现分析

Fri 11 April 2014

引子

Tornado 是一个网络异步的的web开发框架, 并且可以利用多进程进行提高效率, 下面是创建一个多进程 tornado 程序的例子.

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

import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.process


class LongHandler(tornado.web.RequestHandler):

    def get(self):
        self.write(str(os.getpid()))
        time.sleep(10)


if __name__ …

Category: Python Tagged: Python fork_processes tornado 多进程 web 提升 效率

comments

Read More

记一次 zsh 产生僵尸进程解决

Fri 21 March 2014

问题描述

今天使用 vmplayer 运行了 xp 系统, 关闭后在 zsh 里继续敲命令就阻塞了, 然后就关闭了终端重新打开, 还是阻塞, 重复几次依然如此. 然后使用 gVim 将 shell 切换到 bash, 终端可以正常打开, 然后运行

$ ps aux | grep zsh
wh       27552  0.0  0.1  47244  5164 ?        Ss   09:38   0:00 zsh
wh       27553  0.0  0.1  47244  5156 ?        Ss   09:38   0 …

Category: Linux Tagged: zsh 进程 阻塞 命令 D 僵尸

comments

Read More
Page 1 of 10

Next »

Fork me on GitHub