Alembic 速查笔记

Alembic 命令行

初始化

$ cd yourproject
$ alembic init alembic

创建一次 alembic

alembic revision -m "***"

upgrade / downgrade

alembic upgrade head  # 升级到最新版本
alembic upgrade +2
alembic downgrade -1
alembic downgrade base  # 回退到最开始的版本

获取 alembic 版本信息

alembic history
alembic current
alembic heads
alembic branches

自动生成

alembic revision --autogenerate -m "Added account table"

Alembic 语法

增加表

from alembic import op
import sqlalchemy as sa
def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table(
      'account',
      sa.Column('id', sa.Integer()),
      sa.Column('name', sa.String(length=50), nullable=False),
      sa.Column('description', sa.VARCHAR(200)),
      sa.Column('last_transaction_date', sa.DateTime()),
      sa.PrimaryKeyConstraint('id')
    )

公式:

op.create_table(<表名>, sa.Column(<列名>, *属性))

删除表

def downgrade():
  ### commands auto generated by Alembic - please adjust! ###
  op.drop_table("account")
  ### end Alembic commands ###

公式:

op.drop_table(<表名>)

增加一列

from alembic import op
from sqlalchemy import Column,String
    
op.add_column('organization',
    Column('name',String())
)

公式:

op.add_column(<表名>, Column(<列名>, *属性))

删除一列

op.drop_column('organization','name' )

公式:

op.drop_column(<表名>, <列名>)

修改列属性

op.alter_column('user', 'name', new_column_name='username',
                    existing_type=mysql.VARCHAR(length=20))

公式:

alter_column(<表名>, <旧列名>, new_column_name=<新列名>, existing_type=<旧字段类型>, type_=<字段类型修改后>)

注:当表中有数据时,修改字段类型无效或报错

分批处理

with op.batch_alter_table("some_table") as batch_op:
    batch_op.add_column(Column('foo', Integer))
    batch_op.drop_column('bar')

执行 SQL 语句

sql="""ALTER TABLE actions alter column finished_at type float;"""
conn=op.get_bind()
conn.execute(sql)

插入数据

# 在已有表内插入数据
from alembic import op
from sqlalchemy.sql import table,column
from sqlalchemy import String, Integer, Date
    
#Create an ad-hoc table to use for the insert statement.
accounts_table=table('test',
    column('id',Integer),
    column('name',String),
)
    
op.bulk_insert(accounts_table,
    [
        {'id':1,'name':'JohnSmith'},
        {'id':2,'name':'EdWilliams'},
        {'id':3,'name':'WendyJones'},
    ]
)

更新版本,但不操作实际的 upgrade 内容(慎用)

alembic stamp head 
2019/04/25 21:15 下午 posted in  Python

Python 协程

协程(Coroutine)又称微线程,即轻量级的线程。协程可以理解成与调用方协作,产出由调用方提供的值的过程。与线程相比,其优势在于上下文切换的成本更低,且由用户自己控制。

发展史

Python 中的协程主要经历了三个阶段。协程最开始是在 Python 2.5 中实现的,由生成器变形而来,以关键词 yield/send 等实现;引入 yield from,可以把复杂的生成器重构成小型的嵌套生成器;Python 3.5 中引入了 async/await 语法糖。

由于 yield from 已被移除 python 的语法,本文重点分析 yield/send 和 async/await 关键字是怎么实现协程的。

yield / send

协程的运行

在生成器中使用 yield 关键字,而后生成器的调用方使用 .send(value) 方法发送数据,该数据 value 就会成为生成器函数中 yield 表达式的值。换句话说,yield 是生成器中的一个暂停器,第一次调用时在 yield 处暂停,将 yield 右边的值 return 出去;下一次 send 进来的数据成为 yield 表达式的值。举个例子:

def count_num():
    r = 0
    print("Started.")
    while True:
        x = yield r
        print("Received x: {}".format(x))
        r = r + 1


if __name__ == "__main__":
    coroutine = count_num()
    next(coroutine)

    for i in "hello":
        t = coroutine.send(i)
        print("Coroutine times: {}".format(t))
    coroutine.close()

运行结果如下:

Started.
Received x: h
Coroutine times: 1
Received x: e
Coroutine times: 2
Received x: l
Coroutine times: 3
Received x: l
Coroutine times: 4
Received x: o
Coroutine times: 5

由此可以看出,局部变量 r 的值没有随协程的暂停而改变,可知协程中的局部变量保持在一个上下文中。这也是使用协程的一个好处,无需使用类对象的属性或闭包在多次调用中保持在上下文中。

另外,这里还需要注意的是,next(coroutine) 这一处的意思是先调用协程使其先运行到 yield 处进行第一次暂停,使协程处于暂停状态。之后协程再 send 时,才能生效,这一举动称为“预激”。

协程的状态总共有 4 种,分别为:
GEN_CREATED :等待开始执行状态
GEN_RUNNING :解释器正在执行
GEN_SUSPENDED :在 yield 表达式处暂停
GEN_CLOSED :执行结束

协程预激除了 next() 方法,还可以使用 .send(None) 方法,效果一样。如果将上述例子中预激的代码注释掉,运行后会报错:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    t = coroutine.send(i)
TypeError: can't send non-None value to a just-started generator

