本文是对 http://stackoverflow.com/questions/14132789/python-relative-imports-for-the-billionth-time#answer-14132912 这个 SO 答案的翻译。本人的翻译一向只追求含义准确而不追求字字对应,有些不好翻的术语或者固定说法就直接保留。
这个答案能解释大多关于 relative import,即相对导入的疑惑,讲解十分详尽清晰,算是 SO 上被低估的一个答案。
问题不翻译了,直接摘录下来:
The forever-recurring question is this: With Windows 7, 32-bit Python 2.7.3, how do I solve this "Attempted relative import in non-package" message? I built an exact replica of the package on pep-0328:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
I did make functions named spam and eggs in their appropriate modules. Naturally, it didn't work. The answer is apparently in the 4th URL I listed, but it's all alumni to me. There was this response on one of the URLs I visited:
Relative imports use a module's name attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to'main') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
The above response looks promising, but it's all hieroglyphs to me. So my question, how do I make Python not return to me"Attempted relative import in non-package"? has an answer that involves -m, supposedly.
Can somebody please tell me why Python gives that error message, what it Means by non-package!, why and how do you define a 'package', and the precise answer put in terms easy enough for a kindergartener to understand. Thanks in advance!
Edit: The imports were done from the console.
BrenBarn 的精彩答案(这个哥们可以算是 import 专家了,答了好多这方面的题)
简单地说,直接运行 .py 文件和 import 这个文件有很大区别。Python 解释器判断一个 py 文件属于哪个 package 时并不完全由该文件所在的文件夹决定。它还取决于这个文件是如何 load 进来的(直接运行 or import)。
有两种方式加载一个 py 文件:作为 top-level 脚本或者作为 module。前者指的是直接运行脚本,比如 python myfile.py
。如果执行 python -m myfile
,或者在其它 py 文件中用 import
语句来加载,那么它就会被当作一个 module。有且只能有一个 top-level 脚本,就是最开始执行的那个(比如 python myfile.py
中的 myfile.py
,译者注)。
当一个 py 文件被加载之后,它会被赋予一个名字,保存在 __name__
属性中。如果是 top-level 脚本,那么名字就是 __main__
。如果是作为 module,名字就是把它所在的 packages/subpackages 和文件名用 .
连接起来。
例如,moduleX
被 import 进来,它的名字就是 package.subpackage1.moduleX
。如果 import 了 moduleA
,它的名字是 package.moduleA
。如果直接运行 moduleX
或 moduleA
,那么名字就都是 __main__
了。
另一个令人担忧的问题是,一个 module 的名字取决于它是直接从它所在的文件夹 import 还是通过某个 package import 的。不过只有当你在某个路径中运行 Python 并试图从当前文件夹 import 一个 py 文件时,才需要关注它们的不同。例如,在路径 pacakge/subpackage1
中运行 python 解释器,然后脚本中有 import moduleX
这个语句,此时 moduleX
的名字正是 moduleX
,而不是 package.subpackage1.moduleX
。这是因为 Python 解释器在启动时把当前路径(这里答案写的不准确,其实加入的是 top-level 脚本的路径,因为两者在这种状况下相同,所以也并不算错。译者注)加入了它的搜索路径 (sys.path);如果发现要 import 的 module 就在当前路径,那么 Python 解释器就不知道当前路径是属于哪个 package 的,所以 pacakge 的信息就不会成为 module 的名字的一部分。
一个特例是直接运行 python REPL,这个 REPL 的 session 的名字是 __main__
。
关于你遇到的错误信息,关键点来了:如果一个 module 的名字中没有点(即 package.subpackage1 中的那个点,译者注),那么它就被认为不属于任何一个 package。文件在磁盘上的位置在哪里都不影响,唯一起决定作用的就是 module 的名字,而这又取决于它是如何被加载的。
先在我们看看你在问题中引用的这段话
Relative imports use a module's name attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to'main') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
relative import 使用 module 的名字来决定它是否属于一个 package,属于哪个 package。当你使用这种 relative import from .. import foo
,其中的点的数量代表了 package 结构中的某个层次。例如,如果当前 module 的名字是 package.subpackage1.moduleX
,那么 ..moduleA
代表 package.moduleA
。为了让形如 from .. import
的这种导入能够正常工作,module 的名字里的点数量应当至少和 import 语句中一样多。
前面说了,如果 module 的名字是 __main__
,那么 Python 就不认为它属于某个 package。由于名字里不包含点,所以在这个 py 文件中 from .. import
语句无法正常工作。试图执行这条语句就会报 "relative-import in non-package" 错误。
你犯的错误可能是从命令行运行 moduleX
或类似的操作。当你执行这个操作,moduleX 的名字被设置成 __main__
,所以 relative imports 失败了,因为不包含 package 信息。正如前面说的,如果在同一个路径里 import 一个文件,这时 module 的名字就是文件名,不包含 package 信息,所以相对导入也会失败。
记住,因为 REPL session 的名字总是 __main__
,所以试图在 REPL 里执行 relative import 是不行的。relative import 应当只在 module 文件中被使用。
(无法相对导入的问题)有两个解决方法。如果你真的想直接运行 moduleX
,同时又希望它被当作所在 package 的一部分,可以这么做:python -m package.subpackage.moduleX
。-m
参数告诉 Python 解释器,把这个文件当作一个 module 载入,而不是 top-level 脚本。
如果你并不想直接运行 moduleX
,而是想在另一个文件比如 myfile.py
中使用 moduleX
中定义的函数,那么解决方法是把 myfile.py
文件挪到另一个地方,只要不在 moduleX
所属的 package 的文件夹里就行。然后在 myfile.py
中执行 from package.moduleA import spam
,就能正常工作了。
注意,不论哪种解决方案,都需要 package 的路径(上文中的 package
)在 python 的搜索路径也就是 sys.path
里。如果不在,那么就无法使用这个 package 中的任何文件。
(更严谨的说明:从 Python2.6 开始,在做 package-resolution 时,module 的 “名字” 并不完全等于 __name__
属性,还和 __package__
属性有关。这也是为什么上文中我一直尽量避免用 __name__
来指代 module 的名字。从 python2.6 开始,一个 module 的 “名字” 实际上是 __package__ + '.' + __name__
, 或者直接就是 __name__
,如果 __package__
是 None
的话)
文件上传是 Web 开发肯定会碰到的问题,而文件夹上传则更加难缠。网上关于文件夹上传的资料多集中在前端,缺少对于后端的关注,然后讲某个后端框架文件上传的文章又不会涉及文件夹。今天研究了一下这个问题,在此记录。
先说两个问题:
- 是否所有后端框架都支持文件夹上传?
- 是否所有浏览器都支持文件夹上传?
第一个问题:YES,第二个问题:NO
只要后端框架对于表单的支持是完整的,那么必然支持文件夹上传。至于浏览器,截至目前,只有 Chrome 支持。 Chrome 大法好!
不要期望文件上传这个功能的浏览器兼容性,这是做不到的。
好,假定我们的所有用户都用上了 Chrome,要怎么做才能成功上传一个文件夹呢?这里不用drop
这种高大上的东西,就用最传统的<input>
。用表单 submit 和 ajax 都可以做,先看 submit 方式。
<form method="POST" enctype=multipart/form-data>
<input type='file' name="file" webkitdirectory >
<button>upload</button>
</form>
我们只要添加上 webkitdirectory
这个属性,在选择的时候就可以选择一个文件夹了,如果不加,文件夹被选中的时候就是灰色的。不过貌似加上这个属性就没法选中文件了... enctype=multipart/form-data
也是必要的,解释参见这里

