Sun 27 March 2016 by

Python yield小记

yeild

yield 一般搭配函数来定义一个Generator(生成器)

一个简单的例子:

def f():
    print "Today is 7.21"
    yield 6

f() 将会返回一个Generator, 而非像普通函数一样执行。想要使用生成器的话(比如 i)需要使用i.next() (与next(i)等效) 和i.send(value)。

>>> i = f()
>>> i
<generator object f at 0x1041e7dc0>
>>> i.next()
Today is 7.21
6
>>> i.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

生成器调next()或send()方法后会执行函数体内的语句,执行到yield语句后停止, 并返回yield语句的参数,上例中就是 6。再次使用next()或send()会从停止处继续执行,由于之后已经没有yield了,所以会raise StopTteration异常。

有时候可能会遇到这样的情况

def f():
    m = 5
    m = yield 6
    print "m = ", m

上例中 m 会得到(yield 6)这个式子的值。那么,这个值是多少呢?取决于i.next()和i.send(value)。如果是next,则是None。send的话就是value。

can't send non-None value to a just-started generator 第一次调用只能是 i.next()或者i.send(None)

这里需要注意一点,第一次next会在执行(yield 6)后停止,并不会执行m的赋值(重新绑定,毕竟python不太一样)。m的值是轮到下一个next或send来改变的。

>>> def f():
...     m = 5 
...     m = yield 6
...     print "m = ", m
... 
>>> i = f()
>>> i.next()
6
>>> i.send(7)
m =  7
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

使用

def fibonacci(i):
    """关于斐波那契数列的生成器
    """
    if i < 0:
        raise ValueError, i
    if i == 0:
        yield 0

    count = 0
    a, b = 0, 1
    while count <= i:
        yield a
        a, b = b, a + b
        count += 1

>>> for i in fibonacci(13):
...     print i,
... 
0 1 1 2 3 5 8 13 21 34 55 89 144
>>> for i in fibonacci(-1):
...     print i,
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "_yield.py", line 5, in fibonacci
    raise ValueError, i
ValueError: -1
>>> for i in fibonacci(0):
...     print i,
... 
0
>>> for i in fibonacci(1):
...     print i
... 
0 1

随便写了一个关于斐波拉切数列的生成器,并且遍历了它。

终止生成器

fib = fibonacci(6)
fib.close()
fib.throw(GeneratorExit)
# GeneratorExit 继承自 BaseException

使用这两种方法之后,再去访问生成器的方法,会触发StopIteration异常。遍历的话也不会返回有效值。

最后

关于yield暂时只想到这些,以后可能补充。生成器不一定还能通过别的方式定义。比如

(i for i in range(6))

This entry was tagged on #Python, #生成器 and #yield

Sun 27 March 2016 by

Python中一些容易被忽视的东西

旨在记录Python标准库中好像很厉害的模块或功能。

1. functools.partial(func[,args][, *keywords])
2. globals() and locals()

functools.partial(func[,args][, *keywords])

为func的参数指定默认值后返回一个新的函数。比如:

# disable print as the statement
from __future__ import print_function
from functools import partial
from StringIO import StringIO

