Python Decorator 的一些小细节/坑

Decorator 的本质是什么?

decorator 本质就是一个接收对象对象(对,是个对象,而不是大多数人认为的函数),更多的资料可以参照 理解Python的装饰器 | Darkof

1
2
3
4
5
final_func = decorator(wrapped_function) # 与注释部分的实质是一致的。

@decorator
def wrapped_func(*args, **kwargs):
pass

被装饰的函数与之前相比,改变了什么?

行为

这个是最显而易见的,装饰器可以在原函数执行之前或之后添加额外的行为

函数本身的属性

如果你简单的实现了下面的 decorator 会改变什么呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped


class bar(object): # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)


@foo
def f1(a):
print a

print f1.__name__
#输出: 'wrapped'

@bar
def f2(a):
print a

print f2.__name__
# AttributeError Traceback (most recent call last)
# <ipython-input-9-dff5600c49e8> in <module>()
# ----> 1 f2.__name__
#
# AttributeError: 'bar' object has no attribute '__name__'

从上面我们看出来,函数的 __name__ 属性也发生了变化,这也是为什么我们推荐装饰器的时候使用 fucntools.wraps

1
2
3
4
5
6
7
8
import functools

def foo(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

wraps 会把原函数的属性赋给新的 wrapped 这个函数(主要会同步的属性为 __name__, __module__, __doc__, __dict__, 当然,你也可以添加你希望同步的属性)

函数的参数

是的,很少有人会注意到被装饰过会,函数接收的参数也会产生变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inspect.getargspec(f1)
# 输出 ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)

inspect.getargspec(f2)
# TypeError Traceback (most recent call last)
# <ipython-input-16-bff760b02fba> in <module>()
# ----> 1 inspect.getargspec(f2)

# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/inspect.pyc in getargspec(func)
# 814 func = func.im_func
# 815 if not isfunction(func):
# --> 816 raise TypeError('{!r} is not a Python function'.format(func))
# 817 args, varargs, varkw = getargs(func.func_code)
# 818 return ArgSpec(args, varargs, varkw, func.func_defaults)

# TypeError: <__main__.bar object at 0x108729f50> is not a Python function

f1 接收的函数名从 a 变成了 args 和 kwargs, f2 干脆就拿不到了,这也意味着其实装饰器并不能做到 works anywhere(毕竟有很多装饰器会通过参数来判断这是不是一个 classmethod,然后两个装饰器混用可能会导致其中一个失效)

装饰 classmethod/staticmethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import functools

def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

class Bar(object):
@foo
@classmethod
def duck(cls):
print 'Yooooooooooo!'

Bar.duck()
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# <ipython-input-19-ddf54c241cc4> in <module>()
# ----> 1 Bar.duck()
#
# TypeError: unbound method wrapped() must be called with Bar instance as first argument (got nothing instead)

当我们尝试用之前实现的 decorator 来装饰 classmethod 的时候,会遇到 TypeError,原因是 classmethod/staticmethod 本质是一个 Descriptor 而非 function,我们在一开始提到这样一句话:

decorator 本质就是一个接收对象对象

在前面也聊到了,decorator 是个对象,可能是个 class 或是 function, 那么他接收的是什么呢,很多人认为 decorator 接受的是函数,然而严格来说,decorator 接收的是一个对象

当我们知道 classmethod/staticmehtod 是 Descriptor 之后就很容易的知道如何写一个装饰 Descriptor(当然,你需要先了解什么是 Descriptor)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class bar(object):  # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)

class foo(object):
def __init__(self, func):
self.func = func

def __get__(self, instance, owner):
func = self.func.__get__(instance, owner)
return bar(func)

def __call__(self, *args, **kwargs):
return bar(self.func)(*args, **kwargs)

class Duck(object):
@foo
@staticmethod
def fly():
print 'fly'

@foo
@classmethod
def run(cls):
print 'run'

@foo
def stop(self):
print 'stop'

Duck.fly()
# 输出
# in decorator
# fly

Duck.run()
# 输出
# in decorator
# run

Duck().stop()
# 输出
# in decorator
# stop

mysql offset 为什么这么慢。。。

之前从来没觉得 offset 有什么坑,也没有细想过 mysql 的 offset 的实现原理。

直到这周打算把 4000w+ 的数据热到 redis 中,写了一个脚本, 主要的代码大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from redis import Redis
from sqlalchemy import select

import table # Mysql Table Object

redis_cli = Redis(xxxxxxxxxxx)

CUR = 0
MAX = 40000000

while CUR <= MAX:
query = text("""SELECT id FROM example
LIMIT CUR, 1000
ORDER BY id DESC""")
result = table.execute(query).fetchall()
pipe = redis_cli.pipeline() # 使用 pipeline 来减少连接开销
for item in result:
pipe.set(item.id, 'foo')
pipe.execute()
CUR += 1000

开始执行大概下午 6 点左右,然后我就去吃饭逗猫写代码又睡了一觉。

上午 11 点左右来公司发现,才完成了 1000w 左右的数据,内心是崩溃的。。。。。

看了一眼 slow log,一次 Mysql 的查询需要 40s, 然后开始查一些资料找原因,发现 offset/limit 根本无法用到 index 机制,而是读整张表,然后数到需要便宜的位置,所以上面的代码到 1000w 时, mysql 会按照 id 的顺序逐条累加,一直找到第 1000w 的位置(至于为什么不通过 index 来直接找到 id 为 10000000 的数据,原因很简单,id 为 10000000 的数据并不已经代表是第 1000w 条数据,中间有可能会有数据被删除使得 id 非连续)。

找到了原因重写了一把脚本,把 offset 改成 where 就解决了这个问题,然后用了半个小时就跑完了数据- -。

thumbnail

Python 中的「全局变量」的小细节

前几天被同学问了一个问题,为什么自己修改修改了全局变量但是没有生效,例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# File: foo.py
l = 10

def bar():
global l
l = 20

#-----------------------------

# File: main.py
from foo import l, bar
if __name__ == '__main__':
print l
bar()
print l

输出:
10
10

所以我们还是要弄明白什么是全局变量。。。

Python 有全局变量么

有 - -|||。。。。。。
有两种情况是全局变量:

  • 在当前文件中的最外层作用于声明的变量为全局变量
  • 用 global 声明的变量为全局变量

Python 的「全局变量」的作用域是多大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# foo.py
l = 10


def bar():
global b
b = 1

--------------------------

# In ipython
In [1]: import foo

In [2]: 'l' in foo.__dict__
Out[2]: True

In [3]: 'b' in foo.__dict__
Out[3]: False

In [4]: foo.bar()

In [5]: 'b' in foo.__dict__
Out[5]: True

从上面的代码里我们得到以下几个结论

  • 全局变量的作用域被绑定到所在文件之下
  • 即使是先加载再声明全局变量,依然会绑定到该文件之中

为什么我不能跨文件修改全局变量

其实,正确的来说,这个问题问的并没有切中要点,在最初的代码中,我们无法修改 l 的主要原因是 import 的特性导致

The from form does not bind the module name: it goes through the list of identifiers, looks each one of them up in the module found in step (1), and binds the name in the local namespace to the object thus found.

所以当你执行这个语句

1
from foo import l

本质是在当前的 Namespace 中声明了变量 l, 并将 l 指向 foo 这个 module 中的 l 所指向的对象。

当你执行 foo.bar() 的时候,将 foo 中的 l 改为了 20, 但是 main.py 中 l 扔指向 10,所以并没有实现夸文件修改.
同理,在 main.py 中修改 l 也不会影响 foo.l

最后的最后

虽然前面举了例子,有分析了原理,但是其实就像是聊屠龙术的运行原理(更何况「全局变量」连屠龙书都算不上,顶多算是这个图里的 IE

除非是用来定义常量,否则不要在 Python 里用全局变量。

运营商缓存导致的奇葩问题

很久之前遇到的一个问题, 趁还记得, 记录下来, 避免日后忘记.

在公司上线 2014特别项目 之后, 有用户反馈出现了穿好问题. 这个这问题之前从来没有出现过.

通过检查服务器端未出现 Session 冲突, 用户在主站访问正常, 但是在进入 2014 项目之后, 发现信息出现串号现象.

我们的用户信息的 API 请求地址的格式类似为

1
/user_data.json

所有用户都会请求这个地址, 然后由服务器动态生成 Response 并返回, 通过在线上检查发现返回的数据也没有错误.

后来又发现, 遇到的串号用户基本上集中在相同的几个地区. 通过这个线索, 发现问题是出在 ISP服务商 的环节, 有些小的运营商会对你的静态数据做 cache 来加快用户访问速度, 即使你加了 no-cache 的设置, 但是仍然会有些运营商不按照规范强行缓存.

临时的解决方法就是讲请求地址改为

1
/user_data.json?_=[unix_timestamp]

利用 UNIX时间戳 来避开运营商缓存的问题, 最终问题得以解决.

作为总结, 在以后设计 API 的时候尽量要将每个用户的请求地址设计为独一无二的地址, 避免服务器对同一地址动态生成不同的数据, 最终避开这个问题.

PS:
想对运营商说….Fuck!

Amazon S3 的坑

之前就遇到过一次, 今天又有同事遇到, 总之 boto 是个很神奇的项目, 超多 issue 而且跟着文档走 S3 基本不可用, 经常会遇到 400 错误 T_T. 所以单独记录下.

正确的连接 boto:
首先在目录下创建 .boto 文件, 写入 access key 和 secret key:

1
2
3
[Credentials]
aws_access_key_id = YOURID
aws_secret_access_key = YOURKEY

然后连接的时候要注意设置 validate 为 False:

1
2
3
4
import boto.s3
from boto.s3.connection import Location
c = boto.s3.connect_to_region(Location.CNNorth1)
c.get_bucket(BUCKET_NAME, validate=False)

Python与浮点数

在周六参加 TDD Workshop 的时候, 遇到一个问题就是因为涉及到浮点数运算导致单元测试迟迟通不过- -. 回来就在这方面查了查

#简单的例子

1
2
3
4
5
In [1]: 0.1 + 0.2 == 0.3
Out[1]: False

In [4]: round(2.675, 2)
Out[4]: 2.67

简单说这个就是因为浮点数的问题引起的, 也导致我们浮点数的单元测试没有通过.

#关于浮点数
不管是什么数, 在计算机中最终都会被转化为 0 和 1 进行存储, 所以我们需要先弄明白以下几点问题

  • 一个小数如何转化为二进制
  • 浮点数的二进制如何存储

##浮点数的二进制表示
首先我们要了解浮点数二进制表示, 有以下两个原则:

  • 整数部分对 2 取余然后逆序排列
  • 小数部分乘 2 取整数部分, 然后顺序排列

2.25 的二进制表示是?

整数部分的二进制表示为 10, 小数部分我们逐步来算
0.25 * 2 = 0.5 整数部分取 0
0.5 * 2 = 1.0 整数部分取 1
所以 2.25 的二进制表示为 10.01

0.1 的表示是什么?

我们继续按照浮点数的二进制表示来计算
0.1 * 2 = 0.2 整数部分取 0
0.2 * 2 = 0.4 整数部分取 0
0.4 * 2 = 0.8 整数部分取 0
0.8 * 2 = 1.6 整数部分取 1
0.6 * 2 = 1.2 整数部分取 1
0.2 * 2 = 0.4 整数部分取 0

所以你会发现, 0.1 的二进制表示是 0.00011001100110011001100110011……0011
0011作为二进制小数的循环节不断的进行循环.

这就引出了一个问题, 你永远不能存下 0.1 的二进制, 即使你把全世界的硬盘都放在一起, 也存不下 0.1 的二进制小数.

##浮点数的二进制存储
Python 和 C 一样, 采用 IEEE 754 规范来存储浮点数. IEEE 754 对双精度浮点数的存储规范将 64 bit 分为 3 部分.

  • 第 1 bit 位用来存储 符号, 决定这个数是正数还是负数
  • 然后使用 11 bit 来存储指数部分
  • 剩下的 52 bit 用来存储尾数
    Double-precision_floating-point_format

而且可以指出的是, double 能存储的数的个数是有限的, double 能代表的数必然不超过 2^64 个, 那么现实世界上有多少个小数呢? 无限个. 计算机能做的只能是一个接近这个小数的值, 是这个值在一定精度下与逻辑认为的值相等. 换句话说, 每个小数的存储(但是不是所有的), 都会伴有精度的丢失.

#浮点数计算的问题

现在我们可以看一开始提到的例子

0.1 + 0.2 == 0.3


0.1 在 Python 中真正的数字是 0.1000000000000000055511151231257827021181583404541015625
0.2 在 Python 中真正的数字是 0.200000000000000011102230246251565404236316680908203125
0.3 在 Python 中真正的数字是 0.299999999999999988897769753748434595763683319091796875

这就是为什么 0.1 + 0.2 != 0.3 的原因

round(2.675, 2)

1
2
In [4]: round(2.675, 2)
Out[4]: 2.67

为什么 2.675 精确两位小数之后不是 2.68 呢, 因为 2.675 在计算机中真正的数字是 2.67499999999999982236431605997495353221893310546875

坑啊坑.

#我是如何遇到了这个问题
简单地说是因为我理解错了 decimal 这个模块的用法.
我一开始的使用方式是

1
2
In [14]: Decimal(2.675) * Decimal(1.2)
Out[14]: Decimal('3.209999999999999668043315637')

因为没有仔细看库手册导致的错误使用. 正确的用法是:

1
2
In [15]: Decimal('2.675') * Decimal('1.2')
Out[15]: Decimal('3.2100')

将字符串传入 Decimal, 而将数字直接传入, 它的效果是查看该数字在计算机中实际存储的数字.

#decimal是如何实现的计算精准
我粗略的过了一下 decimal 这个库的源代码, 这个根据 General Decimal Arithmetic Specification 来设计, 简单地说就是将传入的字符串记录符号, 记录一个大数(整数和小数部分直接拼接而成), 记录小数点位置, 然后重写这个类的 operation进行实现.

#参考
using-decimal-in-python
PEP327 Decimal Data Type
代码之谜(五)- 浮点数(谁偷了你的精度?)
Double-precision floating-point format
Floating Point Arithmetic: Issues and Limitations
IEEE 754
Decimal Code
General Decimal Arithmetic
Specification

字符串在 Python 2.x 和 3.x 下的适配

RQ 提交了一个 Pull Request 来解决 issue#437 .

最开始的提交 我做了以下的工作:

  • 找到问题所在
  • 写新的单元测试
  • 将相关串做 decode 操作
  • 跑单元测试通过, 提交 Pull Request

然后….就遇到问题了, Travis 跑完发现 Python2.x 下都没问题, 但是 Python3.x 都跑不过, 原因很简单, python3.x 的时候已经不区分 string 和 unicode, 统一采用 unicode, 因此也取消了 decode 方法. 那么我们如何来同时适配 2.x 和 3.x 版本呢. 我想过很多方法, 但是都觉得不够优雅, 后来还是在 RQ 这个库本身里找到了答案.

在 RQ 中, 对字符串的获取都会经过一个 as_text 的函数处理, 该函数位于 compat/__init__.py, 就是为了同时适配 2.x 和 3.x 版本, 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if not PY2:
# Python 3.x and up
text_type = str
string_types = (str,)

def as_text(v):
if v is None:
return None
elif isinstance(v, bytes):
return v.decode('utf-8')
elif isinstance(v, str):
return v
else:
raise ValueError('Unknown type %r' % type(v))

def decode_redis_hash(h):
return dict((as_text(k), h[k]) for k in h)
else:
# Python 2.x
text_type = unicode
string_types = (str, unicode)

def as_text(v):
if v is None:
return None
return v.decode('utf-8')

def decode_redis_hash(h):
return h

首先通过 six 这个库来判断 Python 版本, 然后根据版本的不同, 声明 as_text 方法的具体实现, 这样在整个库在处理字符串时, 不需要考虑版本差异, 直接调用 as_text 进行处理即可.

用了pyenv-virtualenv, 天黑都不怕

之前就有听大妈推荐过 pyenv. 最近给一个项目这个库提交 Pull Request, 但 Python3.X 的单元测试没有跑过, 而我的机器上没有 Python3.X, 也不想把现有的 Python2.7 替换掉, 所以就用起了这个库.

简单的说, pyenv 是一个Python管理工具, 这个是和我们常用的 virtualenv 有所不通, 前者是对 Python 的版本进行管理, 实现不同版本的切换和使用. 后者测试创建一个虚拟环境, 与系统环境以及其他 Python 环境隔离, 避免干扰.

安装方法我就不做赘述了, pyenv readme 已经写的特别详尽

#pyenv使用方法
简单的说一下使用方法

##安装不同版本的 Python

1
2
pyenv install <version> #安装特定版本的 Python
pyenv install 3.3.0 #安装 Python 3.3.0

当我的系统 Python 版本是 2.7, 但是有个 叫做 py3-project 需要用 Python3 来运行的时候, 只需要这样做:

1
2
3
4
cd py3-project         #进入项目目录
pyenv local 3.3.0 #将当前目录下的Python环境切换为3.3.0
pyenv version #运行显示通过pyenv设置之后的python版本, 得到结果是3.3.0
python --version #查看Python版本, 得到结果也是3.3.0

此时就可以通过 python3.3 来运行项目了, 才这个项目之外的目录运行 Python, 你会发现仍然是系统版本. 通过pyenv可以给不同的目录设置不同的 Python 版本, 还可以通过 pyenv global 这个命令切换整个全局的 Python版本. 赞爆了是不是.

#告别virtualenv
接下来, 再介绍一个工具, 配合pyenv, 让我告别了用了很久了virtualenv.这个工具叫做 pyenv-virtualenv, 安装方法依然跳过, 至于使用, 你只需要记住三条命令:

1
2
3
pyenv virtualenv 3.3.0 env    #创建一个 Python 版本为 3.3.0 的环境, 环境叫做 env
pyenv activate env_name #激活 env 这个环境, 此时 Python 版本自动变为 3.3.0, 且是独立环境
pyenv deactivate #离开已经激活的环境

嗯, 写完这篇继续去修复那段 Python3.X 下通不过单元测试的程序.

再见了, virtualenv.

go语言学习(一)

##包

###包的导入方法1:

1
2
3
4
import (
"fmt"
"math"
)

###包的导入方法2:

1
2
import "fmt"
import "math"

作为 Pytonista, 更喜欢第 2 种方式 更加简洁.

##包的变量导出
Go 语言通过命名来确定该变量是否是可以导出到外部供别的包所使用.可以被导出的变量通过将变量名的首字母大写来标志确定.

##函数

###一个函数可以收 0 到任意多个参数. 接收的参数类型在参数名之后声明:

1
2
3
func add(x int, y int) int {
return x+y
}

如果一个函数接受的所有参数是同一个类型的时候, 那么可以不需要每一个参数都标明类型, 只需要在最后一个参数上标明类型就可以.

1
2
3
func add(x, y int){ #当多个参数是同一类型时, 可以省略只声明一次.
return x+y
}

###一个函数可以返回多个值

1
2
3
func swap(x, y int) int, int{
return y, x
}

###go也可以对返回变量命名

1
2
3
4
func split(sum int) (x, y int){
x = sum * 4 / 9
y = sum - x
}

##变量

###变量声明
go 通过 var 关键字来声明变量, 和函数参数声明一致, 变量类型在变量名之后声明:

1
2
var i int
var c, python, java bool #三个值都为bool类型

###变量初始化
变量可以在声明的时候立刻初始化:

1
2
var i, j int = 1, 2
var c, python, java = true, false, "no!"

###短变量声明
短变量声明是一个特殊的使用方法, 有以下特点:

  • 只能在函数内部使用
  • 不需要var关键字和类型
  • 类型通过赋值时隐式声明
    1
    2
    3
    4
    5
    func main(){
    var i, j int = 1, 2
    k := 3
    c, python, java = true, false, "No!"
    }

###变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
// represents a Unicode code point

float32 float64

complex64 complex128

###常量
常量只能使用const关键字生成

1
const Pi = 3.14