The content of this page is under Python.

如何向Python 3过渡?

如何向Python 3过渡?作为软件库提供者,你有三种选择:

  1. 放弃 Python 2 支持,投向 Python 3 怀抱。

  2. 在 VCS (svn, git, hg, bzr) 里保留两个分支,分别支持两个版本。其效果类似第一种选择:保留Python 2分支是为了进行 bugfix , 最终目标是等待大多数用户转而使用 Python 3 分支。

  3. 同时发布两个版本的代码,根据运行环境决定运行哪一个版本的代码。这可能比听上去简单一些。假设你在库的 src2 和 src3 分别放置 Python 2 代码和 Python 3 代码,然后在 setup.py 里面加入下面这段代码::


    import sys
    from distutils.core import setup
    if sys.version < '3':
        package_dir = {'': 'src2'}
    else:
        package_dir = {'': 'src3'}
    setup(name='dual', version='1.0', package_dir=package_dir)

这样用户安装库的时候,会得到正确的版本。

pic2x:把你的图片转换为单一PDF

Kindle 3 到手以后, DaNmarner 不管碰到什么资料都先考虑能不能放进 Kindle 3 里面浏览。尤其是在学校,笔记大部分本身就是用 Sphinx 写成,很多老师又喜欢发 pdf 讲义,所以只有少数实验资料,作业等等是印在纸上的。如果把这些纸上的资料都放进 Kindle 里,那么书包的体积和重量就最小化了!

为了完成这个目标,今天花了一个多小时写了这个小脚本: pic2x

它的主要功能是把接受到的图片列表转换成一个PDF文件,或者tex文件。

用法:

pic2x [选项] 图片列表

除了帮助用-h之外,有4个选项:

-o 文件名:制定输出的文件名。

-t 文件类型:pdf或者tex,默认为pdf

-s 如果使用,每张图片将拉伸到几乎和pdf页面一样大

-i 如果使用,pdf每页将仅包含一张照片

代码放在了github,目前依赖Python 2.4-2.6,pdflatex。创作+测试平台是 Ubuntu 10.10

现在我只要在家把资料扫描成图片,然后运行一下 pic2x ,就可以在 Kindle 3 上阅读 PDF 版本了。

Python黑魔法之__new__、__metaclass__和__init__

或许你对 Python 的 OOP 并不陌生,大致了解 Python 语法的人都知道她的 OOP 语法外观还算“典型”,比如定义一个类是这样的:

class AClass(BaseClass1, BaseClass2):
    attr1 = '属性值1'
    attr2 = '属性值2'
    # 更多属性
    def __init__(self,name ):
        ''' 这是所谓构造函数,用于初始化实例 '''
        self.name = name

    def method1(self):
        pass

    def method2(self):
        pass

类定义好了以后可以拿来用了 :::

a_instance = AClass('Sophie')
# a_instance.name == 'Sophie'

基本上什么继承啊,重载啊,多态啊这些通用 OOP 概念搬到 Python 上都基本适用。

然而大多数了解 Python 的人都听过在 Python 里的一切都是对象(命名空间),一个类的实例是对象,类本身也是对象。

你甚至听说过 Metaprogramming 这个词,知道所谓类也不是什么特殊对象——类也是由type产生的(嗯?type不是用来返回对象类型的吗?如果你有这个疑问,请继续阅读)。用type(类名,基类列表,属性字典)的方式调用type()就能获得一个新的类对象。

好吧,这些你早就知道了,不仅如此,连用type作为基类定义一个Meta Class这种见不得光的活儿你也干过。那你一定知道 __metaclass__ 是干什么的了?恩没错,就是这样 :::

class MetaClass(type):
    def __init__(cls, name, bases, dict):
        if not hasattr(cls, 'precious'):
            cls.precious = []
        else:
            cls.precious.append(cls)

class OfTheDarkLord:
    __metaclass__ = MetaClass

class Ring(OfTheDarkLord):
    number = 9

class King(OfTheDarkLord):
    has_soul = False

这样 OfTheDarkLord 的所有子类都储存在了 OfTheDarkLord.precious 这个列表里面。而 OfTheDarkLord 在创建之初对将会有哪些子类完全没有概念。

如果你看到这里还面不改色心不跳的话,给自己点掌声吧,DaNmarner 认为你的 Python 等级已经到达可以冲副本的水平了(比我 DaNmarner 高级)。简单解释一下,用 class 定义的类通常是由 type 来建立的,但是如果类定义了__metaclass__属性,那么这个类将由这个属性值指向的东西来创建,而不是默认的type。

我最初学习这个技术的时候为一件事情困惑了很久很久:在读过的代码里面,还有一个经常出现的__new__方法,貌似也和Metaprogramming大有关系。去看 Python 的官方文档,里面也是说,__new__是用来创建类的新实例用的。既然type创建的“实例”实际上是类,那如果我在前面的MetaClass定义一个__new__方法,是否能实现原来__init__的效果呢?

答案是不行。什么,你知道的?那您不用再看了,这是小白文章一篇。