f = StringIO.StringIO()
print = partial(print, file=f)
print("This will print to f)
# stdout will print nothing!
s = f.getvalue()
# s = "This will print to f"
f.close()

原本print函数有个file参数用来指定输出到的目标,例子中我们修改了file参数的默认值,从而重定向print到指定的file对象。虽然这个例子举得好像有点夸张了。

globals() and locals()

这两个函数与Python的namespace相关,比如一个函数有它自己的namespace,叫做local namespace,其中存放了只有他自己能够访问的变量和值,包括函数的参数以及在函数内部定义的变量。再比如一个模块有它自己的namespace,叫做global namespace,其中包括了该模块中定义的变量,常数,函数,类,导入的模块。除了这两个之外,还有一个叫build-in的namespace,包含了Python内置函数和异常。

Python解释器按照以下顺序寻找一个变量(或者函数,类等):

  1. local namespace
  2. global namespace
  3. build-in namespace

注:如果在这三个namespace都不能找到该变量,那么解释器就会抛出NameError: There is no variable named 'x'

globals()和locals()返回一个dict,该dict的值是变量名(函数名,类名等),dict的值就是对应得对象了。在最外层代码,也就是模块层面,locals()和globals()是相等的。

>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__doc__': None, '__package__': None}
>>> locals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__doc__': None, '__package__': None}

在Python的一个基于falcon构建的api框架hug中有一段代码动态创建变量的代码:

for method in HTTP_METHODS:
    method_handler = partial(http, accept=(method, ))
    method_handler.__doc__ = "Exposes a Python method externally as an HTTP {0} method".format(method.upper())
    globals()[method.lower()] = method_handler

大致作用是为每个http方法设置一个函数,并将该函数绑定到以该http方法为变量名的变量上。这样能动态创建以指定string为名字的变量名了。 相当于:

get = partial(http, accept=(get,))
get.__doc__ == "Exposes a Python method externally as an HTTP GET method"
# 然后post, put, patch...

This entry was tagged on #Python

Sun 27 March 2016 by

使用Python制作自定义终端命令

介绍

首先,拿pip举个例子,pip是我们使用较多的python包管理工具。当我们安装pip之后,直接在终端中就可以使用pip这个命令。那你有没有想过这是如何实现的?

其实,pip这个命令最终调用了python所在文件夹bin目录的pip文件。为什么说最终?因为使用系统默认的python安装pip时,可能会在/usr/local/bin或者/usr/bin下创建软链接。如果使用了virtualenv,那么pip文件就在env/bin下。那么pip文件到底是什么呢?答案就是,是一个具有执行权限的python文件,只不过去掉了.py的后缀.

也许你会兴高采烈的去尝试一发,在bin文件下创建xxx.py,打上一句 print “hello world”, 然后去掉.py的后缀, 再使用chmod a+x xxx来赋予它执行的权限。打开终端,敲下xxx。最后你只会得到print: command not found的错误提示。为什么?因为操作系统不知道这是一个py文件。思考一下,我们平时运行py文件都需要打上python xxx.py形式的命令,其实我们告诉了操作系统这是一个python文件(哪怕是有.py后缀),需要使用python解释器来解释运行该文件。现在,我们没有指定解释器了,自然就无法顺利的作为py脚本来运行了,它被错误的认为是shell脚本了。而shell编程中只有echo,没有print命令。

那么我们要做的就是在文件的第一行为该文件指定它的解释器(通过环境变量来获取当前使用的python解释器的路径):

#!/usr/bin/env python

这是一种常见写法,当然可以写成:

#!/usr/bin/python

这样,我们就为这个脚本指定了一个固定的解释器(它的路径是/usr/bin/python),可能有些时候你需要这样做。但是第一种更加灵活,尤其是在使用virtualenv创建的虚拟环境中,采用第二种写法的脚本将无法导入虚拟环境中安装的依赖包。

拓展

安装自己创建的python项目时,如何让安装工具自动在python目录的bin目录下创建自己的命令文件?在python2中,安装一个包,通常需要setup.py这个文件。大致写法如下:

     from setuptools import setup
     setup(
         name='project_name',
          version='0.1',
          ...
          entry_points={
            'console_scripts': [
               'hello = hello.main:main',
            ]
          }
     )

表示hello这个命令执行的是hello包下的main模块中的main函数.

实践

环境: python2.7 + mac osx 举个例子,我们现在有个project,目录如下

example/
- setup.py
- hello/
   _ __init__.py
   - main.py

其中, main.py是我们写的命令文件,内容如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

def main():
   print 'hello word'

setup.py的内容如下:

from setuptools import setup
setup(
     name='hello',
     version='0.1',
     entry_points={
         'console_scripts':[
            'hello = hello.main:main'
         ]
     }
)

在安装这个简单的项目之前,我们还需要修改一下hello文件到额权限: chmod a+x hello

接着安装, python setup.py install 或者 pip install . 这两种方式随便哪种都可以,安装完成后在终端中输入hello命令试试,有没有显示hello world?

结尾

说了这么多,提醒一点,并不是所有的python包都是通过上述方式来提供终端命令的,不过这是一种很普遍很简便的方式!

This entry was tagged on #Python

Sun 27 March 2016 by

supervisor+celery+celerybeat的简单使用

1. 提前准备

略去相关工具的安装过程,其实都挺简单的!

celery作为异步任务队列, 需要一个中间人来协助celery存放和消耗任务信息。我们选择rabbitmq来做消息代理人。使用celery之前, 需要使用创建一个rabbitmq的管理员账号和一个能让该账号访问的vhost.

Rabbitmq的安装配置以及网页管理插件

假设准备的rabbitmq的信息如下:

SETTINGS = {
    'user': 'www-data',
    'password': 'www-data',
    'host': '127.0.0.1',
    'port': '5672',
    'vhost': 't_celery'
}

示例项目结构

test.celery/
    |- env/
    |- src/
        |- __init__.py
        |- app.py
        |- task.py

2. celery实例及任务

2.1 生成实例

# -*- coding: utf-8 -*-

# filename: app.py

from celery import Celery

CELERY_CONFIG = {
    'CELERY_TIMEZONE': 'Asia/Shanghai',
    'CELERY_ENABLE_UTC': True,
    # content
    'CELERY_TASK_SERIALIZER': 'json',
    'CELERY_RESULT_SERIALIZER': 'json',
    'CELERY_ACCEPT_CONTENT': ['json'],
    'CELERYD_MAX_TASKS_PER_CHILD': 1,
}

# 第一个参数是实例的名称,  也可以使用模块的名字.
# broker参数是消息代理人url.
# 还有一个backend参数,当我们需要拿到异步任务的返回时需要用到.
# 这里就直接略过了.
app = Celery(
    'test_celery',
    broker='amqp://{user}:{password}@{host}:{port}/{vhost}'.format(
        **SETTINGS)
)
app.conf.update(**CELERY_CONFIG)

2.2 定义Task

# -*- coding: utf-8 -*-

# filename: task.py

from src.app import app

@app.task(queue='test_celey_queue')
def add(x, y, c=0):
    return x + y + c

注意, 我们给app.task这个装饰器传了queue这个参数, 这样当异步执行的时候,这个task会被丢到名称为test_celery_queue的队列中, 然后被为这个队列工作的worker拿到并执行。当然, 我们也可以在CELERY_CONFIG中配置:

CELERY_ROUTES = {
     'src.task.add': 'test_celery_queue',
}

如果我们不指定queue的话,celery会默认自己指定一个队列。task的队列一定要对应的worke, 否者就会只生产不消费, 这些task就永远不会被执行了。

2.3 启动worker

我们需要在项目路径下,也就是test.celery文件下运行python解释器, 否者python解释器无法找到 src 这个包。 或者直接将项目路径添加到PYTHONPATH变量,就像这样export PYTHONPATH=/data/test.celery

然后再启动worker:

celery worker -A src.task --loglevel=info -Q test_celery_queue -f /data/test.celery/celery.log
参数解析
    • A src.task 指定celery实例, celery会到src.task中找app实例
  1. worker 告知celery要启动worker.
  2. --loglevel=info 日志的级别是info.
  3. -Q test_celery_queue 为该worker指定一个消息队列, worker只取该队列中的任务。可以指定多个队列.
  4. -f 日志文件输出位置
  5. -P

更多可配置参数s通过 celery worker -h 查看。

2.4 验证正确性

在一个终端显示日志
tail -f /data/test.celery/celery.log
另一个终端启动python的交互解释器
注:如果没有设置PYTHONPATH, 那就要在我们的项目文件夹下启动
>>> from src.task import add
>>> add(4, 6)
10
>>> add.delay()
<AsyncResult: fe9b4b75-6ba8-44aa-adb2-92d63c8ba1c6>
>>> add.delay(4, 4)
<AsyncResult: e6dfcdbb-7a19-4fb2-aa5e-841b8f393874>
>>> add.apply_async(args=(3,5))
<AsyncResult: 34cf6a59-35db-40d3-a218-6703eb04336b>

如果一切正常的话,运行第二、三、四条命令的时候会看到有日志输出。而且第二条命令会有错误日志,提示缺少参数。

用法说明

如果直接调用某一个task, 那么该task就跟普通的函数一样, 会同步运行并直接返回结果。想要异步执行, 就要使用 delay 或者 apply_async

  • delay: 相对简单,怎么给add传参,就怎么传给它。比如:

add.delay(4, 5, 9) 或者 add.delay(4 , 5, c=19)等.

  • apply_async: 列举几个常用的参数:
  * args: 传给task的参数.
  * kwargs: 传给task的参数.
  * countdown 延时执行的秒数.countdown=10表示10s后执行.
  * eta: datetime类型, 执行的日期.
  * queue: 指定任务消息要去的队列.
  * ...

3. celery beat

我们以定时发邮件为例子

3.1 添加发邮件的task

在task.py的基础上修改:

# -*- coding: utf-8 -*-

from src.app import app
from posts import Posts
import time

@app.task(queue='test_celery_queue')
def add(x, y, c=0):
    return x + y + c

@app.task
def sendmail():
    post = Posts('smtp.qq.com', 'emailname@qq.com', 'password', port=465)
    with post(ssl=True) as mail:
        mail.text(
            recipient ='receive@163.com,
            subject='send by celery beat',
            content=time.ctime()
        )

由于Python标准库发邮件有点繁琐,我这里使用了自己简单封装的posts来发送邮件。邮件的内容就是执行任务的时间。

3.2 配置定时任务的schedule

# -*- coding: utf-8 -*-

from celery import Celery
from celery.schedules import crontab
from datetime import timedelta

CELERYBEAT_SCHEDULE = {
    'send-email-every-30-minutes': {    # 定时任务的名字
        'task': 'src.task.sendmail',     # 具体对应的Task
        'schedule':  timedelta(seconds=30),# 定时设置,这里表示30s执行一次
        # 'args': () ,       # 传给Task的参数
        'options': {     # 设置Task的一些属性, 参见apply_async的参数
            'queue':'test_celery_queue'
        }
    }
}
CELERY_CONFIG = {
    'CELERY_TIMEZONE': 'Asia/Shanghai',
    'CELERY_ENABLE_UTC': True,
    # content
    'CELERY_TASK_SERIALIZER': 'json',
    'CELERY_RESULT_SERIALIZER': 'json',
    'CELERY_ACCEPT_CONTENT': ['json'],
    'CELERYD_MAX_TASKS_PER_CHILD': 1,
    'CELERYBEAT_SCHEDULE': CELERYBEAT_SCHEDULE     # 启动beat,传入相关参数.
}
SETTINGS = {
    'user': 'www-data',
    'password': 'www-data',
    'host': '127.0.0.1',
    'port': '5672',
    'vhost': 't_celery'
}

app = Celery(
    'test_celery',
    broker='amqp://{user}:{password}@{host}:{port}/{vhost}'.format(
        **SETTINGS)
)
app.conf.update(**CELERY_CONFIG)

参数 schedule 说明, 通常使用两种方式来指定:

  1. datetime.timedelta: 用这种方式来指定 秒 级别.
  2. celery.shedules.crontab: 用这种方式指定 分钟 以上级别.
Example 表示
timedelta(seconds=30) 30秒执行一次
crontab() 每分钟执行一次
crontab(minute=0, hour=0) 凌晨执行一次
crontab(minute=0, hour='*/3') 从凌晨开始。每三个小时执行一次
crontab(minute=0,hour='0,3,6,9,12,15,18,21') 同上
crontab(minute='*/15') 每15分钟执行一次
crontab(day_of_week='sunday') 在周日,每分钟执行一次
crontab(minute='',hour='', day_of_week='sun') 同上
crontab(minute=0, hour='*/5') 每5小时执行一次,0,5,10,15,20
crontab(0, 0, day_of_month='2') 每个月第二天执行一次
crontab(0, 0, day_of_month='1-7,15-21') 每月的1-7号和15-21号执行
crontab(0, 0, day_of_month='11', month_of_year='5') 5月份的11号执行一次
crontab(0, 0,month_of_year='*/3') 每个季度的第一个月执行一次

更多用法详见 官方文档

3.3 启动celery beat

celery beat -A src.app --loglevel=info --logfile=/data/test.celery/celery.beat.log

更多可配置参数通过 celery beat -h 查看。

3.4 验证正确性

一个页面显示celery beat 的输入日志
tail -f /data/test.celery/celery.beat.log
一个终端页面显示 celery worker 输出日志
tail -f /data/test.celery/celery.log

如果分别能看到产生和消耗任务的日志输出。那就成功了。

4. supervisor

4.1 (supervisord)配置并启动supervisor

启动supervisor的命令是 supervisord。我们可以使用 -c 参数来指定其配置文件的位置。比如:

supervisord -c supervisord.conf

supervisor的配置文件 supervisord.conf:

[unix_http_server]
file=/var/run/supervisor.sock                   ; (the path to the socket file)

[supervisord]
logfile=/data/log/supervisor/syslog         ; (main log file;default $CWD/supervisord.log)
loglevel=info                               ; (log level;default info; others: debug,warn,trace)
pidfile=/var/run/supervisord.pid                ; (supervisord pidfile;default supervisord.pid)
nodaemon=false                              ; (start in foreground if true;default false)
minfds=1024                               ; (min. avail startup file descriptors;default 1024)
minprocs=1024                             ; (min. avail process descriptors;default 200)
nocleanup=false
umask=022
user=root

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock   ; use a unix:// URL  for a unix socket

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[include]
files = /etc/supervisord.d/*.ini

4.2 配置具体项目

我们会为不同的程序编写独立的ini配置文件,然后放置到一个统一的路径(比如 /etc/supervisord.d/)下让supervisor读取.具体项目的配置文件 test.celery.ini 如下

[group:test_celery]
programs = test_celery.async,test_celery.beat

[program:test_celery.async]
command=/data/test.celery/env/bin/celery worker -A src.app --loglevel=info -Q test_celery_queue
numprocs=1
numprocs_start=0
priority=999
autostart=true
startsecs=3
startretries=3
exitcodes=0,2
stopsignal=QUIT
stopwaitsecs=60
directory=/data/test.celery
user=www-data
stopasgroup=false
killasgroup=false
redirect_stderr=true
stdout_logfile=/data/log/test.celery/test_celery.log
stdout_logfile_maxbytes=250MB
stdout_logfile_backups=10
stderr_logfile=/data/log/test.celery/test_celery.err
stderr_logfile_maxbytes=250MB
stderr_logfile_backups=10
environment=PYTHONPATH='/data/test.celery/';C_FORCE_ROOT="true"

[program:test_celery.beat]
command=/data/test.celery/env/bin/celery beat -A src.app --loglevel=info
numprocs=1
numprocs_start=0
priority=999
autostart=true
startsecs=3
startretries=3
exitcodes=0,2
stopsignal=QUIT
stopwaitsecs=60
directory=/data/test.celery
user=www-data
stopasgroup=false
killasgroup=false
redirect_stderr=true
stdout_logfile=/data/log/test.celery/test_celery.beat.log
stdout_logfile_maxbytes=250MB
stdout_logfile_backups=10
stderr_logfile=/data/log/test.celery/test_celery.beat.err
stderr_logfile_maxbytes=250MB
stderr_logfile_backups=10
environment=PYTHONPATH='/data/test.celery/';C_FORCE_ROOT="true"

简单说明

  • test_celery.asynctest_celery.beat 是两个program,分别对应worker和beat,而它们又同属于 test_celery 这个组,这样便于同时管理。
  • 注意到command参数了吗?因为我使用了virtualenv来隔离每个项目的包环境,所以需要明确指出 celery命令所在的目录(如果全局安装了celery就必要了), 这也是为什么上文项目结构中会有一个 env 文件夹的原因。
  • 使用了虚拟环境之后, 通常需要先激活环境 source /data/test.celery/env/bin/activate
  • environment 下设置 PYTHONPATH 我们在上文中提到过。总之,你要让Python知道你项目包得位置,设置PYTHONPATH 只是一种方式。

4.3 (supervisorctl) 管理程序进程

通过 sudo supervisorctl 可以进入管理客户端。我们可以使用各种命令管理程序的进程:

常用的命令有:

  • status \ 查看状态
  • restart \ 重新启动
  • start \ 启动
  • stop \ 停止
  • update 更新配置

也可以不进入来管理 ,例如每次更新玩配置,我们可以使其快速生效:

sudo supervisorctl update 

5. 结束语

上文中只是简单介绍了supervisor 、celery、 celerybeat的简单运用。如果有兴趣的话可以自己去深入了解!

本篇文章到此结束,原创手打!

人生苦短, 我用python!

This entry was tagged on #celery, #Python and #supervisor

Sun 27 March 2016 by

终端进度条的简单原理

进度条

每次使用pip安装一些第三方库时,总会在终端或者命令行中看到进度条,能够让用户等待时得到当前任务的进度。今天偶然在知乎看到一个帖子中提到了这个有趣的功能。

实现方式

一般情况下命令行和终端的输出总是由上往下一行一行输出。如果我们需要实现进度条的效果的效果的话,就需要每次输出都在同一行上,不断地用后来的输出去覆盖之前的输出,从而模拟出一种动态变化的效果。那究竟怎样实现呢?答案就是使用反杠r

反杠n是换行,反杠r就只是把输出光标移到行首,而不换行。根据这个思路,我们先做个倒计时来玩玩。

import time
import sys

def CountDown(total_sec):
    for i in xrange(10, -1, -1):
        # 不能换行哈
        print '%4d\\r' % i,
        sys.stdout.flush()
        time.sleep(1)
    # 防止输出被之后的输出覆盖
    print

CountDown(10)

结果让我失望了,因为倒计时效果没有出现,而是等待10秒后直接出现0

为什么?

输出是有缓存机制的,输出缓存(即所谓的行缓冲区)是根据换行符来写数据的,看到换行符,就会从缓存打印数据。

修改代码前,先看下命令行和终端中的输出方式:

# print方式
print 'ProcessBar'

# stdout方式
import sys
sys.stdout.write('ProcessBar\\n')

print使用的较为普遍,但以上两种方式其实是等价的,无非是print自带默认自带换行。我们可以借助sys模块中stdout中的flush强制取出缓冲区的内容。

import time
import sys

def CountDown(total_sec):
    for i in xrange(10, -1, -1):
        # 默认不换行
        # 也可以 print '%4d\\r' % i,
        sys.stdout.write('%4d\\r' % i)
        # 强制打印
        sys.stdout.flush()
        time.sleep(1)
    # 防止最后结果被之后的输出覆盖
    print

CountDown(10)

成功实现。接下来来实现进度条!

#!/usr/bin/env python
#-*-coding=utf-8-*-

import sys

class ProcessBar(object):

    def __init__(self, end, char='#'):
        """
        @param end : 总大小
        @param char : 进度条的符号
        """
        self.end = end
        self.char = char

    def show(self, count):
        """
        @param count : 当前进度
        """
        # 百分比
        percent = count * 100 / self.end
        # 符号数量
        s_num = percent / 10
        sys.stdout.write('%3d%%=>|%-10s|\\r' % (percent, self.char * s_num))
        sys.stdout.flush()

if __name__ == '__main__':
    import time
    pb = ProcessBar(100)
    for i in xrange(1, 101):
        pb.show(i)
        time.sleep(0.1)
    print "\\ncompleted"

一个简单的进度条做好了,感觉还不错!

还有一种方法那就是使用反杠b来回退,有空试试。

扩展

每次输出都需要flush感觉很麻烦? 几种方式轻松解决!

  1. python -u ProcessBar.py 带参数u来运行文件
  2. PYTHONUNBUFFERED=1 python ProcessBar.py 与方法一等效
  3. #!/usr/bin/env python -u linux、unix需要直接运行的方式的话,可以在py文件开头加上-u
  4. 使用stderr.write(str)输出,因为stderr不使用缓冲区

This entry was tagged on #Python

Sat 26 March 2016 by

Python单层装饰器小记

解释器什么时候处理装饰器?

答: Python解释器加载代码的时候.

例如, sleep.py 代码如下

import functools
import time


def timer(func):
    print("handle decorator")

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        rtn = func(*args, **kwargs)
        cost = time.time() - start
        print("cost %f s") % cost
        return rtn
    return wrapper


@timer
def sleep(seconds):
    time.sleep(seconds)
    return seconds

以上代码只是定义了一个装饰器和一个使用该装饰器的函数,当运行python sleep.py的时候会打印hanle decorator。当解释器在加载处理代码的时候遇到了"@"的时候,回去调用timer这个函数,并把sleep这个参数当做参数传给timer。这个过程相当于:

def sleep(seconds):
    time.sleep(seconds)
    return seconds
sleep = timer(sleep)

装饰器和闭包?

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

我们不去深究到底什么才是闭包的正确定义。姑且认为就是引用了自由变量的函数。如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包。粗暴的理解为函数套函数,内层函数引用了外层函数的变量,即使外层函数结束了,内层函数函数仍可以访问该变量

在装饰器的用法中,表现为如下方式:

import functools
import time


def timer(func):
    print("handle decorator")
    name = func.func_name

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print name, id(name)
        start = time.time()
        rtn = func(*args, **kwargs)
        cost = time.time() - start
        print("costs %f s") % cost
        return rtn
    return wrapper


@timer
def sleep(seconds):
    time.sleep(seconds)
    return seconds


@timer
def sleep2(seconds):
    time.sleep(seconds)
    return seconds

if __name__ == '__main__':
    print sleep(2)
    print sleep(3)
    print sleep2(2)
    print sleep2(3)

# 运行结果:
handle decorator
handle decorator
# print sleep(2)
sleep 4402047952
costs 2.005124 s
2
# print sleep(3)
sleep 4402047952
costs 3.005203 s
3
# print sleep2(2)
sleep2 4402048048
costs 2.005205 s
2
# print sleep2(3)
sleep2 4402048048
costs 3.000412 s
3

可以看到:

  1. sleep是可以访问timer函数中的name变量。
  2. 同一个函数的多次调用,name变量是同一个(id相同)。
  3. 函数sleepsleep2所引用的name变量不是同一个,因为timer装饰这两个函数,被调用了两次,声明了两个不同的name变量。

利用以上三点特性,我们可以在装饰器中为被装饰函数开辟一个"存放数据空间",来看一种应用方式:

import functools
import time


def cache(func):
    caches = {}

    @functools.wraps(func)
    def wrapper(n):
        if n not in caches:
            caches[n] = func(n)
        return caches[n]
    return wrapper


@cache
def fib(n):
    if n < 2:
        return 1
    return fib(n - 1) + fib(n - 2)


def fib2(n):
    if n < 2:
        return 1
    return fib2(n - 1) + fib2(n - 2)


def fib3(n):
    a, b = 1, 1
    for i in xrange(1, n):
        a, b = a + b, a
    return a

if __name__ == "__main__":
    start = time.time()
    for _ in xrange(10000):
        fib(20)
    print "cache fib without cache spends %f s" % (time.time() - start,)

    start = time.time()
    for _ in xrange(10000):
        fib2(20)
    print "no cache fib spends %f s" % (time.time() - start,)

    start = time.time()
    for _ in xrange(10000):
        fib3(20)
    print "fib3 spends %f s" % (time.time() - start,)

# 运行结果
cache fib without cache spends 0.003719 s
no cache fib spends 31.374117 s
fib3 spends 0.016529 s

递归通常比非递归实现对程序执行的效率影响更大一些,我们带缓冲的递归fib却比非递归版本的fib3耗时更少!

类装饰器

我们在创建一个class的时候可以选择重写以下三个方法:

  • __new__(cls, args, *kwargs) 创建实例
  • __init__(self, args, *kwargs) 初始化实例
  • __call__(self, args, *kwargs) 把实例当函数一样调用
import functools
import time


class Timer(object):

    def __init__(self):
        self.names = []

    def __call__(self, func):
        if func.func_name not in self.names:
            self.names.append(func.func_name)

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print self.names
            start = time.time()
            rtn = func(*args, **kwargs)
            print "%s spends %f s" % (func.func_name, time.time() - start)
            return rtn
        return wrapper

timer = Timer()


@timer
def sleep(seconds):
    time.sleep(seconds)
    return seconds


@timer
def sleep2(seconds):
    time.sleep(seconds)
    return seconds

if __name__ == '__main__':
    sleep(5)
    sleep2(5)

 运行结果
['sleep', 'sleep2']
sleep spends 5.000244 s
['sleep', 'sleep2']
sleep2 spends 5.004623 s

想要使用类来做装饰器,先要准备一个实例,然后调用这个实例。除了这点要注意外,还要注意的是,用类实现的装饰器,可以让所有被装饰的函数共享变量,这种变量就是实例的内部变量。

This entry was tagged on #Python and #装饰器

Sat 26 March 2016 by

Python的metaclass小记

使用metaclass的时候可以使用metaclass__new____init__去改变所定义的类的相关属性,因为这两个方法是在类定义的时候被调用,他们所需的参数也与使用type(name, bases, dict)动态声明类相同。而使用metaclass__call__可以影响我们所声明的类产生实例的过程,因为当我们使用class_name()去产生实例的时候,相当于call了这个class,而这个class又是metaclass的实例,所以其实调用了metaclass的__call__

注意: 通过super来调用父类的__new__时,第一个参数需要是该class,这点是不同于调用__init__等其他父类方法的。因为__new__是个静态方法。还有一个小问题就是当父类是object的时候,也就是调用object.__new__()只传一个cls参数就可以了,至于为什么,请戳herehere。 如果你重写了__new__而没有重写__init__,那么给object.__new__传额外的参数会直接报错, 如果两个都重写了会报出DeprecationWarning,可以继续运行。

class Meta(type):

    def __new__(meta, cls_name, base_clses, cls_attrs):
        print "============Meta_new=============="
        print "meta", meta
        print "class name", cls_name
        print "base classes", base_clses
        print "class attrs", cls_attrs
        instance = super(Meta, meta).__new__(meta, cls_name, base_clses, cls_attrs)
        print "=============Meta_new============"
        return instance

    def __init__(cls_obj, cls_name, base_clses, cls_attrs):
        print "============Meta_init============"
        print 'class obj', cls_obj
        print "class name", cls_name
        print "base classes", base_clses
        print "class attrs", cls_attrs
        print "============Meta_init==========="

    def __call__(cls_obj,  *args, **kwargs):
        print "=============Meta_call=========="
        print "class obj", cls_obj
        instance = super(Meta, cls_obj).__call__(*args, **kwargs)
        print "=============Meta_call=========="
        return instance


class Foo(object):
    __metaclass__ = Meta

    def __new__(cls):
        print cls
        print 'Foo.__new__'
        return super(Foo, cls).__new__(cls)

    def __init__(self):
        print 'Foo.__init__'

if __name__ == '__main__':
    Foo()
    print type.__call__(Foo)

输出

============Meta_new==============
meta <class '__main__.Meta'>
class name Foo
base classes (<type 'object'>,)
class attrs {'__module__': '__main__', '__metaclass__': <class '__main__.Meta'>, '__new__': <function __new__ at 0x1091ad578>, '__init__': <function __init__ at 0x1091b9578>}
=============Meta_new============
============Meta_init============
class obj <class '__main__.Foo'>
class name Foo
base classes (<type 'object'>,)
class attrs {'__module__': '__main__', '__metaclass__': <class '__main__.Meta'>, '__new__': <function __new__ at 0x1091ad578>, '__init__': <function __init__ at 0x1091b9578>}
============Meta_init===========
=============Meta_call==========
class obj <class '__main__.Foo'>
<class '__main__.Foo'>
Foo.__new__
Foo.__init__
=============Meta_call==========
<class '__main__.Foo'>
Foo.__new__
Foo.__init__
<__main__.Foo object at 0x1091b6910>

This entry was tagged on #metaclass and #Python

Sat 26 March 2016 by

升级CentOS的Python版本

服务器(centos)上默认的python版本是python2.6,但是平时使用的是2.7,故手动升级一下吧。

下载最新版本Python-2.7.10

wget https://www.python.org/ftp/python/2.7.10/Python-2.7.10.tgz

tar -zxvf Python-2.7.10.tgz

提前安装依赖

yum install zlib
yum install zlib-devel
yum install openssl -y
yum install openssl-devel -y

配置编译安装替换

cd Python-2.7.10
./configure --prefix=/usr/local/python2.7

修改配置

cd Python-2.7.10
vim Modules/Setup
去掉456行左右
#zlib zlibmodule.c -I$(prefix)/include -L$(exec_prefix)/lib -lz的注释

Go on...

make
make install
rm /usr/bin/python
ln -s /usr/local/bin/python2.7 /usr/bin/python

修复yum

vim /usr/bin/yum
将
#!/usr/bin/python
替换为
#!/usr/bin/python2.6(根据版本而定)

安装pip

wget https://bootstrap.pypa.io/get-pip.py
python get-pip.py

这里比较坑的一点是,以后安装的带有命令行命令的第三方库都不能直接在终端通过命令调用,而是需要使用绝对路径或者在/usr/bin中创建链接才能正常使用。关于这点我暂时还不知道如何优雅的解决它。

This entry was tagged on #CentOS, #Python and #运维

« Page 3 / 3

 

Tags