python puzzle

记录python开发时遇到的一些令人眼前一亮的坑!

python 对象重用

下面代码目的是构造2个 提供不同默认参数的 testfn 函数。正常的思路,如下。。

1
2
3
4
5
6
7
8
9
10
11
12

watch_list = {
'a': 1,
'b': 2,
}

def testfn(name, data):
print(name, data)

for i in watch_list.keys():
fn = functools.partial(testfn, name=i, data=watch_list[i])
print(fn, lambda: testfn(i, watch_list[i]))

理想中的结果应该print出来2个函数是不同的…

然而…

1
2
(<functools.partial object at 0x10df9e208>, <function <lambda> at 0x10dfcf140>)
(<functools.partial object at 0x10dfd1310>, <function <lambda> at 0x10dfcf140>)

lambda 出来的函数竟然是一样的!!!

原因是lambda 里面存的是引用,也就是指针,这又牵扯python的一个设定,他会重用对象。

1
2
3
4
5
6
7
8
In [10]: id('a')
Out[10]: 4328343392

In [11]: id('a')
Out[11]: 4328343392

In [12]: id('a')
Out[12]: 4328343392

于是上面的 lambda 对象也被重用了,于是发生这种结果。

sof的解决方法

  1. 加一层工厂,这样工厂里面的入参引用必定不同
  2. 使用 functools.partial 建新函数

或者 循环创建函数的同时直接 消费或激活函数… (如果可以的话)

1
2
3
4
for i in watch_list.keys():
fn = functools.partial(testfn, name=i, data=watch_list[i])
fn()
print(fn, lambda: testfn(i, watch_list[i]))

用 dis 看python bytecode

比如

1
2
3
4
a = 0
b = 0
a = a + 1
b += 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1           0 LOAD_CONST               0 (0)
3 STORE_NAME 0 (a)

2 6 LOAD_CONST 0 (0)
9 STORE_NAME 1 (b)

3 12 LOAD_NAME 0 (a)
15 LOAD_CONST 1 (1)
18 BINARY_ADD
19 STORE_NAME 0 (a)

4 22 LOAD_NAME 1 (b)
25 LOAD_CONST 1 (1)
28 INPLACE_ADD
29 STORE_NAME 1 (b)
32 LOAD_CONST 2 (None)
35 RETURN_VALUE

所以 a的写法更好些。。。

文件上传

bottles 做文件上传很方便

最近想做一个功能,想把上到的文件内容加密,然后返回回去,为了更快,所以希望不落地,也就是省去保存到文件的过程,内存读,改,write back

写了个demo, 将上传的内容翻倍后返回。

1
2
3
4
5
6
7
@route('/upload', method='POST')
def upload():
uf = request.files.get('tobe_encrpty')
# 将文件不落地 copy 到 output
print(uf.file)
print(type(uf.file))
return uf.file.getvalue() * 2

然后你会惊讶的发现,对于 1000 byte 以内 的文件上面的代码是ok的,超过 1000 byte 的文件就会报错,因为对于
超过 1000 byte 的文件 uf.file 的类型不是 cStringIO.StringO

代码调用链

(bottles.py) BaseRequest.POST -> data = cgi.FieldStorage(**args)

(cgi.py) FieldStorage.__write(self, line) -> 下面

1
2
3
4
5
6
7
8
9
10
11
def __write(self, line):
if self.__file is not None:
if self.__file.tell() + len(line) > 1000: # 这个1000 也挺莫名其妙的...
self.file = self.make_file('')
self.file.write(self.__file.getvalue())
self.__file = None
self.file.write(line)

def make_file(self, binary=None):
import tempfile
return tempfile.TemporaryFile("w+b")

于是

正确的做法是, 利用 StringIO (有和file相同的api) 再利用 bottle 的private api _copy_file 做一次内存拷贝

1
2
3
4
5
6
7
8
9
10
11
12
@route('/upload', method='POST')
def upload():
uf = request.files.get('tobe_encrpty')
output = cStringIO.StringIO()
# 将文件不落地 copy 到 output
uf._copy_file(output)
file_content = output.getvalue()
# print('to', file_content)
# encrypt val*2
response.set_header('Content-type', 'application/octet-stream');
response.set_header('Content-Disposition', 'attachment; filename=patchencrypted.data');
return file_content*2

为什么 1000, 这个1000 意义何在,看这个 issue

貌似是 2001 年的时候认为 1000 就是很大了 。。。 wtf

avatar

lelouchcr's blog