本文档记录了写python代码时候的一些技巧,实践,心得等等。
组织的没有什么逻辑性,更多的就是看到好的整理记录的新式。
使用for…else…
python的for…else…结构可以方便的表示for如果不是break结束情况下的逻辑,对于那种for查找break的方式很pythonic
for x in range(1, 5): |
python的多重继承
class A(object): |
如上面代码,就是说,如果使用多继承的话,结构关系貌似就变为了(D->B->C->A)
的继承关系。。。
没有仔细研究,但是这样子其实有好处,就是B,和C的逻辑对于A而言都是独立的,但是如果组合使用B和C的关系的话,相同的函数可以通过super
调用的方式串联起来。
python好的资料收集
写了requests
库的大神的github非常值得推荐去看看。大神还写了一个关于python最佳时间的书籍,mark下。
for循环中,小心使用lambda
记录一个很隐藏的bug,在for循环中使用lambda需要小心:
cur_week_str = get_current_week_str() |
结果是什么,所有返回的lambda的callback中dst参数都是相同的,因为lambda其实只有一个,lambda是有闭包的,每次调用到这里,就将函数lambda中的闭包中参数更新(理解为有状态的成员对象被修改)。所以当异步callback调用的时候,还是相同的lambda,但是最后的dst参数全部都相同了。(还有一个本质原因是因为异步,如果同步,那么每次调用的都是最正确的参数)
解决方法,这里也可以看到查询的_id中用到了dst,返回的data参数一定是不同的,根据返回的data参数获取dst即可,如果实在不行 ,就想办法在一个更加全局的位置保存即可。
所以,本质上需要留意的就是lambda的非修改参数是保存状态的,而且(个人猜想)lambda不是每次都创建,一旦定义好了格式,就只有一个对象!!!,这个对象在for循环中持续改变!!!
python的输出
以前使用的输出方式是使用%
符合进行多个字符串的输出。另外一个好的方式(也是PEP3101)定义的使用format的格式:
foo = 'foo' |
最后一个的方式可以定位变量,这样子调整参数顺序也不会有问题。
python with
python一个非常有用的语法特性,就是可以将上下文进行管理,在出现错误的时候也可以保证上下文的正确析构。
有几种方式进行with语句的使用:
-
就是已经支持with语句的API,比如open。
-
自己实现,可以用类的方式,重载
__enter__
和__exit__
方法。class Context(object):
def __init__(self, name):
self.file = open(name)
def __enter__(self):
return self.file
def __exit__(self, ctx_type, ctx_value, ctx_traceback):
self.file.close()
with Context('xxx') as f:
pass调用with之后其实是创建了一个对象,这个对象构造之后通过
__enter__
方法返回给后面的as语句的变量,不论是否出现trace,都会再最后调用__exit__
-
另外一个方式就是直接构造一个执行上下文的代码,通过
yield
方法来切换执行流程import contextlib
def custom_open(filename):
try:
f = open(filename):
yield f
finally:
f.close()
with custom_open('file') as f:
pass上面代码通过
contextmanager
将一个普通的函数变为了一个管理上下文的函数,执行到yield的时候会将变量返回,然后切回执行环境到with里面的语句,最后执行结束都执行finally的代码。 -
还有一种简单方式,就是
context.closing
函数可以对有close
接口的API对象进行上下文管理.import contextlib
import urllib
with contextlib.closing(urllib.urlopen('http://xxx.org')) as page:
pass实现类似上面,对构造队形进行管理,然后
__exit__
的时候调用close方法。
总结下:
- 如果复杂的场景使用class方式。
- 简单的场景,使用contextmanager的方法更加直接。
- 如果更加简单,只是析构方法需要进行管理,那么都统一定义为close接口。
参考文档:
python gotcha (python一些诡异的坑)
python的默认构造参数
注意python的默认构造参数是在函数定义的时候进行构造,而不是每次执行都构造一个默认对象(这个和ruby不一样,可能是考虑到效率的问题)
所以一定要注意不要在python的默认参数中定义可变变量,这将非常的危险。
python函数的lazy binding closures
def create_multipliers(): |
上面的输出居然所有的i都是4,这是为什么呢?
这是因为python的函数闭包是延迟绑定的,也就是函数定义之后,只是定义了函数的实际执行体,而没有将对应闭包的参数绑定到闭包中(猜想也是为了保持效率,因为很多函数定义之后并不一定被执行)而是在执行的时候才进行绑定,所以这个时候i绑定都是4.
而且这个问题并不只是lambda的问题,而是所有的函数都这样子,即使使用def定义的函数,如下(也一样有问题):
def create_multipliers(): |
一个比较hack的解决方案就是,通过上面提到的构造默认参数的时候会构造出默认数据,所以修改为下面就ok了。
# 因为i=i会将默认值的i直接绑定到闭包中 |
或者使用系统的partial函数进行参数绑定:
def create_multipliers(): |
参考文档: common gotach from reitz python guide
python不输出pyc文件
如果非常讨厌pyc文件的话,可以定义变量,那么系统运行时候就不输出pyc了,nice:
export PYTHONDONTWRITEBYTECODE=1 |
python yield
python的yield也绝对是一个杀手级别的语法糖,虽然看起来非常的不直观。
有几个概念:
- 迭代器(iterator),python中的迭代器的概念是需要实现接口
next
,如果遇到没有数据的时候,返回raise StopIteration
。这样子的迭代器就可以和各种的系统接口进行融合。 - 生成器(generator),生成器其实就是保存了一些列序列的实例,通过
__iter__
接口得到访问序列的迭代器。(有时候大多数情况下generator和iterator可以混在一起来谈,因为generator也实现了next
迭代访问的接口)
所以一个基本的生成器构造方式如下:
class Iterator(object): |
所以for代码的执行流程就是:
- 通过for后面的生成器的
__iter__
得到一个迭代器。 - 然后不停调用
next
方式得到下一个数据,直到遇到StopIteration
异常
大体代码类似:
try: |
上面的代码还是显得非常麻烦,有没有方便的构造生成器的方式呢?有,就是yield关键字。
一个函数(generator funcion)如果定义了yield那么这个函数就是一个生成器函数,其返回值就是生成器,且接口兼容迭代的接口。 比如下面的代码:
def get_prime_start_from(start_num): |
生成器函数神奇的地方在于,可以:
- 执行函数逻辑,但是可以从函数执行处(yield)跳出到caller的地方。并且保存了++所有的上下文执行环境,局部变量**等信息
- caller执行,caller执行完成之后,可以通过
next
方式切换到刚才的函数中继续执行
生成器函数还有一种语法是send_v = yield return_v
,其逻辑是:
- 通过
generator.send(send_v)
的方式将数值幅值到send_v. - 同时上面的语句返回下一次yield运行的retrun_v
所以第一次运行的,需要下将代码运行到第一次yield所在的位置
一个例子:
import random |
所以概念上这个语法就是提供了修改生成器内部数据的外部(caller)修改方式。
比如上面的代码,其实consume就是一个不停消化数据的容器,每次调用一次next运行就消化一批数据(不一定yield就需要发挥数据,也可以不返回数据,而这样子更多是用来控制流程的概念),通过send的方式来给其喂数据。同理,对于produce而言,通过yield来实现一次次生成数据,发送数据的功能
需要注意的是,第一次启动的时候需要调用send(None)
, 否则开始的时候没有等待结果的yield
,从而导致报错。之后就可以调用send(xxx)
将上一次的yield
返回结果。
当然也可以调用next
,调用next等价于next = send(None)
就是说next
会得到yield的结果,但同时会告诉返回值是None,所以一路调用next
是肯定没有问题的(第一次就是需要None)
所以可以将生成器函数扩展理解为保存了所有状态的实例: 这个实例每次通过next切换到原环境中运行,运行结束后保存运行状态,等待下一次运行,并可以和caller交互数据。
扩展来看,生成器并不一定就是完全用来生成数据,而也可以被用来实现控制程序执行流程的逻辑。比如tornaod coroutine, 其概念就是执行一个异步的代码像写同步代码的方式。
比如下面的执行逻辑:
from tornado import gen |
fetch
代码是一个异步调用的代码,其返回值是一个叫做Future
的东西,这个东西被tornado的ioLoop进行管理,一旦完成调用,通过callback的方式通知完成体。
使用coroutine的方式将future和callback的方式写成了同步的方式,大致实现逻辑为:
# Simplified inner loop of tornado.gen.Runner |
流程大致是:
- 运行gen,得到yield结果,这个结果加入callback。
- yield结果运行完成之后,callback调用,将结果通过send发送会yield处继续运行。
- 循环,同理又运行结束,调用callback,继续下一个yield的运行,知道运行结束。
好NB的hook,直接将异步代码写成了同步的感觉,利用yield的切换运行逻辑和保存上下文的特性。
参考文档:
- stackoverflow - yield in python
- great blog - improve your python yield and generators explained
- 网易云阅读 - writing idiomatic python
- tornado - coroutines
python logging
python的log模块系统自带,用起来也非常的方便。log模块主要有四个类支撑:
- log
- handler
- filter
- format
log就是定义正常的log进行输出,而handler表示将log输出到什么位置,比如stream表示流,file表示文件等,filter表示输出的过滤规则,而format表示具体的输出数据的格式。几个类配合起来进行组合使用。
log是有层级关系的,最上层的log就是root,这是一个logging模块的模块变量,通过调用代码logger.getLogger()
得到,如果指定logger的名字就是logger.getLogger(log_name)
使用相同的log_name
就会得到相同的log,这点也是logging模块自己维护了一个log的映射dict实现的。
一个log的输出先经过子类的handler输出,之后再经过父类handler,依次传递。。名字可以体现父子关系,比如base
就是base.test
的父类。
这样的层级关系好处体现在:
- 层级的输出控制。
- 通过定义root的handler,等价于定义了最基本的handler,后面生成的log都可以复用这个handler,起了复用的作用。
简单的调用log的方式,就是直接使用logging模块提供的几个全局方法:
- basicConfig : 会对root log进行简单的定义。
- error/info/debug : 直接就是使用root log,同时如果root log没有定义的话,会使用basicConfig进行定义初始化。
注意的是,root默认的debug_level
是WARNING。
debug_level
有几个维度的定义方式:
- log层级,如果不符合log层级的log不会输出。
- 父log层级,如果自己没有定义log层级,那么会使用父类的,依次找上去,直到root的level。名字上面叫做
getEffectiveLevel
,表示得到有效的level信息,也就是一直沿着继承结构向上直到找到为止。 - handler层级,如果log层级符合,还需要符合handler层级,才会输出到handler中。比如log层级是info,所有info数据都会进入log后续系统,但是一个handler的层级(比如需要输出所有的错误信息到文件),那么就将handler的级别设置error,这样子info的数据都会输出,但是能到文件中的只有error级别数据。
filter的概念就比较清晰了,就是过滤当前的信息,可以加入到logger中,也可以放入到handler中。如果加入到logger中,那么所有从logger输出的log都需要经过filter,而handler就只针对进入到handler的log了。
python的logging模块有层级结构, 层级结构继承了父类的logLevel和handler, 具体是:
- 如果子类没有设置对应的logLevel, 那么得到父类的logLevel
- log输出的时候, 会先看下自己的level是否符合, 如果不符合什么都不干.
- 然后遍历所有的handler, 这包括自己的和父类的handler
- handler处理过程中会看下handler自己的logLevel是否符合(而不是看节点的, 如果不设置, 默认的话, 是NOTSET, 就是0)
但是filter机制是不继承的, 也就是说, filter机制和比较level的逻辑是一致的, 都是发送消息的时候:
- 先看下自己的filter, 如果有, 就过滤, 然后才会到handler
- 如果自己没有filter, 不会到父类中找父类的filter
- 父类的filter也只会用在check父类自己的log输出
- 但是handler的filter会用来handler自己的输出检测中, 也就是可以子类和父类公用(因为都会访问到这个handler)
出了手动配置logger的方式, 还可以通过配置文件, 或者是配置dict, 比如下面的代码使用dict进行配置
import logging |
参考文档:
python functools wraps
python的decorator是一个强大的东西,可以给一个既定的函数或者类进行修饰,达到「一键扩展」功能的效果。
但是装饰之后的函数(或者类)的很多原信息,比如__name__ __doc__ __module__
都变为了修饰之后的函数,而不是被修饰的原函数,这样子有时候会造成很多的奇异的地方。所有python系统库中引入了工具functools.wraps functools.update_wrapper
,这两个工具函数的作用就是将将修饰器(wrapper)的属性修改为被修饰的属性(wrappered),这样子修饰之后的东西看起来和原来一模一样:
from functools import wraps |
其实原理就是,wraps
调用了update_wrapper
,将wrapped的原信息都拷贝到wrapper中,从而实现看起来都一样的效果,贴一下源代码:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') |
参考文档:
metaclass
- 如果说类是object-factory的话,那么元类就是class-factory
- metaclass的话构造出来的是一个对象,所以metaclass中的所有方法都可以任务是class-object的类方法。
- 原来可以控制类的构造,如果需要对类生成的过程,成员函数进行控制。
class Meta(type): |
上面的代码有几个可以说明的地方:
- 类也是一个对象,类这个对象构造的实际是类定义结束的时候
- python定义类对象的时候,是看下当前是否有定义
__metaclass__
,如果有的话,使用该元类构造类(如果没有,找父类,还是没有,找当前moudle),如果没有,使用默认的元类type
- 别的过程就对照起来了,
__new__
方法是一个类方法,第一个参数是类本身,用来构造一个类对象,而__init__
方法是在生成了对象之后进行初始化,所以第一个对象是cls(也就是类自己) - 元类的其他方法,可以看成是成员方法,而成员就是构造出来的对象,也就是类对象本身,所以hook了
__call__
方法,就是hook了构造类成员的构造方法。
ORM的实现可能就需要使用metaclass,因为ORM需要根据当前类中的类变量定义知道当前类操作数据的格式要求,使用metaclass就可以将该格式要求转变为有效的后续操作。
几个参考文章
pythonic way
list的dict,按照dict的特定subkey进行排序
'a':1}, {'a':0}] a=[{ |
dict的key按照value进行排序
'a': 2, 'b': 1} d={ |
首先需要考虑,返回的结果是什么,这里是需要得到dict的key的排序,所以将d放到第一个参数,表示迭代器的源头。然后分析一下,如何如何进行比较,这里的方式使用key将比较的参数进行转化为对应的value进行比较。 所以实际上sorted的比较过程是将两个元素用key进行转换,然后再用cmp进行比较得到结果。
cmp(key(element_a), key(elemenet_b)) |
python collections
python的collections即可库中有几个有意思和使用的库。这里大概介绍一下
defaultdict
传入一个callable的对象作为工厂方法,当属性不存在的时候,使用工厂函数产生的对象作为默认的数值。
比如我需要一个dict的value是list记录相关的数据:
from collections import defaultdict |
namedtuple
namedtuple如同名字一样,就是tuple变成可通过dot.attr_name方式进行访问的结构。正常的tuple只能通过索引进行访问,但是如果tuple的数据比较长,索引的方式不直观。当然另外一种方式使用结构体,就是自己定义class,但是python的实现是每一个class结构体都有一个__dict__对象,not memory friendly。所以结合tuple内存高效和存储方便的特性,引入了nametuple类。
其实现原理是产生nametuple对象的时候(其实返回的是一个类对象,只是使用tuple基类和提供扩展接口)构造了一个类,使用模板的方式动态生成:
_class_template = '''\ |
我们通过代码生成一个Point对象的时候,就是动态的生成了一个内存优化的tuple,同时提供访问接口:
'Point', ['x', 'y']) Point = namedtuple( |
不过namedtuple的实现方式基于tuple,创建出来之后是immutable的。如果我们希望是mutable的,可以使用基于**__slots的实现方式,就是构造一个普通的对象类,利用__slots__的方式提高内存使用效率**. 这里别人已经实现类似接口的namedlist以及基于CPython实现的recordclass, 这里比较推荐使用namedlist库,基于python实现,简单高效。
参考文档:
- why-use-namedtuple
- mutable-namedtuple-version-implementation-from-C
- namelist-python-index
- about-python-slots
OrderDict
这个使用就广了,经常有需求是一方面使用dict的O(1)操作功能,一方面又有list的顺序记录功能。
仔细一想,如需实现,有一个list记录当前dict的所有key的传入顺序,同时删除时候,去掉对应key。系统实现的方式是:完全使用空间换时间,实现方式:
- 一个double-linked-list用来记录当前插入key信息
- 一个额外dict来记录对应key在list中位置,这样子删除的时候,可以O(1)找到对应list节点,进而执行删除操作。
贴一下python的实现,使用list作为可变节点的存储结构(虽然不够高效,但是也是比较简单的struct实现),使用sentinel的方式来简化算法的实现复杂程度
class OrderedDict(dict): |
python exception hierarchy
BaseException |