好好学习,天天向上

一次抓虫引发的对python导入机制的初步认识

前段时间因为生产上连接数的问题,手机被告警信息一波一波地袭击。问题的现象是,项目中每个进程都有一个全局的连接对象,一个对象对应一个socket连接。从输出日志可以看出这个对象有正常的建立连接和断开连接过程。但奇怪的是,通过netstat查看连接数,却超过了进程数,说明进程的连接对象并不是进程内全局唯一的。

于是给日志加上了线程id,再次运行发现,同一个线程(该项目使用的是多进程单线程)中对该连接对象进行了两次初始化,并且这两次初始化的对象id(通过内置的id()方法得出)不同。说明该连接对象并非进程内全局唯一的。

可是这是为什么呢???

这个连接对象是作为模块内全局对象定义的,并且被同一个模块内的函数所访问。其他模块通过导入该模块的函数来间接使用该连接对象。因此,理论上讲,导入相同的模块,使用的应该是同一个对象才对。百思不得其解的情况下,有一个疑问摆在面前 —— python,是如何处理导入的呢?

为了弄清这个问题,我们来做个测试。

测试验证目录树如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ tree
.
├── main.py <- 程序主入口
└── test_import
├── a.py
├── b.py
├── __init__.py
├── main.py
├── query
│   ├── client.py <- 这个模块就是保存全局连接对象的模块啦
│   └── __init__.py
└── v1
├── c.py
└── __init__.py

每个文件内容如下: * main.py:主入口

1
2
3
4
5
6
from test_import import main

main.main()
#import sys
#print "id of conn in test_import.query.client", id(sys.modules['test_import.query.client'].__dict__['conn'])
#print "id of conn in query.client", id(sys.modules['query.client'].__dict__['conn'])
* test_import/a.py
1
2
3
4
from query.client import get_data
def a():
print "in", __file__
get_data()
* test_import/b.py
1
2
3
4
5
from query.client import get_data

def b():
print "in", __file__
get_data()
* test_import/main.py
1
2
3
4
5
6
7
8
9
10
from v1 import c

from a import a
from b import b

def main():
print "in", __file__
a()
b()
c.c()
* test_import/query/client.py:我们在这里创建一个模块全局变量,并且将其当成整个project的全局变量使用
1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-

class Conn(object): pass

conn = Conn()

def get_data():
print "[{0}] id of conn is {1}".format(__file__, id(conn)) # 通过conn的id来识别是否属于同一个实例
* test_import/v1/c.py
1
2
3
4
5
6
7
8
9
import os, sys
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(current_dir))

from query.client import get_data

def c():
print "in", __file__
get_data()
好了,准备好测试项目之后,让我们开始研究吧~~

首先,运行一下:

1
2
3
4
5
6
7
8
$ python main.py 
in /Users/ele/lab/test/test_import/main.pyc
in /Users/ele/lab/test/test_import/a.pyc
[/Users/elexu/lab/test/test_import/query/client.pyc] id of conn is 4327254928
in /Users/ele/lab/test/test_import/b.pyc
[/Users/ele/lab/test/test_import/query/client.pyc] id of conn is 4327254928
in /Users/ele/lab/test/test_import/v1/c.pyc
[/Users/ele/lab/test/test_import/query/client.pyc] id of conn is 4327254480
从结果可以看出,a.py和b.py中访问的是同个conn对象,而在c.py中访问的却是另一个conn对象。

这里要提到一个东东 —— sys.modules。关于它有几点信息: 1. sys.modules是python标准库sys中的一个类型为dict的成员,包含自python启动起导入的所有模块。该字典中,键为模块名,值为模块对象。 2. 当导入一个新模块的时候,python会把它们添加到sys.modules中。当第二次导入时,python会在sys.modules中进行查找

因此,为了解释我们第一次运行的结果中,a.py和b.py中访问的对象与c.py不同的原因,可以在a.py/b.py/c.py中的from query.client import get_data之后都加上这么一句话:

1
2
import sys
print __file__, [(k, v) for k, v in sys.modules.items() if "client" in k]
> 说明:因为我们要探究的是client这个模块,因此使用if "client" in k来过滤掉不需要的模块信息。

再次运行:

1
2
3
4
/Users/ele/lab/test/test_import/v1/c.py [('query.client', <module 'query.client' from '/Users/ele/lab/test/test_import/query/client.pyc'>)]
/Users/ele/lab/test/test_import/a.py [('test_import.query.client', <module 'test_import.query.client' from '/Users/ele/lab/test/test_import/query/client.pyc'>), ('query.client', <module 'query.client' from '/Users/ele/lab/test/test_import/query/client.pyc'>)]
/Users/ele/lab/test/test_import/b.py [('test_import.query.client', <module 'test_import.query.client' from '/Users/ele/lab/test/test_import/query/client.pyc'>), ('query.client', <module 'query.client' from '/Users/ele/lab/test/test_import/query/client.pyc'>)]
……
在c.py中,涉及到client到模块信息只有'query.client',而a.py和b.py却多了'test_import.query.client'。

对这个结果进行推论:python导入了两个模块,'query.client'和'test_import.query.client'。虽然这两个模块实际上使用到是同一份源代码(即client.py),但是由于模块名不同,因此python将它们二者当成两个完全不一样到模块处理。在导入过程中,这两个模块fen b分别初始化了一次conn对象。c.py是从'query.client'导入的,而a.py和b.py是从'test_import.query.client'导入,所以a.py和b.py中访问的conn对象才与c.py不同。

取消main.py中最后两行注释语句,看看结果是否如我们所推断的那样:

1
2
3
4
5
6
7
8
9
in /Users/elexu/lab/test/test_import/main.pyc
in /Users/elexu/lab/test/test_import/a.pyc
[/Users/elexu/lab/test/test_import/query/client.pyc] id of conn is 4533709840
in /Users/elexu/lab/test/test_import/b.pyc
[/Users/elexu/lab/test/test_import/query/client.pyc] id of conn is 4533709840
in /Users/elexu/lab/test/test_import/v1/c.pyc
[/Users/elexu/lab/test/test_import/query/client.pyc] id of conn is 4533709392
id of conn in test_import.query.client 4533709840
id of conn in query.client 4533709392
Bingo!!!

接下来有第二个疑问。代码层面,a.py/b.py/c.py使用的都是相同的导入代码:from query.client import get_data。因此,为什么导入模块名会不同呢?对于该问题,我们可以从python的模块导入路径切入。

再次,在a.py/b.py/c.py中的from query.client import get_data之前加入以下语句:

1
2
import sys
print __file__, filter(lambda p: "test" in p, sys.path)
> 说明:为了避免打印出一堆无关的路径,使用filter()进行了过滤

再次运行下:

1
2
3
4
5
6
/Users/ele/lab/test/test_import/v1/c.py ['/Users/ele/lab/test', '/Users/ele/lab/test/test_import']
……
/Users/ele/lab/test/test_import/a.py ['/Users/ele/lab/test', '/Users/ele/lab/test/test_import']
……
/Users/ele/lab/test/test_import/b.py ['/Users/ele/lab/test', '/Users/ele/lab/test/test_import']
……
发现在a.py/b.py/c.py中,与我们的测试目录相关的路径都是一样的。

看来,只能逐个击破了。

将c.py中的下面两行代码注释掉:

1
2
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(current_dir))
运行之后会得到ImportError: No module named query.client这样的错误。这说明了,c.py中的query.client是从/Users/ele/lab/test/test_import中导入的。

现在,来看看a.py和b.py。将test_import.main中涉及到c.py到内容注释掉,然后运行。此时sys.path中已经没有了/Users/ele/lab/test/test_import这一项,但是a.py和b.py仍然可以正常导入。说明,a.py和b.py中的from query.client import get_data()并非从/Users/ele/lab/test/test_import这个路径上搜索导入的。

现在我们来总结解释下。

python在a.py/b.py和c.py中运行到from query.client import get_data()这条语句的时候,由于搜索路径不同,因此导入生成到模块名不同,这近一步导致了python讲它们当成两个完全独立到模块进行处理。

修改方法也很简单。只需将c.py中的from query.client import get_data修改成from test_import.query.client import get_data即可。

运行验证下:

1
2
3
4
5
6
7
in /Users/ele/lab/test/test_import/main.pyc
in /Users/ele/lab/test/test_import/a.pyc
[/Users/ele/lab/test/test_import/query/client.pyc] id of conn is 4373236432
in /Users/ele/lab/test/test_import/b.pyc
[/Users/ele/lab/test/test_import/query/client.pyc] id of conn is 4373236432
in /Users/ele/lab/test/test_import/v1/c.py
[/Users/ele/lab/test/test_import/query/client.pyc] id of conn is 4373236432

o()o

碎碎念

python这个磨人的小妖精~~

扩展阅读

请言小午吃个甜筒~~