如果用 ajax 方式,我们可以省去<form>
,只留下<input>
就 OK。
<input type='file' webkitdirectory >
<button id="upload-btn" type="button">upload</button>
但是这样是不够的,关键在于 Js 的使用。
var files = [];
$(document).ready(function(){
$("input").change(function(){
files = this.files;
});
});
$("#upload-btn").click(function(){
var fd = new FormData();
for (var i = 0; i < files.length; i++) {
fd.append("file", files[i]);
}
$.ajax({
url: "/upload/",
method: "POST",
data: fd,
contentType: false,
processData: false,
cache: false,
success: function(data){
console.log(data);
}
});
});
用 ajax 方式,我们必须手动构造一个 FormData
Object, 然后放在 data 里面提交到后端。 FormData
好像就只有一个 append
方法,第一个参数是 key,第二个参数是 value,用来构造表单数据。ajax
请求中,通过 input 元素的 files 属性获取上传的文件。files
属性不论加不加 webkitdirectory
都是存在的,用法也基本一样。不过当我们上传文件夹时,files
中会包含文件相对路径的信息,之后会看到。
用 ajax 上传的好处有两点,首先是异步,这样不会导致页面卡住,其次是能比较方便地实现上传进度条。关于上传进度条的实现可以参考这里。需要注意的是contentType
和processData
必须设置成false
,参考了这里。
前端说完了,再说后端。这里以 flask 为例。
@app.route('/upload/', methods=['POST'])
def upload():
pprint(request.files.getlist("file"))
pprint(request.files.getlist("file")[2].filename)
return "upload success"
现在可以解释为什么说所有后端框架都支持文件夹上传了,因为在后端看来文件夹上传和选中多个文件上传并没有什么不同,而后者框架都会支持。flask 的 getlist
方法一般用来处理提交的表单中 value 是一个 Array 的情况(前端用name="key[]"
这种技巧实现),这里用来处理多个上传的文件。
我们选择了一个这样的目录上传
car/
|
+--+-car.jpeg
| +-download.jpeg
|
+--car1/
|
+-SS.jpeg
pprint(request.files.getlist("file"))
打出下面的结果:
[<FileStorage: u'car/car.jpeg' ('image/jpeg')>,
<FileStorage: u'car/download.jpeg' ('image/jpeg')>,
<FileStorage: u'car/car1/SS.jpeg' ('image/jpeg')>]
可以看到,相对路径被保留了下来。可以用filename
属性获取相对路径,比如request.files.getlist("file")[2].filename
的结果就是u'car/car1/SS.jpeg'
。接下来,该怎么保存文件就怎么保存,这就是各个框架自己的事情了。
EDIT
查了一下,确实文件夹上传模式和文件上传模式是不兼容的,参见 这里,引用关键部分:
We only propagate a single file chooser mode which could be one of: { OPENFILE, OPENMULTIFILE, FOLDER, SAVEASFILE }. Only one mode can be selected and they cannot be or'ed or combined. Therefore there's no chance to enable both mode.
EDIT 2
文件上传的程序给师兄和自己用了几个月,单文件上传一直很稳定。今天试图传一个包含很多文件(2000+)的文件夹上去,遇到了问题——进度条能够走到最后,但是随后就看到浏览器控制台里报错 net::ERR_EMPTY_RESPONSE
。看了下后端的输出,Flask 并没有接到请求。首先怀疑的是文件数量过多或者大小太大,查了下,浏览器有限制但似乎是 4G,我还远远没达到,所以不是这个原因。然后看有人提到 PHP 的 timeout,即一个请求如果持续太久后端就直接断掉它。不过 Flask 似乎也没有这种设置。偶然间看到这里提到 gunicorn 默认 timeout 是 30s,恍然大悟,原来是 gunicorn 导致的。
于是在 gunicorn 启动的时候加上 --timeout 120
,这次是 500,总算有点进展,因为请求至少发到后端去了。看了 flask 的 log,错误原因是 too many open files,然后 ulimit
一下果然只有 1024。Flask 上传文件时,500KB 以下的直接以 StringIO Object 的形式存在内存中,大于 500KB 的用 tempfile 存在磁盘上。所以我传的文件一多,很容易开的文件描述符就超了。改 ulimit 也遇到一点小问题,最后按照这里说的用
sudo sh -c "ulimit -n 65535 && exec su $LOGNAME"
解决。之后再次上传就没有问题了。
参考资料:
1. http://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean
2. https://developer.mozilla.org/en-US/docs/Web/Guide/UsingFormDataObjects
3. http://www.w3schools.com/jsref/propfileuploadfiles.asp
4. https://github.com/kirsle/flask-multi-upload
5. http://stackoverflow.com/questions/9622901/how-to-upload-a-file-using-jquery-ajax-and-formdata
资料4的那个demo给了我巨大帮助,没它的代码估计我会多花几倍时间,虽然它实现的是文件上传而非文件夹,但其实没什么不同。
曾经我很难理解,为什么有人愿意花很长时间在Quora或者知乎上写那么长的答案。并不是因为我觉的在问答网站上回答问题这件事难以理解,而是我很好奇Quora和知乎这种无积分的体系是如何激励人去作答的。作为一个混了好几年iAsk以及一年多StackOverflow的人,我很喜欢去回答问题,一方面是觉得在自己擅长的领域回答问题可以进一步提升技能,另一方面积分上涨也让我觉得颇爽。对我来讲积分是很重要的,如果没有积分,大概我也很难有动力去回答问题了。
所以Quora和知乎的流行让我困惑。不过当我注册了知乎并且回答了几个问题之后,疑问便烟消云散了。
因为这两个网站能给人提供极大的满足感,这种满足感是的效果远远能超过积分的激励。
回答问题被upvote/赞让人满足,这件事情没什么奇怪的。但是为什么SO要靠积分和badge来激励人答题,而Quora/知乎不需要,也就是说,其实这里想探讨的问题是,为什么在Quora/知乎上回答问题给人带来的满足感更强烈。当然也有人不认同关于满足感的比较,总之我的感觉是这样。
首先,在综合性问答网站上获得点赞更容易。StackOverflow毕竟是程序员的网站,针对的群体相当单一,更广泛地说,StackExchange系网站都是这样,不同的站点提供不同的内容,而每个站点都只针对特定人群,比如ServerFault针对运维人员,AskUbuntu针对Ubuntu用户等,StackOverflow和Programmers已经是针对用户群最广泛的网站了,而这个群体也只不过是所有程序员。Quora和知乎就不用说了,外国人只要能上网绝对不会没看过Quora的问题,中国人和知乎也是一样。这两个网站特意不区分用户群,首页上会给你推送较火的新问题,你只要呆在网站上,保证你会看到感兴趣的问题,即使这个问题你看到之前压根就不会想要去了解。在这两个网站浏览问题是会上瘾的,而且还是自我感觉良好得一种上瘾,毕竟是在“学知识”嘛。说回点赞数的话题,既然人多了浏览量大了,点赞的量自然也就上去了,一个回答在知乎能收获100赞,同等质量的回答在SO可能也就20个Upvote,你说哪边满足感更强?
虽然放在第二但是和之前同等重要的一个原因:Quora/知乎是社交网站,StackOverflow不是。问答网站发展到今天,功能早已不局限于“问”和“答”,但这些变化的核心就是一个词:社交。Quora我不知道,但是知乎约X还是很普遍的,这就是一个例子。那么具体是哪些功能让Quora/知乎成为了社交网站?我先说个最重要但是又最容易被忽视的:实名制点赞。你看不到StackOverflow上获的upvote是由谁发出的,而知乎会把每个给你点赞的人都告诉你。这个差别就太大了。曾经我以为StackOverflow开得早所以没有做实名制upvote,直到看见SegmentFault也没有实名upvote之后才悟出道理,原来不是它们不重视或者技术做不到,而是SO和SF本身就不是社交网站(其实SF社交性稍微强一点,不过upvote这里还是遵循传统形式)——问答网站不需要太多用户之间的互动,你问我答就好了,但是社交网站要想尽一切办法增强用户互动,这是社交网站的生命。举个例子,SO上知乎上一个问题被某人赞了,可能我就会去看看这个哥们是谁,而如果之前就和此人互动过,那关系可能就更好了一点,这些行为啊结果啊在非实名点赞的情况下根本就不会发生。第二个功能就是关注/follow,因为太明显了大家都懂,所以没什么好说的。知乎为了加强互动还搞出一个东西叫“感谢”,我至今都不明白这玩意和点赞的区别到底在哪里。之前写了这么多,都是为了说明Quora和知乎的社交网站本质,当然这并不是说他们就不是问答网站了,如果说Quora是有社交属性的问答网站,那知乎就是以问答为主的社交网站。讨论满足感,为什么又写这么多社交网站的事情
?很简答,在社交网站回答问题远比在问答网站回答问题有满足感。写到这里,终于可以讲明白一直在说的满足感的本质是什么,那就是“受关注的感觉”。没有人不想be a super star,但不是每个人都可以。Quora/知乎提供了一个这样的平台,彻底引爆了那些在某个领域有知识但是之前一直无法展示出来的人。你很宅?没问题,看看知乎二次元界大牛有多少粉丝;你喜欢编程,没问题,Quora上"如何学习编程"类问题至少有100个,好的回答点赞数上千不要太容易;冷门领域的专家?简直不能再好,因为没有人能回答那些问题这样一来你就能独享所有关注。而且要注意,看到真实用户在给你点赞的感觉不是匿名的100个upvote所能比的,因为它让你感觉到,是这100个活生生的用户给你点了赞,而不是你收到了100个赞,更不要提粉丝数上涨给人带来的成就感了,说白了是不是明星不就在于有没有粉丝么?SO上有个C#领域的大牛叫Jon Skeet,混SO的人基本都知道他,so what?除了被当成梗出现在某些问题里,他和普通用户基本一个样:他回答问题,然后大家upvote,不会说你是个大牛大家就觉得你是个star,而他也根本感觉不到自己是个star,即使大家都认识他——社交网站才有star,问答网站只有专家,造成这种原因除了有没有粉丝等网站功能带来的区别之外,也在于两类网站培养出了不同的用户习惯:问答网站用户不习惯说没有内容或者和问题关系不大的话,而社交网站用户习惯于想到什么说什么,如果是对好的回答,自然就是一堆称赞的话,回答者也会因此更感到自己受到了关注。所以你在SO上是找不到当super star的感觉的,但是在知乎或者Quora就可以。
实际上Quora和知乎又略有不同。之前说Quora是“有社交属性的问答网站”,知乎是“以问答为主的社交网站”,相比“答案”,社交网站用户更追求“有趣”,所以你不可能在Quora上靠抖机灵获得几百个赞,但是在知乎上这种答案随处都是。正因为知乎更偏社交,在知乎上受关注也更容易,除了像之前说的特定领域专家,只要你够机灵,或者只是恰巧在一个很火的问题下面留了一个很机智的回答,那可了不得,哗哗哗就是几百个赞,瞬间就幸福感爆棚了,又有一堆留言回复称赞答主之机智,可能比现实中一年收到的称赞还要多,一般人怎么能受得了这种感觉,再加上右上角的提醒“xx、xxx、xx等xxx人关注了你”,简直就好像明星一般。一个好的回答就是一场个人演唱会,可能只要打上几行字,就能享受到当super star的感觉,还有比这更美妙的事吗?所以,Quora和知乎用户不需要积分,回答问题带来的满足感已经让他们无法自拔,而这两个网站可以用他们庞大用户群无中生有变出这种满足感提供给答题者,就像毒枭们生产毒品给吸毒者一样。吸毒要钱,答题不要钱,为啥回答问题,就这么简单。