继续说下去,希望这段阴暗潮湿的 Python 探险没有毁掉她在你心里简约大方的形象。下面这段基本上要去 C 代码里找了(去看PyPy的实现吧!)。

一个用 class 关键字定义的类在执行a = ClassA(arg1, arg2,...)(初始化其实例)的时候,会发生 :::

a = type(ClassA).__call__(ClassA, arg1, arg2,...)

type(ClassA)实际上返回的是ClassA的metaclass,默认值为type。这个type.__call__的实现可以用如下代码表示(只能表示表示,实际代码都是用C实现的)。

def __call__(cls, *args, **kwargs):
    result = cls.__new__(cls, *args, **kwargs)
    if isinstance(result, cls):
        type(result).__init__(result,*args,**kwargs)
    return result

也就是说type.__call__先尝试用__new__来创建类的实例,只有__new__返回了类的实例,__init__才会得以应用到实例上。

回到 MetaClass 的实现上,__init__得到调用的时候,OfTheDarkLord经由 Metaclass 用默认的__new__ 生成了,这时__init__给他安方一个precious属性。等到OfTheDarkLord 子类初始化的时候,__init__ (还是由默认 __new__ 生成的)类定义里通过继承链找到了 OfTheDarkLord 的 precious 属性,所以这次子类不会再创建 precious 属性,而是把自己加入到 OfTheDarkLord 的 precious 列表里。一切都是在 __new__ 运行完之后完成的,在那之前,MetaClass 还没法判断 precious 是否已经存在于继承链中,所以相同的代码会给每一个 OfTheDarkLord 的子类都安插一个空白的 precious 属性,达不成预期的效果。

Python项目版本规范

Python 作为一门编程语言对用它开发的项目的版本格式没有任何限制。实际上大多数的 Python 代码根本没有版本这个属性。在 PEP345 通过之前,项目版本的格式几乎是无关紧要的。然而这个 PEP345 给 Disutils 模块增加了一个 Requrie-Dist 属性,试图通过它和其他增改的属性来解决不同 Python 项目之间的依赖关系难以表述的问题。有了它,将来的项目可以通过原生的 Python 环境来提供自己的安装和卸载信息,包括对其他项目的依赖。这样 Python 就获得了类似 Debian Linux 下面 apt-get 工具的能力!

这个 PEP 的通过对 Python 项目的使用者和开发者都是一个好消息。

那么这个 PEP 和版本号有什么关系呢?答案是新增的 Require-Dist 属性里可以包含版本范围的信息(x项目以来y项目的版本不小于z)。而这个信息自然会应用到版本大小的比较。如果一个项目的版本格式是随机的,那 Python 项目的安装工具就无法识别 Require-Dist 里的版本范围,从而无法通过比较版本大小来安装项目的依赖包。所以作为一个 Python 开发者,如果想让自己的项目能在将来顺利安装,最好还是从今天开始使用规范的版本系统和格式。

怎样的版本格式才算规范呢?Python 核心已经通过 PEP386 为我们回答了这个问题。Python 在将来能原生识别和比较的版本应该具有以下格式::

N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]

从左向右做一个简单的解释:

  1. "N.N": 版本号里唯一必备的部分,两个"N"分别代表了主版本和副版本号,绝大多数现存的工程里都包含该部分。
  2. "[.N]": 次要版本号,可以有零或多个。
  3. "{a|b|c|rc}": 阶段代号,a, b, c, rc分别代表alpha, beta, candidate 和 release candidate, 可选
  4. "N[.N]": 阶段版本号,如果提供,则至少有一位主版本号,后面可以加无限多位的副版本号。
  5. ".postN": 发行后更新版本号,可选。
  6. ".devN": 开发期间的发行版本号,可选。

不难看出这个格式其实是建立在大多数既存项目已经普遍应用的格式的基础之上的,这也是这个PEP的提出者的初衷:尽量保持与现状的兼容性。

下面举几个规范版本格式的例子:

  • '1.2'
  • '3.2.3.2c5.4.12' (3.2.3.2 的 candidate 阶段的 5.4.12 版本)
  • '2.7.dev231' (2.7开发版231次发行)
  • '1.0.1rc2.post2' (1.0.1 的 release candidate 第二发行版本)

符合规范的版本号即使格式不同也能相互进行比较,PEP386 里面给的例子能说明很多问题:

>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
...  < V('1.0a2.dev456')
...  < V('1.0a2')
...  < V('1.0a2.1.dev456')
...  < V('1.0a2.1')
...  < V('1.0b1.dev456')
...  < V('1.0b2')
...  < V('1.0b2.post345')
...  < V('1.0c1.dev456')
...  < V('1.0c1')
...  < V('1.0.dev456')
...  < V('1.0')
...  < V('1.0.post456.dev34')
...  < V('1.0.post456'))
True

结果非常的符合直觉。至少 DaNmarner 是这么觉得。

最后补充: 上面例子中的 verlib 还在开发阶段,代码在这里,它将成为未来的 distutils.version 模块。 PEP 386 是 Python 和 Ubuntu,Fedora 一起提出的。

你的 Python 项目有一个符合 PEP386 的版本吗?