标签 python 下的文章

Python Socket 编程

Socket (套接字)是进程通信的一种方式,主要应用于不同主机的进程之间的数据交换。

Unix/Linux 的基本哲学之一是:一切皆文件。遵循以下模式:

open -> write/read -> close

而 Socket 就是上述模式的一种实现方式。

一、如何标识进程

用一个三元组标识进程:(IP地址, 协议, 端口)。我们称之为半相关,它指定一个连接的半个部分。

一个完整的网间通信由两个进程组成,并且只能使用同一种高层协议,称之为全相关:(协议, 本地IP地址, 本地端口, 远程IP地址, 远程端口)。

操作系统的端口号,是一个 16 位的编号(2^16 = 65536),范围是 0~65535。一个进程在通讯时均会占用一个端口号。

1~255 知名端口、默认端口,如 80 是 Web 服务的端口,21 是 FTP 服务的端口。
256~1023 Unix 系统占用的端口,普通用户无权使用。
1024~5000 临时端口,一般客户端随机分配的端口在此范围。

协议包括地址族(Address Family)和套接字类型。

Address Family:

AF_UNIX / AF_LOCAL  本地进程间通信,以绝对路径作为地址
AF_INET / AF_INET6  网络间进程通信,用 ipv4/6 和端口号组合成地址
AF_NS
AF_ROUTE

套接字类型:

SOCK_STREAM  TCP流式套接字,面向连接,提供可靠的数据传输服务。
SOCK_DGRAM  UDP,数据报式套接字,无连接的,数据包以独立形式发送,可能丢失或重复,且不按顺序。
SOCK_RAW  原始套接字,允许使用较低层次协议,如 ICMP/IGMP 等。
SOCK_PACKET
SOCK_SEQPACKET  可靠的连续数据包服务。

二、相关函数

以下是 C 语言相关函数:

socket() 创建一个 socket
bind() 绑定本地地址
listen() 监听连接
accept() 接受连接,返回收到的套接字
send() 发送数据
recv() 接收数据
select() 输入输出多路复用
closesocket() 关闭套接字
connect() 与远程进程建立连接,它会自动给本地 socket 分配端口号

三、Python Socket 编程

Python 提供了两个基本的模块:

socket 提供了标准的 BSD Sockets API。
socketserver 提供了服务器相关方法,如开发网络服务器。

创建 socket 函数:

socket(family,type[,protocal]) # 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。

创建 TCP socket:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

创建 UDP socket:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

1. 服务端 socket 函数

s.bind(address)
将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址。

s.listen(backlog)
开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,一般设为 5。

s.accept()
接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。如果没有收到连接,则会挂起,一直等待下去。

2. 客户端 socket 函数

s.connect(address)
连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。hostname 可以是域名或者IP地址。

s.connect_ex(adddress)
功能与connect(address)相同,但是成功返回0,失败返回errno的值。

3. 公共 socket 函数

s.recv(bufsize[,flag])
接收TCP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。

s.send(string[,flag])
发送TCP数据。将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。

s.sendall(string[,flag])
完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

s.recvfrom(bufsize[,flag])
接收UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

s.sendto(string[,flag],address)
发送UDP数据。将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。

s.close()
关闭套接字。

s.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

s.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)

s.setsockopt(level,optname,value)
设置给定套接字选项的值。

s.getsockopt(level,optname[,buflen])
返回套接字选项的值。

s.settimeout(timeout)
设置套接字操作的超时时间,timeout 是一个浮点数,单位是秒。值为None表示没有超时时间。一般,超时时间应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())

s.gettimeout()
返回当前超时时间的值,单位是秒,如果没有设置,则返回None。

s.fileno()
返回套接字的文件描述符。

s.setblocking(flag)
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。

s.makefile()
创建一个与该套接字相关连的文件。

说明:
TCP 发送数据时,已建立好连接,所以不需要指定地址。
UDP 是无连接的,因此每次发送数据都要指定地址。

参考文档:Python Socket 网络编程

如何选择面向对象编程与面向过程编程?

可能我们脑海里天生就有面向过程编程的思想,大致就是把一件事情分成几步,一步一步地完成。不假思索写出来的代码,自然就是面向过程的。

对于面向过程编程,变量(数据)与函数(行为)是分开的,它们都被安排在某个模块(文件)里。模块就好像是一个对象(类),因此,面向过程编程就好比在一个默认的类中编程,而这个类往往不被注意到。

对象同时包含数据和行为。如果只考虑数据,使用列表、字典等 Python 数据结构即可。如果只关注行为而不存储任何数据,那一个简单的函数更合适。

听起来有点枯燥,我们通过例子来理解。

计算多边形的周长

面向过程

通常来说,我们会以一个存储着各个顶点的列表来代表多边形。每个顶点以一个二维元组(x, y)建模,该元组描述顶点的位置坐标。

square = [(1, 1), (1, 2), (2, 2), (2, 1)]

计算周长,我们只需要计算相邻两个顶点之间的距离,然后加起来。因此需要一个计算两点之间的距离的函数。编码如下:

import math

def distance(p1, p2):
    return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)

def perimeter(polygon):
    perimeter = 0
    points = polygon + [polygon[0]]
    for i in range(len(polygon)):
        perimeter += distance(points[i], points[i+1])
    return perimeter

这是面向过程的解决办法,调用方法如下:

square = [(1, 1), (1, 2), (2, 2), (2, 1)]
perimeter(square)

4.0

面向对象

上面的函数已经很简洁明了,但不妨来看看面向对象的版本是怎样的。

import math

