2016年10月

Python 装饰器详解

什么是装饰器

装饰器是一个函数,一个用来包装函数的函数,接收一个函数对象,返回一个新的函数。新函数在原函数的基础上,增加了附加的功能。

1. 最简单的装饰器

def foo():
    print("foo() called.")

这是一个简单的函数,如果我们想在函数执行前做点事,可以通过以下装饰器来实现:

def decorator(func):
    def wrapper():
        print("before %s" % func.__name__)
        ret = func()
        return ret
    return wrapper

装饰器函数 decorator 接收一个函数对象作为参数,然后返回一个内部函数,内部函数的作用是在原函数执行前打印一行说明。这一行可以是其它更复杂的逻辑,当然也可以在原函数执行后做一些操作。

foo = decorator(foo)
foo()

现在执行 foo() 就看到我们要的效果了。

装饰器的实现,归功于 Python 对内部函数的支持,在函数内部创建另外一个函数,同时函数可以作为函数的参数。

2. 使用 Python 语法糖

上面对 foo 的修饰动作,有一种语法支持它。

@decorator
def foo():
    print("foo() called.")

即在函数声明前一行加上 @ 标记,这看起来更加简洁,逻辑更紧密。这个语法使得,foo 在声明时,就执行了 foo = decorator(foo)。

类的静态方法(staticmethod)和类方法(classmethod)的实现,通常装饰器使代码更直观。

3. 装饰带参数的函数

其实就是让返回的函数带参数,同时将参数传递到原函数让其调用。

def deco(func):
    def wrapper(*args, **kwargs):
        print("before %s" % func.__name__)
        ret = func(*args, **kwargs)
        return ret
    return wrapper

以上是通用的不确定参数的例子,如果参数确定,则将 wrapper 的形参定义与 原函数 func 相同即可。

4. 带参数的装饰器

装饰器自身带参数,使得装饰器更灵活。这需要在上面不带参数装饰器的基础上再包裹一层,意思是装饰器先调用自己的参数返回一新的装饰器,这个装饰器和上面的装饰器一样。

def deco(*args2, **kwargs2):
    # 这里可以使用参数 args2, kwargs2 处理一些逻辑,外部函数的参数在内部函数均可使用
    def _deco(func):
        def wrapper(*args, **kwargs):
            print("before %s" % func.__name__)
            ret = func(*args, **kwargs)
            return ret
        return wrapper
    return _deco

这也是一个通用的带参数的装饰器。使用示例:

def check_session(is_login=False):
    def _deco(func):
        def wrapper(*args, **kwargs):
            if is_login:
                # 根据相关条件判断是否已登录,若未登录则抛出错误
                pass
            return func(*args, **kwargs)
        return wrapper
    return _deco

@check_session(is_login=True)
def profile(user_id):
    pass

这个装饰器通过 is_login 参数来决定是否需要登录,才能获取用户的资料。

5. 多层装饰

对一个函数使用多个装饰器,也是合法的。

@deco2
@deco1
def foo(*args, **kwargs):
    pass

它相当于 foo = deco2(deco1(foo))。如果装饰器自身带参数,那么同样是先调用自身参数返回一个新装饰器,再执行上面的多层装饰逻辑。

6. 类也可以被装饰

类的装饰器,返回一个新的类。

def deco(cls):
    def show(self):
        print(self.__doc__)
    cls.show = show
    return cls

@deco
class MyObject:
    ''' class MyObject sample '''
    pass

这个装饰器给类增加一个 show() 方法,打印类的文档说明。

7. 包装内部函数

使用上面的装饰器,装饰过的函数,返回的是类似下面的对象:

<function deco.<locals>.wrapper at 0x000000C129C6F158>
<function deco.<locals>._deco.<locals>.wrapper at 0x000000C129C6F268>

foo.__name__ 返回的是 'wrapper' 字符串。

但我们希望 foo 仍像装饰前一样。这时,我们需要使用 functools.wraps 对内部函数再次包装。

from functools import wraps

def deco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("before %s" % func.__name__)
        ret = func(*args, **kwargs)
        return ret
    return wrapper

它相当于 wrapper = wraps(func)(wrapper)。functools.wraps 装饰器能将被装饰的函数的特殊属性保留下来,这样就可以正常地对 foo 使用反射相关特性。

8. 装饰器也是一种设计模式

装饰模式,是动态地给一个对象添加额外的功能,它比生成子类更灵活。

装饰模式,使任何对象被装饰前后,遵循相同的交互方式。装饰后的对象,可以说是原对象的鸭子类型(duck typing)。

装饰器的作用

装饰器可以在被装饰的函数前执行预备代码,在它之后执行清理工作。

装饰器有点继承的感觉,它将函数的通用功能剥离出来,成为装饰器。然后在使用时,给需要某通用功能的函数装饰上去,让代码更简洁。

因此,装饰器擅长于记录日志、通过计时来测试代码性能、实现函数的事务处理等。

支付模块后端设计

现在很多 App 都有应用内购买项目,因此这些 App 都需要一个支付模块。

一般地,我们都是接入第三方支付,如支付宝、微信、网银等。支付的通用流程如下:

  1. 用户在 App 内发起支付请求;
  2. App 通过自己的服务端创建订单;
  3. App 将订单号和应付金额等信息,传递给第三方支付的 SDK,随即显示支付界面;
  4. 用户在第三方支付界面完成支付;
  5. 返回 App 界面,查看支付结果。

下面是一个使用支付宝支付的流程图
支付宝支付流程

App 这边主要维护一个订单表,当然需要自己的服务器,订单数据保存在服务器上。
订单表:

  • 订单号:需要生成一个全局唯一的订单号
  • 用户ID:标明该订单是哪个用户创建的
  • 商品名称
  • 商品描述
  • 商品价格:这是打折前的价格
  • 应付金额:用户实付金额,传递给第三方支付 SDK
  • 创建时间
  • 更新时间:该条记录更新的时间
  • 支付状态:NEW 新创建,PAID 支付成功,FAILURE 支付失败,DONE 订单完成

核心问题

1. 创建订单

关键是生成全局唯一的订单号。最简单的办法是直接使用数据库的自增字段。

一般我们会设计一个有意义的订单号:订单标识 + 时间标识(YYYYMMDDHHMMSS 或者时间戳) + 序号。
订单标识,是预定义好的,作为订单号的前缀,可用于区别不同的业务。
序号,可以是递增的序号,也可以是无意义的随机值,但要保证在同一秒内不重复。递增的序号,可以使用NoSQL 数据库生成,例如 redis 的 incr 操作。

2. 支付回调

上面的通用流程中,显示的支付结果,是同步结果(由第三方支付 SDK 返回的结果)。这可以明确告诉用户,支付已经成功。但是对订单的发货操作,需要等待服务端订单的更新。这就是服务端的异步回调,由第三方支付的服务器向 App 的服务器发送支付结果通知(即上图中的第 13 步),然后 App 的服务端更新订单,执行发货操作。

一般回调请求的参数是带签名或者加密的,App 服务端需要验证签名,以确保通知的合法性。

这里关键是并发的问题,如果同一个订单有多个支付通知同时发过来,而更新订单的逻辑里包括发货操作,且不是原子性的。这就是导致重复发货。对于虚拟商品,如游戏中的金币,就会多次给用户发放金币。所以更新订单的逻辑必须加锁处理,或者采用队列的方式。

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 网络编程