错误栈中表达的很清楚:在生成器还处于开始状态时,不可 send 不为 None 的值。

协程异常处理

协程中若出现未处理的异常,会向上传至 next 或 send 的调用方,且此时协程停止。而我们大多时候需要协程内部在出现异常时不退出,这时候通常的处理方法是 throw 方法。

throw 可以使协程抛出指定的异常,而不影响其运行的流程,协程依然在 yield 处暂停。在上述的例子中加入异常处理的功能:

class Error(Exception):
    pass


def count_num():
    r = 0
    print("Started.")
    while True:
        try:
            x = yield r
            print("Received x: {}".format(x))
        except Error:
            print("Coroutine error.")
        r = r + 1


if __name__ == "__main__":
    coroutine = count_num()
    next(coroutine)

    n = 0
    for i in "hello":
        n = n + 1
        if n % 2 == 0:
            coroutine.throw(Error)
        else:
            t = coroutine.send(i)
            print("Coroutine times: {}".format(t))
    coroutine.close()

运行结果:

Started.
Received x: h
Coroutine times: 1
Coroutine error.
Received x: l
Coroutine times: 3
Coroutine error.
Received x: o
Coroutine times: 5

协程处理异常除了用 throw 方法,还可以用 send 方法传入一个非法的值,比如常用的 None,这个也称为哨值。将上述代码中的 coroutine.throw(Error) 换成 coroutine.send(None) 也是一样的效果。

上述代码中最后调用了 close 方法,将协程的状态切换成 GEN_CLOSED。该方法的原理是在 yield 暂停处抛出 GeneratorExit 异常,若协程调用方没有处理这个异常或抛出了 StopIteration 异常,则不做处理,且将其状态切换成 GEN_CLOSED

async / await

从 python3.5 开始,Python 新加了一种协程定义方法 async def。简单的讲,async 定义一个协程,await 用于挂起阻塞的异步调用接口;而协程的调用方法在 Python3.7 中做了些许改动,所以这一节以 Python 版本分成两部分来讲解。

python 3.5 - 3.6

阅读协程的官方文档就会知道:协程本身无法运行,只有将其置于事件循环(event_loop)中才能运行其代码。那么事件循环是什么?在源码中的定义为:

# A TLS for the running event loop, used by _get_running_loop.
class _RunningLoop(threading.local):
    loop_pid = (None, None)


_running_loop = _RunningLoop()

event_loop 继承了 threading.local,创建一个全局 ThreadLocal 对象。后续将协程推进这个 loop,只有 loop 是 running 的,协程才得以执行。

协程的执行

执行协程首先需要将协程包成 future 或 task 再推进 event_loop;然后执行 loop.run_until_complete ,运行 loop 中所有协程。

这里 future 指一种对象,表示异步执行的操作;task 指对协程进一步封装,其中包含任务的各种状态,其中 task 是 future 的子类。

有两种方式:asyncio.ensure_futureloop.create_task 。但二者本质都是一样:将协程包成 future。下面两种实现方式效果一样:

async def count_num(num):
    print("count num: {}".format(num))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    future = asyncio.ensure_future(count_num(100))  
    loop.run_until_complete(future)
    loop.close()
async def count_num(num):
    print("count num: {}".format(num))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    task = loop.create_task(count_num(100))
    loop.run_until_complete(task)
    loop.close()

需要注意的是:也可以直接执行 loop.run_until_complete(coroutine),但这里是其实是先将 coroutine 包进了 ensure_future 里的。

协程的并发与阻塞

既然协程是为了异步而生,那么其异步执行肯定是重点。asyncio 的调用方法 asyncio.gather() 就可以将多个协程推进同一个事件循环。看个例子:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at".format(num), time.strftime('%X'))
    for i in range(num):
        time.sleep(1)
        print("[coroutine #{}] count: {} at".format(num, i), time.strftime('%X'), "...")
    print("Finish coroutine #{} at".format(num), time.strftime('%X'))


if __name__ == "__main__":
    print("Start.")
    loop = asyncio.get_event_loop()

    loop.run_until_complete(asyncio.gather(
        count_num(3),
        count_num(4),
    ))
    loop.close()

    print("Finish at", time.strftime('%X'))

该例子中调用了两个协程,功能是数数,从 0 开始数到传进去的数结束,每数一个数都会 sleep 1s。为了直观,每一步都将当前的时间打印出来。执行结果:

Start.
Started coroutine #3 at 11:51:10
[coroutine #3] count: 0 at 11:51:11...
[coroutine #3] count: 1 at 11:51:12...
[coroutine #3] count: 2 at 11:51:13...
Finish coroutine #3 at 11:51:13
Started coroutine #4 at 11:51:13
[coroutine #4] count: 0 at 11:51:14...
[coroutine #4] count: 1 at 11:51:15...
[coroutine #4] count: 2 at 11:51:16...
[coroutine #4] count: 3 at 11:51:17...
Finish coroutine #4 at 11:51:17
Finish at 11:51:17

通过执行结果可以看出,#3 和 #4 是分别执行,没有我们想要的并发的效果。而这里就需要 await 关键词来发挥作用了,await 可以将阻塞的协程挂起,让事件循环执行别的协程,直到其他协程挂起或执行完毕。我们将上例中的 sleep 进行修改:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at".format(num), time.strftime('%X'))
    for i in range(num):
        await asyncio.sleep(1)
        print("[coroutine #{}] count: {} at".format(num, i), time.strftime('%X'), "...")
    print("Finish coroutine #{} at".format(num), time.strftime('%X'))


if __name__ == "__main__":
    print("Start.")
    loop = asyncio.get_event_loop()

    loop.run_until_complete(asyncio.gather(
        count_num(3),
        count_num(4),
    ))
    loop.close()

    print("Finish at", time.strftime('%X'))

执行结果:

Start.
Started coroutine #3 at 11:59:16
Started coroutine #4 at 11:59:16
[coroutine #3] count: 0 at 11:59:17...
[coroutine #4] count: 0 at 11:59:17...
[coroutine #3] count: 1 at 11:59:18...
[coroutine #4] count: 1 at 11:59:18...
[coroutine #3] count: 2 at 11:59:19...
Finish coroutine #3 at 11:59:19
[coroutine #4] count: 2 at 11:59:19...
[coroutine #4] count: 3 at 11:59:20...
Finish coroutine #4 at 11:59:20
Finish at 11:59:20

python3.7

Python3.7 在原来的基础上对协程的执行做了一层封装,使得这个功能更加亲人。我们只需要定义我们需要的协程,然后调用 .run() 即可;在多协程的情况下,我们做出一个统一入口即可,看个例子:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at ".format(num), time.strftime('%X'))
    for i in range(num):
        await asyncio.sleep(1)
        print("[coroutine #{}] count: {} at ".format(num, i),
              time.strftime('%X'), "...")
    print("Finish coroutine #{} at ".format(num), time.strftime('%X'))


async def main():
    await asyncio.gather(
        count_num(3),
        count_num(4),
    )


if __name__ == "__main__":
    print("Start.")
    asyncio.run(main())
    print("Finish at ", time.strftime('%X'))

这段代码的结果与上面例子的结果一毛一样,可以看到调用起来就简单很多,这是因为绝大部分的逻辑(包括事件循环)都在 .run() 方法中替你封装好了。拜读一下源码:

def run(main, *, debug=False):
    if events._get_running_loop() is not None:
        raise RuntimeError(
            "asyncio.run() cannot be called from a running event loop")

    if not coroutines.iscoroutine(main):
        raise ValueError("a coroutine was expected, got {!r}".format(main))

    loop = events.new_event_loop()
    try:
        events.set_event_loop(loop)
        loop.set_debug(debug)
        return loop.run_until_complete(main)
    finally:
        try:
            _cancel_all_tasks(loop)
            loop.run_until_complete(loop.shutdown_asyncgens())
        finally:
            events.set_event_loop(None)
            loop.close()

然后这里有一点需要注意的是,.run() 函数不能在同一个线程已有事件循环的情况下调用,它始终会新建一个事件循环,并且在执行完所有的协程后将其关闭。

2018/09/14 21:08 下午 posted in  Python

符合 Python 风格的对象

在 Python 中,自定义的类也可以表现得像内置类型一样自然,这都得益于鸭子类型:我们只需按照预定行为实现对象所需的方法即可。这篇文章主要介绍自定义类的一些特殊方法,来让类的行为跟真正的 Python 对象一样。

类的特殊方法

类的特殊方法是为了被解释器调用,目的是可以将一些内置的方法用在对象上。比如特殊方法 __len__ 是为了 len() 函数的调用,我们在使用的时候就可以直接使用 len(a) 这种写法,而不是 a.__len__() ,如果 a 是自定义对象,Python 就会自己去调用我们自定义的 __len__ 方法。

Python 中与运算符无关的特殊方法:

与运算符有关的特殊方法:

下面介绍几个常用的特殊方法。

对象表示形式

Python 提供了两种获取对象的字符串表示形式: repr()str()

Python 有一个内置 repr 函数,将一个对象用字符串形式表示出来,通过调用 __repr__ 这个特殊方法来实现的。同样, str() 是通过调用 __str__ 实现的。

在类中,我们需要实现 __repr____str__ 方法来实现将对象用字符串表示。需要注意的是, __repr__ 所返回的字符串应该准确无歧义。

__repr____str__ 的区别在于,后者是在 str() 函数被使用,或在用 print 函数打印对象的时候才被调用,并且它返回的字符串对终端用户更友好;而一个对象没有 __str__ 函数, Python 又需要调用它的时候,解释器会用 __repr__ 函数作为代替。

格式化显示

Python 内置的 format 方法和 str.format 方法将各个类型的格式化方法委托给相应的 __format__ 方法。

如果类中没有定义该方法,在对对象使用 format 方法时,从 object 继承的方法会返回 str() 。然而在这种情况下,有一种弊端就是如果我们传入格式说明符,object.__format__ 方法会报错。

我们希望得到的结果是对象的每个属性都以我们传入的形式表示出来。举个例子:

In [1]: class Test:
   ...:     def __init__(self, x, y):
   ...:         self.integer = x
   ...:         self.decimals = y
   ...:     def __iter__(self):
   ...:         return (i for i in (self.integer, self.decimals))
   ...:     def __str__(self):
   ...:         return str(tuple(self))
   ...:     def __format__(self, format_spec=''):
   ...:         contexts = (format(s, format_spec) for s in self)
   ...:         return "{}.{}".format(*contexts)
   ...:     

In [2]: format(Test(123, 9), "d")
Out[2]: '123.9'

类的散列化

为了实现类的散列化,我们需要实现 __hash__ 方法。根据散列化的定义,我们需要保证对象唯一不变,且需要返回对象属性的散列值,所以另外需要实现 __eq__ 方法。

为了保证唯一不变,我们需要将对象属性设置成只读。举个例子:

class Test:
    def __init__(self, x, y):
        self.__integer = x
        self.__decimals = y

    @property
    def integer(self):
        return self.__integer

    @property
    def decimals(self):
        return self.__decimals

    def __iter__(self):
        return (i for i in (self.integer, self.decimals))

    def __str__(self):
        return str(tuple(self))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash(self.integer) ^ hash(self.decimals)

property 装饰器即可将读值方法标记成特性,只有将对象的属性都设置成不可变,才能实现 __hash__ 方法。这个方法需要返回一个整数,且需要保证相等对象的散列值相同,所以最好的实现方式是使用异或运算来混合各属性的散列值。我们来看看上面这个类的散列值:

In:
    t1 = Test(123, 9)
    t2 = Test(234, 8)
    t3 = Test(234, 8)

    print("t1 hash is: {}".format(hash(t1)))
    print("t2 hash is: {}".format(hash(t2)))
    print("t3 hash is: {}".format(hash(t3)))
    
Out:
    t1 hash is: 114
    t2 hash is: 226
    t3 hash is: 226

classmethod 和 staticmethod

Python 中有两个特殊的装饰器:classmethod、 staticmethod

classmethod :即类方法,是 Python 专用的。该装饰器定义了该方法是操作类,而不是操作实例,因此类方法的第一个参数时类本身,而不是实例。按照约定,类方法的第一个参数名为 cls 。classmethod 最常见的用途是定义备选构造方法。

staticmethod:即静态方法。该装饰器也会改变方法的调用方式,但第一个参数不是特殊的值。静态方法就是普通的函数,只是碰巧在类的定义体中,所以无法引用类或对象的属性。

举个例子看两者的差别:

class Test:
    cont = 1

    @classmethod
    def c_method(cls):
        return "This is a classmethod, which cont is {}".format(cls.cont)

    @staticmethod
    def s_method():
        return "This is a staticmethod."

输出结果:

This is a classmethod, which cont is 1
This is a staticmethod.

类的属性

在 Python 的底层实现中,类的所有对象的属性都存在 __dict__ 属性中,且所有对象的属性共用 key

Python 不能像其他语言一样用 private 修饰符来创建私有属性,但是有一个简单的机制来避免子类意外覆盖私有属性,即以 __ 开头来标记属性私有化,比如上面例子中 __integer__decimals 属性。这些私有属性存入 __dict__ 中时,属性名变为 _ + 类名 + 属性名,比如 __integer 会变为 _Test__integer。这种语言特性称为名称改写。

需要注意的是,Python 中不会使用单下划线对属性名做特殊处理,不过很多 Python 程序员会严格遵守 “不在类外部访问这种属性” 的约定。

在前面的博客中,我们讲到了 Python 中字典的底层实现,字典的运算速度很快,但相当吃内存。而当类的属性多到一定数量时,我们需要用到 __slots__ 属性,来节省内存。

使用 __slots__ 类属性节省空间

在 Python 中,唯一节省内存的数据结构是元组,所以 __slots__ 属性的实现方法是让解释器在元组中存储实例属性,而不是字典。其定义十分简单:

class Test:
    __slots__ = ("__integer", "__decimals")

使用元组的形式也就意味着这些属性不可变。有几点需要注意的是:

  • 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承该属性;
  • 对象只能拥有 __slots__ 中列出的属性,除非将 __dict__ 加入其中;
  • 用户自定义的类中默认有 __weakref__ 属性,若想把对象作为弱引用的目标,需要把 __weakref__ 也添加到 __slots__ 中。

如果使用得当, __slots__ 属性能显著节省内存,而该属性的存在是为了优化,不能用此作为限制用户赋值的属性。

2018/07/08 16:05 下午 posted in  Python

Python 内置数据结构

Python 内置了强大的数据结构,比如列表、元组、字典,让 Python 开发者处理数据时可以信手拈来,但是正是因为 Python 做了太多,让我们忽视了很多细节,本文通过解析 CPython 源码,介绍 Python 的内置数据结构的设计与实现。

Python 序列类型概览

Python 标准库用 C 实现了丰富的序列类型。根据存放的内容可以分为 容器序列扁平序列

容器序列:list、tuple、collections.deque
扁平序列:str、bytes、bytearray、memoryview、array.array

简单讲,容器序列存放的是对任意对象的引用;扁平序列存放的是值,也就是说扁平序列只能存放字符、字节、数值等基础类型。接下来我们从 CPython 实现的角度出发,详细讲解 Python 中最常见的两种序列——列表和元组。

序列之列表

list 作为 Python 中最常用的内置数据结构,运用十分广泛且灵活。首先 list 是个可变序列,可以自由增加或删除元素,其次 list 可以存放任意类型的元素,光这两个特点就足够程序员开心的了。下面看看 list 是如何实现的。

列表的实现

首先看看 CPython 中对 list 结构的定义,其对象接口定义在 listobject.h 中:

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; 
} PyVarObject;

其中 PyObject_VAR_HEAD 由下面的 PyVarObject 定义,而其中的 ob_size 记录的是实际使用的内存的数量;ob_item 指向了列表所在内存的首地址;allocated 则记录了当前列表中可存放的所有元素的数量总和。

每一次需要申请内存的时候,总会申请大块内存,将申请的内存大小记录在 allocated 中,而实际使用的内存大小记录在 ob_size 中。这样做就很高效的实现了内存管理,可以频繁地进行插入、删除等操作。

list 的所有操作都是通过指针 ob_item 实现的。指针指向存储对象的内存地址,也就实现了存放任意类型的元素这一功能。list 的实现定义在 listObject.c 中,代码就不贴出来了,链接:https://github.com/python/cpython/blob/master/Objects/listobject.c

CPython 在列表中维护了一个缓冲池 free_list,里面存放了可用的 list 对象,总长度为 80。创建列表前先在这个缓冲池中查找可用对象,如果有直接唤醒,对其 ob_item 分配空间;如果没有则另外申请内存,再对其 ob_item 分配空间。相对应的,销毁 list 时,先销毁其 ob_item 指向的空间,再检查 free_list 中是否有空间,如果有将其放入以供下次使用;如果没有直接销毁。

正如前面所说,list 的所有操作都是通过 ob_item 实现的,那么基于 C 中指针的了解,不难理解列表的索引、修改等操作,这里不赘述。

需要注意的是,insert 和 append 操作都对列表当前的使用内存产生影响。所以在插入元素前调用 list_resize 函数来调整内存。调整过程为:

  1. allocated/2 <= newsize <= allocated 时,直接赋值,即 ob_size = newsize
  2. 否则调用 realloc 重新分配内存并缩小 allocated ,以实现内存空间的最大利用。

而 insert 和 append 的区别在于:insert 将 i (需要插入的位置) 向后的元素向后顺移,再在 i 处插入;append 在 ob_size + 1 处插入。

而删除操作,需要遍历整个列表,找到满足条件的元素时,将其删除,并将后面位置的元素前移一位。

了解了列表的基本操作之后,我们知道列表的索引、修改和 append 操作的复杂度为 O(1) ,而 insert 和删除需要遍历,复杂度为 O(n)

序列之元组

Python 中的元组以其不可变特征闻名,可以理解成是一个不可变的列表,下面看看元组的底层实现。

元组的实现

元组的结构定义在 tupleobject.h 中:

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;

与列表类似, PyObject_VAR_HEAD 中的 ob_size 记录元组长度(一经创建不可改变); ob_item 是存放元组元素的指针数组。

元组的相关操作定义在 tupleobject.c 中,链接:https://github.com/python/cpython/blob/master/Objects/tupleobject.ctupleobject.c 中也维护了一个元组缓冲池 free_list ,定义如下,但是长度只有 20。这个缓冲池与列表不一样的是,数组中每个元素指向的是一个单链表的头指针,这个链表中元组对象的 ob_item[0] 指向下一个元组,且每个元组长度一致。而 numfree 记录的是这个链表的长度,最长为 2000。

static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
static int numfree[PyTuple_MAXSAVESIZE];

元组的创建与列表类似,先从缓冲区中查找是否有可用对象,有则提取头指针,同时 numfree 对应减一;没有则另外开辟内存。删除元组的时候,先判断缓冲区对应的链表长度是否超过最大长度,没有就将其放入单链表头;超过则直接销毁。元组一经建立不可改变,所以没有其他赋值操作。

从以上分析可以看出,元组的缓冲区仅对长度小于 20 的元组做了优化。元组的元素索引也是通过指针读取,这一点跟列表一致。而与列表相比,元组中没有 allocated ,可以看出相同元素的列表比元组耗内存。

由于元组是通过指针数组 ob_item[] 存储的,换句话说,元组储存了元素的地址。元组的不可变在于其记录的内存地址不可变,而该地址中存储的内容是可以改变的(除非该地址中的内容本身也是不可变的)。

对序列的操作

Python 的序列一般都支持切片、+、* 等操作,基础操作这里不做介绍,只介绍一个特殊的操作——增量赋值及其可能引发的 bug 。

增量赋值

增量赋值是指 +=*= 操作,其表现如何取决于左边的操作对象。+= 相当于调用特殊方法 __iadd__ ,如果此对象没有实现 __iadd__ 方法则会调用 __add__

__iadd__ 是就地加法(不会创建新变量),对于可变序列而言, a += b 相当于对 a 直接调用 a.extend(b) ;如果没有实现 __iadd__ ,就相当于 a = a + b ,而此过程是 a + b 创建出一个新对象,再赋值给 a

*=+= 一样,只是背后的特殊方法是 __imul__。总体来说,可变序列都实现了 __iadd____imul__ 方法,所以 +=*= 都是就地加法。

然而,对某些元组使用增量赋值,会产生神奇的事情,看个 🌰:

In [1]: a = (1, 2, [3, 4])

In [2]: a[2] += [1, 1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-c3bcb5efc80e> in <module>()
----> 1 a[2] += [1, 1]

TypeError: 'tuple' object does not support item assignment

In [3]: a
Out[3]: (1, 2, [3, 4, 1, 1])

如果元组中有可变类型的变量,再对元组中的这个可变对象进行增量赋值的时候,会提示元组不支持赋值,但实际又赋值成功了。

这是因为增量赋值不是原子操作,这行代码的执行顺序是:

  1. 执行操作 a[2] + [1, 1]
  2. 将第一步执行的结果赋值给 a[2]

由于 a[2] 是可变列表,所以第一步可以执行成功, a[2] 的值已经发生改变;但第二步是个赋值操作,由于元组是不可变的,所以报错。

上述这种边界情况十分罕见,为了避免这种情况出现,还是避免出现在元组中放入可变序列这种操作。

字典

Python 中另外一种十分重要的数据结构就是字典,在各种程序中被广泛使用。而 Python 也对其进行了高度优化。为了更好的使用字典,我们来剖析字典的内部构造。

字典的结构

字典是基于散列表实现的,其结构定义在 dictobject.h 中:

typedef struct {
    PyObject_HEAD
    Py_ssize_t ma_used;
    uint64_t ma_version_tag;
    PyDictKeysObject *ma_keys;
    PyObject **ma_values;
} PyDictObject;

其中, ma_used 记录了字典中元素的个数; ma_version_tag 记录了字典版本,每次字典有修改该值都会变;指针 ma_keys 指向一个 PyDictKeysObject 类型的对象; ma_values 记录的是字典的 value 值,而我们根据这个值是否为 None 来判断字典的类型( combined/split )。

PyDictKeysObject 的结构如下:

PyDictKeysObject
dk_refcnt
dk_size
dk_lookup
dk_usable
dk_nentries
dk_indices
dk_entries

其中, dk_size 记录了 hash 表 dk_indices 的大小; dk_lookup 表示在 hash 表中查找元素的方法; dk_usable 记录的是 dk_entries 中可用的数量; dk_nentries 记录的是 dk_entries 中已用的数量; dk_indices 是真正的 hash 表; dk_entries 是一组 PyDictKeyEntry 对象的数组,其定义如下。

typedef struct {
    /* Cached hash code of me_key. */
    Py_hash_t me_hash;
    PyObject *me_key;
    PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;

由于对象的属性都是通过字典的形式存储,会导致很多对象的属性都是键一样但值不一样。Python 针对这一特性对字典的内存管理做了优化。将字典分成 combined 和 split。

combined 型字典中, dk_refcnt = 1 ,字典的值存放在 ma_keysdk_entriesme_value 中;
split 型字典中,dk_refcnt >= 1,字典的值存放在 ma_values 中。

dk_entries 数组的 index 是根据 ma_keysdk_indices 获取的,有4种状态:

  1. Unused. index == -1.
  2. Active. index >= 0, me_key != NULL and me_value != NULL
  3. Dummy. index == -2 (combined only)
  4. Pending. index >= 0, key != NULL, and value == NULL (split only)

显然,对于 Unused 和 Dummy 类型的 slot, dk_entries 中没有再浪费内存;同时,还会根据需要索引的 dk_entries 的数量动态的决定用什么类型的变量来表示 index。再来说说 split 类型的字典,这种字典的 key 是共享的,有个引数计数器 dk_refcnt 来维护当前被引用的个数,其 value 值以数组的形式存放在 ma_values 中,这样就避免同一个 key 多次储存 value ,节省了内存,也使得同一个 key 值的 value 以更紧凑的方式存储在内存中。

字典的实现原理

基于对字典结构的认知,我们再来看看字典的实现原理。关于字典的操作源代码链接:https://github.com/python/cpython/blob/master/Objects/dictobject.c

(1) 创建字典

字典中也维护了一个缓冲池 freelist ,长度为 80。创建字典的逻辑也类似,先在缓冲池中查找可用的字典,有则直接使用,没有则走申请内存的逻辑。在某些特定情况(比如对象的属性赋值)下,会将字典初始化为 split 型。

字典在每次 insert 新键值对前,都会检查 dk_entries 中可用的空间,必要时重新分配以保证至少有三分之一是可用的。在插入新键值对时,先计算 key 的 hash 值,再用这个 hash 值根据一套完整的算法计算出 dk_entries 数组的 index。最后对应变量记录数据。

(2) 字典的索引

字典的索引也是根据 key 的 hash 值来获得的,计算出 hash 值后,将该值的最低几位数字当做偏移量,在 hash 表中查找 index,若找到的 dk_entries 为空,则抛错;若不为空,检查 me_key 是否等于 key,相等则对应的 me_value 即为所求,不相等则发生 hash 碰撞,为了解决 hash 冲突问题,算法会在 hash 值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找,重复以上步骤。可用图表示如下:

字典的特征

通过以上对字典的实现原理的分析,不难得出以下结论:

key 必须是可散列的。即满足以下条件:

  1. 支持 hash() 函数,且得到的值是唯一的;
  2. 支持通过 __eq__() 方法来检测相等性;
  3. a == b 为真,则 hash(a) == hash(b) 也为真;

字典在内存上的开销巨大

由于字典使用了 hash 表,而 hash 表又必须是稀疏的,这导致它在空间上的效率低下。而用元组取代字典可以节省相当的内存开销,原因有二:1. 避免了 hash 表所消耗的内存;2. 无需把记录中字段的名字在每个元素里都存一遍。

键查询很快

dict 的实现是典型的空间换时间,只要字典能被装在内存里,就可以提供无视数据量大小的快速访问。

键的次序取决于添加顺序

当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安排存放到另一个位置。

往字典里添加新键可能会改变已有键的顺序

无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。所以最好不要对字典同时进行迭代和修改。

2018/06/13 00:02 上午 posted in  Python

Python 装饰器和闭包

装饰器是 Python 中常见的语法糖,这篇文章讲了闭包和装饰器的原理,并且分析了函数中变量的作用域,以及尝试总结了常见的坑。

装饰器基础

首先来看看装饰器的定义:装饰器本身是一个可调用对象,接收一个函数作为参数,然后将其返回或替换成另一个函数或可调用对象。可以理解成,原来那是一堵白墙,经过了一层装饰,这层装饰可能是漆也可能把墙掏空镶进一层黄金变得金光闪闪,最后呈现在你眼前的就是装饰后的样子。

严格来说,装饰器只是语法糖,可以把它完全当成常规函数来调用,其参数是另一个函数。装饰器有两大特征,一是能把被装饰的函数替换成其他函数,二是装饰器在加载函数时立即执行。用一个🌰来看看这两个特性:

def red_oil(func):
    print("ready to paint!")

    def red_wall_func():
        print("red wall!")

    return red_wall_func


@red_oil
def wall():
    print("wall!")


if __name__ == "__main__":
    print("start painting!!!")
    wall()

运行结果:

ready to paint!
start painting!!!
red wall!

可以看出,装饰器 red_oil 将函数 wall 替换成了另一个函数,就好比在原来的墙上刷了一层红漆。而装饰器在被装饰的函数被定义时立即执行,而被装饰的函数在运行的时候才执行,这也是导入时和运行时的区别。

闭包

通常函数里声明的局部变量的作用范围是函数内,而未声明的变量视为全局变量,如果连全局变量都没有声明,就是 bug 了。但是 Python 的设计是不要求声明变量,那么函数中变量的作用范围就不一样了,为了延长作用范围,引进了闭包。下面一一做解析。

函数中变量的作用范围

Python 中不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。如果在函数中赋值时想把某变量当成全局变量,需要使用 global 声明:

red_paint = "red oil"


def wall(paint):
    print(paint)
    global red_paint
    print(red_paint)


if __name__ == "__main__":
    wall("yellow oil")

闭包的定义

闭包是指延伸了作用域的函数,其中包含函数定义体中引用、但不在定义体中定义的非全局变量。来看个🌰:

def wall():
    paints = []

    def paint_wall(color):
        paints.append(color)
        return paints

    return paint_wall


if __name__ == "__main__":
    white_wall = wall()
    white_wall("red")
    result = white_wall("yellow")
    print(result)

运行结果: ['red', 'yellow'] 。可以看出,变量 paints 是在函数 paint_wall 外定义的,这称作自由变量,指未在本地作用域中绑定的变量。而 paint_wall 的闭包衍生到函数的作用域之外,包含自由变量 paints 的绑定。在之前的文章《Python 一等函数》中讲到,函数的 __code__ 属性指“编译成字节码的函数元数据和函数定义体”,意思是指编译后的函数定义体,保存了局部变量和自由变量的名称。我们可以用上面的🌰查看。

In [9]: white_wall.__code__.co_varnames
Out[9]: ('color',)

In [10]: white_wall.__code__.co_freevars
Out[10]: ('paints',)

闭包实际上也是一种函数,但是会保留自由变量的绑定,这样在调用函数时,虽然定义作用域不可用了,但是这些绑定仍然能够使用。

闭包中的坑

上面刷墙的🌰中,自由变量是个可变类型的变量。但是当这个自由变量是个不可变类型的时候,比如数字、字符串、元组等,就掉进坑里了。我们把上个🌰稍作修改:

def wall():
    nums = 0
    oils = []

    def paint_wall(color):
        nums += 1
        oils.append(color)
        print("Paint wall by color: " + color)
        print("Paint wall {} times".format(nums))
        return oils

    return paint_wall


if __name__ == "__main__":
    white_wall = wall()
    white_wall("red")
    white_wall("yellow")

运行后出错:

Traceback (most recent call last):
  File "/Users/zww/.../decorator.py", line 28, in <module>
    white_wall("red")
  File "/Users/zww/.../decorator.py", line 17, in paint_wall
    nums += 1
UnboundLocalError: local variable 'nums' referenced before assignment

可以看到报错提示是赋值前引用了局部变量 nums 。这是因为闭包中定义的自由变量 nums 被赋值为 0, 这是不可变量,只能读取不能更新。而函数 paint_wallnums += 1 其实是重新绑定,会隐式创建了一个局部变量 nums,也就意味着这个时候 nums 不再是之前那个自由变量,自然不会保存在闭包里。

nonlocal 声明

为了从上面这种坑中跳出来,Python3 引入了 nonlocal 声明,作用是将变量标记为自由变量,也就意味着我们可以对做了 nonlocal 声明的变量进行修改。上面的🌰修改后如下:

def wall():
    nums = 0
    oils = []

    def paint_wall(color):
        nonlocal nums
        nums += 1
        oils.append(color)
        print("Paint wall by color: " + color)
        print("Paint wall {} times".format(nums))
        return oils

    return paint_wall


if __name__ == "__main__":
    white_wall = wall()
    white_wall("red")
    white_wall("yellow")

运行结果:

Paint wall by color: red
Paint wall 1 times
Paint wall by color: yellow
Paint wall 2 times

functools.wraps

理论上讲,被装饰后的函数在被调用后成了另一个函数。但是往往我们需要装饰器实现的作用是:在增强一部分逻辑的基础上,不改变原函数的属性和方法。而 Python 标准库中 functools.wraps 就是实现这个功能的。 wraps 把相关属性从原函数复制到装饰器中,并且能够正确处理关键字参数。

from functools import wraps


def oil(func):
    @wraps(func)
    def wall_func(*args, **kwargs):
        func(*args, **kwargs)

    return wall_func


@oil
def wall(**kwargs):
    color = kwargs["color"]
    print("paint wall to " + color)
    return color


if __name__ == "__main__":
    wall(color="red")
    print(wall.__name__)

运行结果:

paint wall to red
wall

带参数的装饰器

既然装饰器本质上就是一个函数,那装饰器也可以传参。那么具体应该怎么做呢?在 Python 中需要建立一个装饰器工厂函数,把参数传给它,再返回一个装饰器,然后应用到要装饰的函数上。

from functools import wraps


def oil_box(box_name):
    def oil(func):
        @wraps(func)
        def wall_func(*args, **kwargs):
            print("from box: " + box_name)
            func(*args, **kwargs)

        return wall_func

    return oil


@oil_box("iron box")
def wall(**kwargs):
    color = kwargs["color"]
    print("paint wall to: " + color)
    return color


if __name__ == "__main__":
    wall(color="red")

运行结果:

from box: iron box
paint wall to: red

虽然看上去 oil_box 是作为装饰器使用,但是它却不是真正意义上的装饰器,而是一个装饰器工厂函数,其返回的是装饰器。所以带参数的装饰器需要两层嵌套的结构才能实现。

单分派泛函数

在程序中,一种很常见的场景就是需要根据一个变量取不同值时,分别调用不同的函数或对象来适应不同的应用场景。比如我们上面刷墙的例子中,我需要根据输入变量的类型来做不同的事情,而 Python 不支持重载函数,所以 Python 中常用的方式将 oil 函数变成分派函数,用 if/elif/else 调用不同的函数,这样十分不利于代码管理。

Python 3.4 中新增的装饰器 functools.singledispatch 为这种情况提供了很便利的方案:把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数,而被 singledispatch 装饰的函数就成了泛函数,即根据第一个参数的类型,以不同方式执行相同操作的一组函数。我们使用这一方案来改装上面的🌰:

from functools import singledispatch


@singledispatch
def oil(obj):
    print("start painting !")
    return ""


@oil.register(str)
def _(color):
    print("paint wall to: " + color)
    return color


@oil.register(int)
def _(n):
    print("paint {} walls".format(n))
    return n


@oil.register(tuple)
def _(seq):
    print("paint walls in different directions: {}".format(seq))
    return seq


if __name__ == "__main__":
    oil("red")
    oil(2)
    oil(("north", "east"))

运行结果:

paint wall to: red
paint 2 walls
paint walls in different directions: ('north', 'east')

@singledispath 的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数,也就变相地实现了函数重载。

类实现装饰器

Python 中一切皆对象,那么装饰器也可以通过类来实现。而当我们需要在装饰器中实现一些比较复杂的逻辑的时候,函数明显不够用,用类实现是最好的选择。

用类来实现装饰器做法跟函数一样,同样是需要语法糖 @ ,然而 @ 是直接调用后面的对象,所以这里跟函数不同的是,需要实现类的 __init__ 方法来接受参数, __call__ 方法来实现调用。我们用类来重构上面刷墙的🌰。

from functools import wraps


class Oil_box:
    def __init__(self, box_name):
        self.box_name = box_name

    def oil(self):
        print("from box: " + self.box_name)

    def __call__(self, func):
        @wraps(func)
        def wall_func(*args, **kwargs):
            self.oil()
            func(*args, **kwargs)

        return wall_func


@Oil_box("iron box")
def wall(**kwargs):
    color = kwargs["color"]
    print("paint wall to: " + color)
    return color


if __name__ == "__main__":
    wall(color="red")

甚至还可以将类作为装饰器参数,只要实现不同的类,共用一套装饰器的逻辑,就能有更多的玩法。

from functools import wraps


class IronBox:
    box_name = "iron_box"

    @classmethod
    def paint(cls):
        print("from box: " + cls.box_name)


def oil_box(cls):
    def oil(func):
        @wraps(func)
        def wall_func(*args, **kwargs):
            cls.paint()
            func(*args, **kwargs)

        return wall_func

    return oil


@oil_box(IronBox)
def wall(**kwargs):
    color = kwargs["color"]
    print("paint wall to: " + color)
    return color


if __name__ == "__main__":
    wall(color="red")

上面这个🌰中需要注意的就是每个装饰器的类参数,都需要实现一个类函数 paint()

总结

到这里本文关于装饰器的内容就介绍完了。主要从装饰器、函数的变量作用域、闭包以及不同的方法实现装饰器等方面,对装饰器进行了介绍。灵活运用装饰器的不同实现方法,可以实现很多好玩的功能。

2018/06/01 11:17 上午 posted in  Python