class Point(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def distance(self, p2):
        return math.sqrt((self.x - p2.x) ** 2 + (self.y - p2.y) ** 2)

class Polygon(object):

    def __init__(self):
        self.vertices = []

    def add_point(self, point):
        self.vertices.append(point)

    def perimeter(self):
        perimeter = 0
        points = self.vertices + [self.vertices[0]]
        for i in range(len(self.vertices)):
            perimeter += points[i].distance(points[i+1])
        return perimeter

这段代码比上面的面向过程版本长了很多,几乎是上面的两倍。我们先来看看它是如何调用的。

square = Polygon()
square.add_point(Point(1, 1))
square.add_point(Point(1, 2))
square.add_point(Point(2, 2))
square.add_point(Point(2, 1))
square.perimeter()

4.0

是不是通俗易懂,它比函数版本的代码更易读。对于函数版本的代码,我们可能需要写更多的文档以表明它的用法。

我们还可以对 Polygon 类进行优化,使它更容易使用,让它像函数版本那样接受一个元组的列表。

def __init__(self, points=[]):
    self.vertices = []
    for point in points:
        if isinstance(point, tuple):
            point = Point(*point)
        self.vertices.append(point)

如果我们要为多边形增加一些特征,如 color 或者 texture,将这些数据封装进类会变得更有意义。一般来讲,拥有复杂的数据集,需要对它进行特定操作,这时使用具有属性和方法的类会很有帮助。它不仅可以使代码易读,还可以使逻辑紧凑。

如何选择

在一个具体的场景里,如果只需要计算多边形的周长,一个函数可能是最简单、最快的开发方法。但是如果我们需要对多边形进行多种处理(计算周长、面积、和其它多边形的交点等),那么我们需要一个对象来满足这些需求。所以,对于较复杂的问题,一般采用面向对象编程。

Python 编码规范

一、文件编码

所有的 Python 脚本文件都应在文件头部指定如下标识或其兼容格式的标识:

# -*- coding: utf-8 -*-

二、注释

注释分为两类,以 # 开头的“真正的”注释,和 docstring。前者一般用于说明实现的原理及难点,后者用于说明使用方法、示例以及单元测试。

坚持适当注释原则。对不存在技术难点的代码坚持不注释,对存在技术难点的代码必须注释。

三、格式

注释

以 # 开头的注释,在 # 符号后加空格,再写注释内容。对于行内注释,在语句后空出至少2格,再加注释部分。

缩进

Python 严格依赖缩进来确定代码块的层次。可以使用空格或者 Tab 来缩进代码。一般地,一个缩进层次采用 4 个空格。

空格

代码中适当加入空格可增强代码的可读性。

(1) 在二元算术、逻辑运算符、赋值运算符前后加空格:

a = b + c

算术运算符左右一般要加空格;但复杂表达式中,优先级高一些的部分,算术运算符左右不加空格:

x = x*2 - 1  # 乘号的优先级高
c = (a+b) * (a-b)  # 括号部分的优先级高一些

(2) 冒号(:)用在行尾时前后皆不加空格,如分支、循环、函数和类定义语言;用在非行尾时右边加空格,如 dict 对象的定义:

d = {'key': 'value'}

(3) 括号(含圆括号、方括号和花括号)前后不加空格,作为分隔符的逗号后加空格:

do_something(arg1, arg2)

函数定义及调用时的形参赋值,等号左右不加空格:

def complex(real, imag=0.0):
    return magic(r=real, i=imag)

(4) 逗号后面加一个空格,前面不加空格。

空行

适当的空行有利于增加代码的可读性,加空行可以参考如下几个准则:

  • 在类、函数的定义间加空行
  • 在 import 不同种类的模块间加空行
  • 在函数中的逻辑段落间加空行,即把相关的代码紧凑写在一起,作为一个逻辑段落,段落间以空行分隔
断行

一般地,行的最大长度不超过 78 个字符。可以使用括号(包括圆括号、方括号、花括号)和反斜线来断行,在断行后增加一个缩进。

四、命名

常量

常量名所有字母大写,由下划线连接各个单词。

APP_REQUEST_TIMEOUT = 600
变量
  • 变量名全部小写,由下划线连接各个单词
  • 私有类成员使用单一下划线前缀标识
  • 变量名不应带有类型信息,因为 Python 是动态类型语言。如 iValue、names_list、dict_obj 等都不是好的命名
函数

函数名的命名规则与变量名相同。

类名采用 Pascal 命名法。单词首字母大写,不使用下划线连接单词,也不加入 C、T 等前缀。

模块

模块名全部小写,对于包内使用的模块,可以加一个下划线前缀。注意不要与内置的模块名重复,否则会导致内置模块不可用。

包的命名规范与模块相同。

五、语句

import

import 的次序,先 import Python 内置模块,再 import 第三方模块,最后 import 自己开发的项目中的其它模块;用空行分隔这几类模块。

一条 import 语句只导入一个模块。

当从模块中 import 多个对象且超过一行时,使用如下断行法(此语法 py2.5 以上版本才支持):

from module import (obj1, obj2, obj3, obj4, 
    obj5, obj6)

不要使用 from module import *,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。

赋值

不要做无谓的对齐:

a        = 1                  # 这是一个行注释
variable = 2                  # 另一个行注释
fn       = callback_function  # 还是行注释

没有必要做这种对齐,原因有两点:一是这种对齐会打乱编程时的注意力,大脑要同时处理两件事(编程和对齐);二是以后阅读和维护都很困难,因为人眼的横向视野很窄,把三个字段看成一行很困难,而且维护时要增加一个更长的变量名也会破坏对齐。直接这样写为佳:

a = 1 # 这是一个行注释  
variable = 2 # 另一个行注释  
fn = callback_function # 还是行注释
分支和循环

(1) 不要写成一行,如:

if not flg: pass 

应该这样:

if not flg:  
    pass  

(2) 条件表达式的编写应该足够 pythonic,如以下形式的条件表达式是拙劣的:

if len(alist) != 0: do_something()
if s != "": do_something()
if var != False: do_something()

应该这样:

if seq: do_something() # 注意,这里命名也更改了
if var: do_something()

注:本文主要参考赖勇浩的《简明Python编程规范v2》。

Python 字典的合并效率

在 Python 编程中,字典的使用频率非常高,合并操作也很常见。

x = {'name':'Eric', 'age':28}
y = {'age':29, 'hobby':['sport', 'travel']}

例如要合并这两个字典:更新其中某项,并添加新项。你能想到什么办法?

  1. update 方法

    z = x.copy()
    z.update(y)

  2. 使用 dict() 函数的字典参数

    z = dict(x, **y)

  3. items 变换

    z = dict(x.items() + y.items())

  4. lambda 表达式

    z = (lambda a, b: (lambda a_copy: a_copy.update(b) or a_copy)(a.copy()))(x, y)

这些方法都可以达到要求,哪种效率最高呢?

我们可以使用 timeit 模块来测试它们的运行效率。

import timeit

# 1 update 方法
timeit.timeit("z = x.copy(); z.update(y)", "x = {'name':'Eric', 'age':28}; y = {'age':29, 'hobby':['sport', 'travel']}")
# output: 0.31891608238220215

# 2 使用 dict() 函数的字典参数
timeit.timeit("z = dict(x, **y)", "x = {'name':'Eric', 'age':28}; y = {'age':29, 'hobby':['sport', 'travel']}")
# output: 0.30063605308532715

# 3 items 变换
timeit.timeit("z = dict(x.items() + y.items())", "x = {'name':'Eric', 'age':28}; y = {'age':29, 'hobby':['sport', 'travel']}")
# output: 0.8453028202056885

# 4 lambda 表达式
timeit.timeit("(lambda a, b: (lambda a_copy: a_copy.update(b) or a_copy)(a.copy()))(x, y)", "x = {'name':'Eric', 'age':28}; y = {'age':29, 'hobby':['sport', 'travel']}")
# output: 0.6631548404693604

结果显示,方法1和2效率最高,且两者运行时间差不多。其次是方法4,最差的是方法3。(对运行时间的计算,多次运行取平均值会更准确一点。)

总结下来,运行效率用公式表示如下:
1 = 2 > 4 > 3

Python 模块的工作原理[译]

作者: Tomasz Lisowski 翻译: Eric Ling

译注

这是一篇老文,作者至少写于 5 年前,但它的内容仍然是“新鲜”的。可以通过阅读本文,深入理解 Python 模块的工作原理。

1. 前言

当我们写更大的 Python 程序时,变量名、函数名、类名的数量增长如此巨大,因此很有必要将他们组织成类别或小组,这通常被称为命名空间。下面的语言结构提供了这种便利:

  • classes (类)
  • modules (模块)
  • packages (包)

本文中我们不打算讨论"类",因为它值得单独讨论。然而,我们将试着阐述 Python 模块和包的功能,并特别关注模块的导入机制。我希望这能帮助 Python 编程人员更好地理解那些有用的包,比如 wxPython 或者 NumPy。

这篇文章受启发于船舶设计软件 Tribon M3 用户的提问。这个软件是 AVEVA Solutions Ltd 公司开发的,它内嵌了一个包含超过 600 个专门函数的 Python 2.3 解释器,作为自定义工具。当然,这里讨论的功能,通常是 Python 语言的特性,不但对 Python 2.3 有效,而且对新的发行版 2.4 和 2.5 也有效。

2. 模块

2.1 模块是什么

简单来讲,模块就是变量、函数以及类的定义的集合。他们也可包含直接执行的代码,我们将在后面讨论这个特性。通常,我们可以区分出 3 种类型的模块:

  • Built-in modules (内建模块)

他们总是可访问的,因为他们已经创建到 Python 解释器里了。例如:sys, math, time。

内建模块的名字被保存在一个名叫 sys.builtin_module_names 的列表中。下面的代码将打印出当前运行的 Python 解释器中所有的内建模块的名字。

import sys
for name in sys.builtin_module_names:
    print name
  • 用 Python 编写的标准模块

这里我们需要模块的源代码(它们是用 Python 写的),或者至少一个特殊的二进制版本文件(以 .pyc 结尾),它包含了模块预编译的字节码。为了在程序中可用,这个文件必须位于定义在 sys.path 中的某个文件夹。例如:os, re, csv, types。

  • 用其它语言(例如 C)编写的模块,而且已经编译成一个 DLL

真正编译语言的使用可以提高只有 Python 的解决方案的性能,而且可以访问更低层次的 API。为了让 DLL 成为 Python 的一个可用模块,代码必须遵循一定的规范,手册 Extending and Embedding the Python Interpreter 详细地列出了这些规范。这个手册是 Python 语言的作者 Guido van Rossum 写的。

习惯上,编译的 DLL 文件以 .pyd 结尾,而不是 .dll。跟标准模块一样,它也必须位于定义在 sys.path 中的某个文件夹。例如:_tkinter, _sre。

2.2 怎么使用模块

想使用模块中定义的类和方法,必须先导入模块。这可以通过 import 语句来完成,使用以下任意一种语法:

  • import moduleName
  • import moduleName as alternateName
  • from moduleName import *
  • from moduleName import id1, id2, …
  • from moduleName import id1 as altName1, id2 as altName2, …

通常,模块导入的流程如下:

  1. 首先,Python 解释器在字典 sys.modules 中寻找这个模块名,这个字典中包含了解释器当前已经加载和初始化的所有模块。如果找到了这个模块,系统将跳过第 2 步,直接前往第 3 步。
  2. 如果模块还没有加载,系统首先搜寻内建模块列表,如果也没有找到,则继续到 sys.path 所包含的目录中寻找。如果找到,则加载它并初始化。最后,如果加载和初始化都没有出错,那么这个模块将被注册到 sys.modules 字典中。如果没有找到,将抛出 ImportError 异常。
    注意:如果一个模块已经存在于 sys.modules 字典中,随后的所有导入这个模块的 import 语句,不会再初始化这个模块。
  3. 到这一步,Python 解释器将怎样运行取决于刚刚使用的 import 语句的形式。
A. import moduleName

模块的名字被注入到本地命名空间(the local namespace, 可理解为当前的 Python 运行环境)。模块中所有定义的标识符,需要通过模块名字修饰的前缀形式来访问。(参见2.7)

例如:

import myModule
res = myModule.doIt(arg)
B. import moduleName as alternateName

模块的别名被注入到本地命名空间。模块中定义的所有标识符,需要通过模块别名修饰的前缀形式来访问。(参见2.7)

例如:

import sys
# Define the module 'utils' by importing either
# winUtils or linuxUtils depending on the platform
if sys.platform == 'win32':
    import winUtils as utils
else:
    import linuxUtils as utils
...
# platform-independent module access
user = utils.getUser()
C. from moduleName import *

这种情况,模块的名字没有被注入到本地命名空间,但是系统会搜索这个已经加载的模块的名字空间,并把模块中所有的公共标识符的名字注入到本地命名空间。

Python 是如何确定哪些标识符是公共的,哪些不是呢?你将在2.3小节找到答案。

D. from moduleName import id1, id2,… or from moduleName import id1 as altName1, id2 as altName1, …

这种情况下,模块的名字也没有被注入到本地命名空间,但是系统会搜索这个已经加载的模块的名字空间,把列出来的标识符的名字(或者别名)注入到本地命名空间。

当然,如果列出来的名字中有不存在的,将抛出一个 ImportError 异常。

注意:在使用 C 或者 D 类型的语法时,存在对已有标识符重新绑定值的风险(译注:模块中的标识符覆盖了本地命名空间中相同的标识符),导致无法访问原标识符的值。

例如:

doIt = True
...
from myModule import *
...
print "doIt =", doIt

上面的代码简单地认为,变量 doIt 仍然是第 1 行定义的布尔值。不过,如果模块 myModule 中定义了一个相同的公共变量,我们就有麻烦了。此时,我们的布尔变量被模块中导入的同名标识符重定义(或者叫覆盖)了。当我们再试着打印出原来的值,会发现变量 doIt 已经不是布尔值,而是一个函数或者字符串,因为我们已经失去了对原值的访问能力(它很可能已经被垃圾回收机制回收了)。

当然,如果我们使用另外一种导入语法,就不会发生这种情况。

doIt = True
...
import myModule
...
print "doIt =", doIt

这里的变量 doIt 仍然是第 1 行定义的布尔值。doItmyModule.doIt 是两个截然不同的对象。

当然,在模块中采用好的命名规范,同样能降低这种名字冲突带来的风险。

2.3 公共标识符

在前面的章节,我们提到过如下语句:

from myModule import *

它将从模块中导入所有的公共标识符。那么 Python 怎么辨别出哪些是公共的标识符,哪些不是呢?

首先,Python 会检查导入的模块中是否已经定义了全局变量 __all__ 。如果定义了,那么这个变量是一个以字符串为基本元素的序列,这些字符串所代表的变量,被认为是公共的标识符。这个特性可以阻止某些变量的导入,尤其是那些私有性质的标识符。

设想有一个大型的模块,包含了许多函数,但是只有一个主函数被其它的模块调用。而其它的函数仅作为辅助函数,只在这个大型模块内部被调用。这种情况下非常适合使用 __all__ 变量。例如:

模块:myModule.py

def fun1():   # Private function
    ... # Code
...
def main():   # Main (public) function
    ... # Code
__all__ = ['main']

现在我们尝试执行下面的代码:

from myModule import *
res = main()

没有任何问题。我们再来执行这个代码:

res = fun1()

呃,抛出了 NameError 异常。这是怎么回事呢?

尽管函数 fun1main 都存在于被导入的模块,但只有其中一个可以被访问。抛出这个异常是因为 'fun1' 没有出现在变量 __all__ 所定义的列表中。这就导致 import 语句忽略了这个函数。

如果被导入的模块中不存在 __all__ 变量,所有的变量都被认为是公共的,除了以下划线(_)开头的标识符。

译注:如果使用 from import 方法指定某个标识符,即使它不在__all__变量中,甚至以下划线开头的标识符,也会被导入到本地命名空间。

2.4 模块初始化

Python 初始化一个模块,要么执行一个专门的初始化函数(对于非 Python 语言编写的模块),要么运行模块的主体(对于 Python 语言编写的模块)。对于后者,会解析模块中定义的内容,并立即执行可执行的代码。

正如2.2节中所讲的,模块只会被初始化一次,而且是模块不存在于 sys.modules 字典中时。这种行为,对于包含直接可执行代码的模块产生了重大的影响。这些代码只会在模块初始化的时候执行,而且只是一次,无论模块被当前的运行环境导入(import)了多少次。

因此,把直接可执行的代码放在模块中(并且模块在程序中被导入多次),这不是一个好的设计习惯。我们不能简单地认为在每次导入模块时,这些代码都会运行一次。相反,我们应该把这些代码放到一个函数中(如 run()),并且使用下面的方式:

import myModule
myModule.run()

明确地调用函数 run()

然而在某些情况下,我们需要写这样一个模块,有时候被导入,有时候作为主程序直接执行。这对于模块开发时尤其有用,我们可运行模块来做单元测试。

遵循前面的建议,我们应该将测试代码(也就是所讲的可直接执行的代码)隐藏到一个函数中(如 runTest())。在模块直接运行时,这个函数被执行;被导入时则不执行。为了达到这个目的,我们需要一种方法来有条件地执行这个函数,能检测出模块是直接运行的还是被导入的。我们可以通过在模块底部增加下面的代码来达到这个目的:

if __name__ == '__main__':
    runTest()

如果模块被导入,则特殊的字符串变量 __name__ 被赋值为模块的名字;如果模块直接运行,则为 __main__ 。因此,上面的 if 语句只在模块直接运行时,才执行 runTest() 函数,被导入时不会执行。

为了越过模块只被初始化一次的限制,Python 语言提供了 reload() 函数。

reload(module_name)

这会使模块再次初始化,并且返回这个模块对象作为函数结果。

2.5 .py, .pyc, and .pyo 后缀文件

如果一个模块是用 Python 写的,它的源代码被存储在以 .py 结尾的文件中(例如 myModule.py 就是模块 myModule 的源代码文件)。

模块无论何时被成功编译,都会尝试把编译的内容写到以 .pyc 结尾的同名文件中(例如 myModule.pyc)(译注:通常它被称为字节码文件,和其它的编译语言有所不同)。如果编译失败,也不会是一个错误;比如由于某种原因,文件是不完整的,则结果文件 myModule.pyc 被认为是无效的,并且会忽略掉。

字节码文件(.pyc)的内容是平台独立的,一个 Python 模块的目录可以被不同架构的机器共享。

如果 Python 解释器调用时使用了 -O 参数(或者 -OO),Python 将会优化编译后的字节码文件。不再生成 .pyc 文件,而是产生一个以 .pyo 结尾的字节码文件。它和 .pyc 文件使用上是一样的,不同点在于 .pyc 文件是未经优化的。而 .pyo 文件是 Python 解释器在优化模式下工作的。

编译后的字节码文件(.pyc 或者 .pyo 文件)中存储了对应的模块文件(.py 文件)的修改时间。在以下情况,Python 会加载编译后的字节码文件(不会重新编译模块):

  1. 如果模块文件(.py)不存在。
  2. 如果模块文件存在,并且它的修改时间和字节码文件中保存的时间一致。

否则,Python 会忽略字节码文件,重新编译源代码文件。这是自动进行的,你不用考虑去更新你的字节码文件,Python 会替你完成这些。

有时在开发 Python 程序时,你可能已经注意到,Python 好像没看到你对模块的修改,除非你退出并重启开发环境。这可以通过下面几步来解释:

  1. 第一次运行程序时(例如对某部分进行测试),Python 会根据 import 指向的模块解析这个模块。因为它之前没有被导入(在当前 Python 解释器会话中),它没有被列入 sys.modules 字典中。因此,Python 加载并解析它,创建或者更新字节码文件(.pyc),并且把编译后的模块对象存储到 sys.modules 字典中。
  2. 然后你对模块源代码进行了修改,保存文件,然后再次运行测试代码。噢,有点不对劲!测试代码运行得跟之前一样,就好像你没有对模块进行修改一样。
  3. 这是因为之前,Python 根据 import 指向的模块,对模块源代码进行了解析。然而与第1步相反的是,现在我们的模块对象已经存在 sys.modules 字典中。因此 Python 不会再尝试加载和解析模块——它只会从 sys.modules 字典中读取模块对象。所以它不是模块的最新版本,它不包含第2步所做的修改。

幸运的是,大多数开发环境提供了一些工具,来重新加载已经加载的模块。(例如一些 '重新加载模块' 的按钮。)如果没有,你总是可以使用临时的语句 reload(moduleName) ,在 import 模块之后,重新加载模块。

2.6 sys.path 变量

变量 sys.path 定义了一个文件列表,将在这些文件夹中搜寻要导入的模块。它是在解释器初始化时创建的,也可以在运行时对它进行更改。重要的是,我们需要知道这个列表是如何创建的,以及怎样将新的路径添加到这个列表,使得我们的脚本能够找到它们的模块。

首先,这个变量通过视窗环境变量(the Windows environment variable) PYTHONPATH 来初始化。Python 解释器和其它使用 Python 解释器的软件系统(例如 Tribon M3),可以定义和更新这个变量,设置它为一个用分号分隔的路径列表。

然后一个特殊的 site 模块被导入。注意,这里不需要显式地导入 import site —— Python 会为你完成这一步。这是一个标准的 Python 模块,用户可以通过它来自定义环境,例如添加新文件夹到 sys.path 列表。默认地,(如果你安装了一个独立的 Python 解释器),它会添加一些标准的文件夹,比如'/Python23/lib/site-packages'。

此外,site 模块会从 sys.path 列表中的文件夹中搜寻 *.pth 文件(路径配置文件,path configuration files)。如果找到了,就读取这些文件,并且将定义在这些文件中的路径自动地添加到 sys.path 列表。这些文件被用于某些 Python 包,例如 wxPython,它定义了 wxPython 包中的模块的位置。

更多的系统自定义可以放到一个可选的 sitecustomize 模块,这是 site 模块尝试导入的模块。这样我们就可以保持 site 模块不变,将所有的自定义内容写入 sitecustomise 模块。

最后,模块搜寻路径(sys.path)也可以在运行时自定义。例如:

path = 'E:\\PRIVATE\\MODULES'
import sys
if path not in sys.path:
    sys.path.append(path)
import my_test_module

这里,模块文件 my_test_module.py 位于文件夹 E:PRIVATEMODULES 中。

在上面的例子中,用户定义的文件夹放在 sys.path 列表的末尾。如果你想把它放在列表的开关,可以通过替换以下语句来完成:

sys.path.append(path)

替换为

sys.path.insert(0, path)

为什么这个很重要?我们假定 my_test_module 模块除了在文件夹 E:PRIVATEMODULES 中,还在 sys.path 列表中的其它文件中也存在。它可能是同样的模块,或者老版本的模块,甚至一个根本不相关的模块,只是碰巧它俩拥有同样的名字。无论什么原因导致了同名模块,处理的规则很简单:

按照 sys.path 列表定义的文件夹顺序,以先找到的文件为准。

因此,你可能导入了一个不同的模块,它不是你希望导入的,同时没有产生任何警告。

当然,对模块使用良好的命名规范,可以减少这种产生歧义的风险。

2.7 访问被导入的标识符

正如 2.2 章节中所讨论的,标识符的访问方式,取决于模块的导入方式。也取决于被导入的标识符所在的命名空间。

我们来分析下面两种导入语句:

  1. import math -> math.sin(…)
  2. from math import sin -> sin(…)

第一种情况,模块名字本身被注册到当前的命名空间,但是函数名 sin 是在模块的命名空间。因此,这里我们需要用限定的名字来访问 math.sin。

第二种情况,标识符 sin 本身被注册到当前的命名空间,因此可以直接使用这个名字。这里不能使用限定的名字 math.sin 来访问这个函数,因为模块的名字(math)在当前的命名空间中不存在。

总的来说,from 版本的 import 语句让我们可以使用简短的标识符,可能也提高了程序性能,但是导入的许多新的标识符会使当前的命名空间很乱。使用下面的语句,发生这种负面影响更严重:

from moduleName import *

这真的会使许多标识符被导入到当前的命名空间。

2.8 命名空间和名字解析(the name resolution)

为了理解 Python 是如何解析(限定的或者没有限定的)标识符名字到变量、函数等的内存地址,我们需要知道它是如何搜索可用的命名空间。通常有三个命名空间,会按照下面的顺序依次搜索:

  1. 局部命名空间(Local namespace)
  2. 全局(模块的)命名空间(Global (module's) namespace)
  3. 内建命名空间(Built-in namespace)

局部命名空间所包含的名字只作用于当前的范围(例如一个函数中的局部变量)。全局或者模块的命名空间所包含的名字作用于当前的模块(这里,主程序也被认为是一个模块)。在这里,你可找到所有全局的变量、函数、类等等。最后,内建命名空间包含的名字被定义在 __builtin__ 模块中,它们总是可访问的。它包含了这些名字,例如:

  • 所有异常的名字(如 ArithmeticError, KeyError, ValueError 等等)
  • 内建函数的名字(如 ord, abs, int, str, range 等等)

理解名字解析搜索命名空间的顺序,有助于我们写出更高效的程序。例如:

def codes(name):
    codeList = []
    for c in name:
        codeList.append(ord(c))
    return codeList

上面的函数通过字符串 name 中的字符生成一个 ASCII 码列表。如果这是一个很长的字符串,优化函数中的循环或许是值得的。让我们来考虑循环中需要解析的名字:

  • codeList.append
  • ord

codeList.append 是一个限定的名字,需要两次搜索。首先,Python 需要查找名字 codeList ,恰好它定义在局部命名空间中。然后它被找到了,它是一个列表,继续去列表对象的定义寻找标识符 append。这两次搜索每次都发生在 for 循环中。我们可给限定的名字 codeList.append 定义一个局部的别名函数,把它用到循环中,来降低程序开销。

下一个需要优化的是 ord 函数。它是一个内建函数,因此 Python 不得不无效地搜索局部命名空间和全局命名空间。(译注:即不得不在局部命名空间和全局命名空间没有找到后,才能在内建命名空间中找到它。)通过定义一个局部的别名,我们可以让 Python 在第一次搜索便找到它,即在局部命名空间中找到它。

归纳起来,优化后的代码如下:

def codes(name):

codeList = []
locAppend = codeList.append  # 注意:没有圆括号
locOrd = ord  # 注意:还是没有圆括号
for c in name:
    locAppend(locOrd(c))
return codeList

当然,这还不是最高效的版本。还有优化的空间,但它已经超出了我们探讨的主题“命名空间和名字解析”。所以,我把寻找更好的方法作为练习留给读者。

如果我们理解了名字解析的基本原理,那就明白 Python 是如何处理限定的名字了。例如,标识符 csv.DictWriter.writerow 指的是 DictWriter 类的 writerow(…) 方法,并且这个类定义在 csv 模块中。让我们来分析一下下面的代码:

def fun():
    import csv
    method = csv.DictWriter.writerow

函数 fun() 是这样执行的,成功导入模块 csv 后,Python 将 csv 作为函数 fun() 局部命名空间中的标识符。

执行下一个语句,Python 进行了如下活动:

  1. 它在局部命名空间中寻找 csv 标识符。找到后,Python 不会再进一步到外层的命名空间中寻找(全局命名空间和内建的命名空间)。
  2. Python 发现 csv 标识符是一个模块。因此 Python 在字典 sys.modules 中找到它的定义。(译者注:Python 加载成功的模块,都会把它们加入到这个字典。)
  3. 然后 Python 到这个模块的名字空间中寻找 DictWriter 标识符,并且找到了。
  4. Python 发现 DictWriter 标识符是一个类,然后把这个定义加载到内存中。
  5. 然后到 DictWriter 类的名字空间中去寻找 writerow 标识符,并且找到了。
  6. Python 发现 writerow 标识符是 DictWriter 类的一个方法,并且定位到了这个代码块在内存中的地址。
  7. 总结起来,Python 把限定的名字 csv.DictWriter.writerow 解析为 writerow 方法在内存中的地址。然后把它绑定到了局部名字 method ,以便更快地访问它。

最后,我们必须理解 Python 是如何处理赋值的,是如何限定新旧标识符的值。默认地,Python 将值绑定到局部命名空间的变量,并且很可能隐藏在其它命名空间中的同名对象。例如:

abs = 5

执行上面的赋值语句后,我们失去了对内建函数 abs() 的访问能力。只要新创建的变量 abs 存在于当前的区域,内建函数 abs() 就是隐藏的。我们可以通过删除这个变量来恢复原状。这样就可以使 abs() 恢复访问。

del abs
x = abs(-3)

另外一种方法是在隐藏内建函数之前给它定义一个别名:

locAbs = abs
abs = 5
x = locAbs(-3)

当然,最好的办法是避免这种含糊的语句。

也有可能,赋值应该指向全局命名空间中的变量,而不是局部变量。例如:

lastX = None
def fun(x):
    global lastX
    lastX = x
    return 2*x

在上面的代码中,赋值语句 lastX = x 并没有创建一个局部变量 lastX,而是更新了全局变量——多亏了 global 声明语句。

2.9 __import__ 函数

到目前为止,我们所讨论的 import 语句提供了静态的导入功能。导入的模块名硬编码在你的程序中。Python 语言还提供了一个可以动态导入模块的函数,被导入的模块名事先是不知道的。例如:

def listGlobals(modName):
    module = __import__(modName)
    return dir(module)

上面的函数返回被导入模块的全局的标识符,模块的名字就是传入函数的参数。__import__() 函数 被 import 语句内部调用。它导入给定的模块,并返回模块对象,然后可以像使用模块名一样使用这个返回的对象。例如,我们可以这样写:

res = module.run()

使用 __import__() 函数返回的变量。

__import__() 函数还有额外的可选的参数:

module = __import__(modName, globalDict, localDict, fromList)

字典 globalDict 包含全局的标识符(在这里你可以使用 globals() 函数给它赋值),字典 localDict 代表局部的标识符(可以通过 locals() 函数赋值)。参数 fromList 用于限定导入的标识符,像这样的语法 from modName import id1, id2, …

有时候我们可以通过继承来扩展内建的 __import__() 函数,以支持某些特殊的导入情形。如需编写自己的导入函数,imp 模块是非常有用的。

2.10 内嵌函数

这个话题跟模块没有明显的关系,但它跟之前讨论的命名空间和名字解析关系很大。Python 允许在函数内部定义函数。例如:

def fun(x):
    status = 0
    def step1(a):
        if status == 0:
            ...
    def step2(a):
        if status == 0:
            ...

    step1(x)
    step2(x)

这里参数 x 正常地传递到内嵌函数。但是请注意,内嵌函数也访问了其外层空间的变量(status),这个相对的外层空间指的是函数 fun() 的作用域。这允许一些变量通过参数传递给内部函数。

但这也不是 100% 正确的。你可稳妥地读取这些变量,但如果你试图在内嵌函数中修改这些变量,你将创建一个同名的局部变量,仅在这个内嵌函数内有效,从而屏蔽了外层名字空间的变量。

总的来说,内嵌函数可以帮助你隐藏私有函数。它非常有用,但仅适用于简短的函数,并且不能修改外层空间中的局部变量。

这会促使你使用这种方法来减少一个模块的外部可见函数,但这显然会让查找错误更加困难,并且降低代码的可读性。因此,我建议你保守地使用这个特性。

2.11 我的模块可用吗?

有时候,我们写的代码,假定给定的模块在目标系统中是可用的。也有可能,这个模块丢失了,我们需要做一个替换。这就需要检测的能力,确定模块导入是否成功:

try:
    import myModule
    myModuleOK = True
except ImportError:
    myModuleOK = False
    import alternativeModule as myModule

布尔变量 myModuleOK 会告诉你,模块 myModule 是否被导入了。此外,你可能需要导入一个替代的模块,提供类似的功能。

多亏有了 as 语句,剩下的代码可以假定 myModule 是存在的——并不需要知道,它提供了一个替代原版本的模块。

注意,我们必须捕获 ImportError 异常,其它的异常无法表明这是一个导入错误。

3. 包

通常,包是模块的分级结构。因此,我们所讲的关于模块的大部分内容,也适用于包。在这个章节,我们聚焦在针对包的特性。

使用包的时候,模块的名字由点号拼接成了新的名字。例如 win32com.client (来自 pywin32 模块,针对 Windows 的扩展包)。

3.1 包的目录结构

我们通过 pywin32 包的例子来分析包的目录结构。安装后,在这个目录 /Python23/Lib/site-packages ,你会发现一个 pywin32.pth 文件,它会给 sys.path 列表增加下列目录:

  • /Python23/Lib/site-packages/win32
  • /Python23/Lib/site-packages/win32/Lib
  • /Python23/Lib/site-packages/pythonwin

这是标准的 Windows 目录,它包含了包提供的模块。但这不是所有的。在 site-packages 目录中,可以发现更多来自 pywin32 包的目录,但并没有列在 pywin32.pth 文件中。其中一个目录是 /Python23/Lib/site-packages/win32com 。

这个目录有什么特别呢?你会发现它包含一个 __init__.py 文件,作为 win32com 包的初始化。这个目录还包含一些其它的 Python 源文件(例如 util.py),也包含子目录。其中一个是 'client',有趣的是,它也包含一个 __init__.py 文件。

它表明,import 语句把子目录包含的 __init__.py 文件当作模块。因此下面的语句可以很好地执行:

  • import win32com – 它会加载文件 win32com/__init__.py, 提供名字 'util' 和 'client' 作为内部名字 (通过 'win32com.util', 和 'win32com.client' 访问)
  • from win32com import util – 它会加载文件 win32com/__init__.py, 提供名字 'util' 作为局部命名空间中的标识符

这个规则是递归的,如果导入的元素也是一个包含 __init__.py 文件的子目录。

  • import win32com.client - 加载文件 win32com/client/__init__.py,提供名字例如 'Dispatch' 作为内部名字(win32com.client.Dispatch 是一个用于生成 COM 对象实例的常用函数。)
  • from win32com.client import Dispatch - 加载文件 win32com/client/__init__.py ,提供名字 'Dispatch' 作为局部命名空间中的标识符

文件 __init__.py 必须存在于子目录中,子目录被称为子包,这个文件可以为空。

仔细一点,你会发现函数 Dispatch 定义在子目录 'client' 中的 __init__.py 文件中。 但是除了这个文件,子目录 'client' 还包含了其它 Python 文件。它们都是 win32com.client 子包的模块。总结起来,要使用 win32com.client.Dispatch() 函数,我们需要首先执行下面的 import 语句:

  • import win32com.client -> obj = win32com.client.Dispatch(…)
  • from win32com.client import Dispatch -> obj = Dispatch(…)
  • from win32com import client -> obj = client.Dispatch(…)

正如你从上的例子中看到的,语句 from package import item 可以导入子包、子模块,以及定义在包中的其它标识符。

3.2 from package import *

Python 解释器无法通过自身找到包或者子包的子模块。这里我们需要提供一个 __all__ 变量(也可参考2.3小节),是由公开的可用的子模块名字组成的序列。在特殊的包中,需要在 __init__.py 文件中定义这个变量。

例如:

假如我们把下面的语句加在 'client' 目录的 __init__.py 文件中。

__all__ = ['build', 'util']

然后,下面的语句:

from win32com.client import *

将只会导入下面的子模块到当前的命名空间:

  • build
  • util

尽管 'client' 目录还包含了其它模块。

如果 __all__ 变量没有定义,import 语句不会导入包的所有子模块。

如果只是确认,包已经被成功加载,只有下面的标识符会被导入到当前的命名空间:

  • 定义在包的 __init__.py 文件中是名字
  • 显式地被包的 __init__.py 文件加载的子模块
  • 被之前的 import 语句加载的子模块

3.3 内部包引用

子模块经常需要引用包中的其它子模块。如果其它子模块被定义在同一个子包中,这样我们可以使用简单的引用,不再需要前缀名字。

如果有一个 'test' 模块定义在 win32com.client 子包中,就可以通过下列简单的方式来引用:

import util

而不需要使用限定的名字:

import win32com.client.util

它可以正常工作,因为此时的模块搜索路径已经变为包含当前子包,并且将当前子包作为第一搜索路径。

如果引用其它子包中的模块,就必须使用完整的限定的名字。

这里仍然考虑假想的子模块 'test',它属于 win32com.client 子包。我们希望从 win32com.server 子包中导入 'policy' ,就必须使用下面的语句:

import win32com.server.policy

3.4 关于 wxPython 的一点说明

关于 wxPython 的一个常见问题是它的双重命名规范。你可以使用其中任何一种,尽管旧风格已经不赞成使用。

旧风格(变种一):

from wxPython import wx
class MyFrame(wx.wxFrame):
    ...

旧风格(变种二):

from wxPython.wx import *
class MyFrame(wxFrame):
    ...

变种一需要写2次 'wx' 前缀,非常烦人。变量二则具有很大可能导致名字冲突,因此 'wx' 前缀强加在所有 wx 模块的所有名字前。这种风格不应该再使用。

新风格像下面展示的这样:

import wx
class MyFrame(wx.Frame):
    ...

这里你导入的不是 wxPython 模块,而是 wx 模块。在新的 wx 模块中,所有的标识符都丢弃了 'wx' 前缀,这大大地提高了代码的可读性。那它是怎样工作的呢?

在 Python23/Lib/site-packages/wx 目录中,我们可以看到 __init__.py 文件,这表明 wx 是一个 Python 包。进一步研究这个文件发现,wx 包是如何将原来的 wxPython 包中的标识重命名的。

它定义了一个 _rename() 函数,通过一定的规则,将名字字典转换成对应的丢弃了前缀的字典。在 wxPython.wx 模块本身的命名空间,也调用了这个函数进行标识符名字转换,并把新的名字存储到 globals() 函数引用的字典中,那么调用新的模块 wx 就自然地展示了新的名字。

这是一个低层次处理模块的很好例子,也证明了 Python 语言是如此的灵活,做了这么一种外部不可见的特殊转换。

使用上面所讨论的新风格,我们要记住首先使用 import wx 语句从 wxPython 包导入 wx 。然后,可以进行其它导入,例如:

from wx import html

如果你没有首先导入 wx ,其它的导入语句将不会进行适当的名字转换,会产生问题。

4 最后的话

我真心希望这篇文章能够帮助 Python 开发者使用 Python 的全部潜能。我也会对任何错误和遗漏负全责。事实上,如果有反馈意见,我将感激不尽,这将能帮我完善这篇文章。

非常感谢 MBM Project Tribon forum 的成员与我的讨论,以及向我提的问题,还有 Tribon Vitesse trainings 的参与者。

英文原文