Nisen

Nisen 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 hackingx.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Nisen 关注了用户 · 2018-05-18

WalkerQiao @walkerqiao

专注前端开发。个人博客地址: http://www.mh611.com/blog http://joescott.coding.me/blog 。 两个博客内容相同,均为个人学习记录, 欢迎有兴趣的一起探讨前端技术。

关注 125

Nisen 发布了文章 · 2018-04-28

Python数据模型构建和迁移方案:SQLAlchemy&Alembic

背景

Python的世界里有许多web框架:比如大而全的 Django, 提供了模型定义迁移,到路由处理,再到视图的渲染等整套功能;比如小巧灵活的Flask, 虽然只包含了核心的请求处理内容,但却可以通过安装生态丰富的插件来完成大多数所需功能;比如面向ERP行业的Odoo, 除了基础的MVC, 还提供了常用的进销存和人力资源等模块以及方便的web数据管理界面;比如以异步IO为特色的SanicTonado, 提供了一套基于异步IO的请求处理方案;还有其他Bottle, Cherrypy, Pyramid 等。

这么多web框架其中一类是全套web解决方案的,像django,pyramid,odoo等,一类是提供路由和请求处理的"api"微型框架,像flask, sanic, bottle, cherrpy等。当使用到后者这类微型框架时,根据业务场景不同,如果需要处理模型的建立、升级和迁移的问题,可以考虑下接下来要介绍的sqlalchemyAlembic

SQLAlchemy是python里的处理模ORM(模型关系映射)一套工具,可以通过直观地通过定义python中的class来定义数据表结构,通过操作class的具体object来操作数据记录。 Alembic是一套管理数据库升降级的迁移工具,比如在实际业务场景中需要对已经定义好的模型进行增删字段操作,可以通过alembic来对升降级进行方便地可控地操作。

SQLAlchemy和alembic的安装和详细配置可以参考官方文档,这里我通过一个示例来说明如何实现model的定义和迁移。代码地址在这里

初始化和配置

安装python依赖(主要是SQLAlchemy和alembic):

pip install -r requirements.txt

初始化alembic:

alembic init YOUR_ALEMBIC_DIR

alembic会在根目录创建 YOUR_ALEMBIC_DIR 目录和 alembic.ini 文件,
所以在我的示例代码里, alembic_diralembic.ini 是运行 alembic init alembic_dir 初始化创建的。

alembic.ini 文件 提供了一些基本的配置,比如数据库的连接选项。

alembic_dir 的目录结构和作用为:

$ tree alembic_dir

alembic_dir
├── README
├── env.py          # 每次执行Alembic都会加载这个模块,主要提供项目Sqlalchemy Model 的连接
├── script.py.mako  # 迁移脚本生成模版
└── versions        # 存放生成的迁移脚本目录

1 directory, 4 files

接下来先来看看sqlalchemy里model的定义,在model目录里:

$ tree
.
├── __init__.py # 打包成一个模块
├── base.py     # 定义所有模型继承的Base类
├── role.py     # 定义“角色”模型
└── user.py     # 定义“用户”模型

0 directories, 4 files

其中,sqlalchemy的模型类继承自一个由 declarative_base() 方法生成的类,所以在 base.py 里有如下两行代码:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

模型定义完成后,我们需要告诉alembic去哪里找寻模型定义,所以在 alembic_dir/env.py 的21行左右可以看到指定:

# Custmosized
import os
import sys
# 将当前目录(项目更目录)加入sys.path, 当然也可以将根目录下的model加入sys.path,这样就不需要将model封装成模块
sys.path.append(os.getcwd())
from model import Base
target_metadata = Base.metadata

另外通常我们也改一下生成模板 script.py.mako ,加上编码信息,否则在升级脚本中如果有中文会报错,参见 alembic_dir/script.py.mako 的前两行。

接下来需要配置alembic连接管理那个数据库,在 alembic.ini 的第38行修改数据库连接选项,这里代码中采用本地的mysql为示例:

sqlalchemy.url = mysql://root:@localhost/test2

运行

配置工作做完后,确保本地mysql服务启动,并且有上面配置的数据库后,让我们来生成第一份迁移脚本, 在 sqlalchemy-alembic 目录下运行:

# 其中 "First create user add role table" 是这次迁移脚本的备注,类似git commit的message
alembic revision --autogenerate -m "First create user add role table"

运行完命令后,会发现在 sqlalchemy-alembic/alembic_dir/versions 下生成了一个迁移脚本,迁移脚本的主体是:

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('roles',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('user',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('username', sa.VARCHAR(length=32), nullable=True),
    sa.Column('password', sa.VARCHAR(length=32), nullable=True),
    sa.Column('email', sa.VARCHAR(length=32), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index('ix_email_pwd', 'user', ['email', 'password'], unique=False)
    op.create_index('ix_user_pwd', 'user', ['username', 'password'], unique=False)
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_user_pwd', table_name='user')
    op.drop_index('ix_email_pwd', table_name='user')
    op.drop_table('user')
    op.drop_table('roles')
    # ### end Alembic commands ###

可以发现,是根据model定义的内容,自带生成的升级和降级代码,实际项目中需要检查一下升降级脚本是否有误。

接下来,在项目根目录下运行升级命令:

$ alembic upgrade head

INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 8deb154aaaa3, First create user add role table

其中 head 表示升级到迁移脚本中最新的版本。

这时候检查数据库,可以发现生成了3张表,升级工作就完成了。

alembic_version  # alembic用来追踪目前数据库表的版本的表,表的内容只有一行,为当前版本号,对应于升级脚本上的版本号
roles            # 自动生成的表
user             # 自动生成的表

接下来,如果对模型有其它改动,比如新增字段等,可以再次生成迁移脚本,检查无误后运行upgrade完成迁移动作。

其它

  • alembic的迁移脚本也是可以自己手写的,这样不需要配置 env.py 里的 target_metadata ,每次迁移做的事完全有手动来决定对数据库操作。
  • alembic的降级可以用类似 alembic downgrade -1 的命令, -1 代表降级到上个版本,也支持其他参数,具体可以查询文档。
  • sqlalchemy 可以以ORM的方式在业务逻辑处理的时候引用,这样每次查询到一条或多条数据,就可以得到一个或多个对象,类似于django的model。
  • 在使用类似sanic这样的异步框架时,需要注意orm的选取,是否需要一个异步的orm框架呢,可以考虑的异步orm可以参照这里 ,另外sqlalchemy的作者在15年对此有思考,这篇文章 可以看看。而我在使用sanic的实际项目中是操作的原生sql,异步io类型的orm配合使用留待以后探究。
查看原文

赞 2 收藏 2 评论 0

Nisen 发布了文章 · 2018-03-20

搭建Docker私有仓库

摘要

这篇文章内容包括搭建docker私有仓库的一些配置项和遇到的问题及解决方案。

1.配置项
1.1. 数据持久化
1.2. TLS 支持
1.3. 登录授权验证
1.4. docker compose
2. 测试
3. NGINX做代理
3.1. 我的方式和遇到的问题
3.2. NGINX 作为一个容器
4. 其它方案
5. 相关链接

Docker官方提供了 registry镜像, 可以方便的搭建私有仓库,详细文档参考这里

配置项

数据持久化

可以通过采用数据卷挂载或者直接挂载宿主机目录的方式来进行。挂载到容器内默认位置: /var/lib/registry
比如可以像如下方式启动, 这里将容器数据存储在了 /mnt/registry.

$ docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v /mnt/registry:/var/lib/registry \
  registry:2

当然,镜像还提供了其它支持的存储方式,比如OSS等。

官方文档参见这里

TLS 支持

为了使得私有仓库安全地对外开放,需要配置 TLS 支持。

测试的时候,如果不配置的话TLS,可以在docker客户端中的 "insecure registry" 里添加私有仓库地址,不然默认的都以安全的tsl方式来访问私有仓库,具体更改方式可以参考这里

我的CA证书是从阿里云获取的(因为域名是在上面注册的,可以提供免费的证书,虽然如果做得很隐蔽)。

registry镜像可以通过 REGISTRY_HTTP_TLS_CERTIFICATEREGISTRY_HTTP_TLS_KEY 环境参数配置TLS支持。
例如下面这样, domain.crtdomain.key 是获得的证书,另外配置容器监听ssl默认的 443 端口。

$ docker run -d \
  --restart=always \
  --name registry \
  -v `pwd`/certs:/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  -p 443:443 \
  registry:2

官方文档参见这里

登录授权验证

可以通过 htpasswd 来配置简单的authentication (注意:验证需要 TLS 支持)。

首先在 auth 目录下通过reistry里的 htpasswd 工具创建 验证文件 auth/htpasswd

$ mkdir auth
$ docker run \
  --entrypoint htpasswd \
  registry:2 -Bbn testuser testpassword > auth/htpasswd

启动的时候通过 REGISTRY_AUTH, REGISTRY_AUTH_HTPASSWD_REALM, REGISTRY_AUTH_HTPASSWD_PATH 来配置:

$ docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v `pwd`/auth:/auth \
  -e "REGISTRY_AUTH=htpasswd" \
  -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -v `pwd`/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

这样就启动了一个监听5000端口的、支持TLS和简单登录验证的docker 私有仓库。

官方文档参见这里

docker compose

"docker compose" 是一个方便定义和运行多个容器的工具, 安装参见这里, 或者通过pip安装: pip install docker-compose

以上配置项通过 docker compose 的方式组织起来如下:

文件命名成 docker-compose.yaml

registry:
  restart: always
  image: registry:2
  ports:
    - 5000:5000
  environment:
    REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
    REGISTRY_HTTP_TLS_KEY: /certs/domain.key
    REGISTRY_AUTH: htpasswd
    REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
    REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
  volumes:
    - /path/data:/var/lib/registry
    - /path/certs:/certs
    - /path/auth:/auth

docker-compose.yaml 所在目录运行:

docker-compose up

测试

私有仓库搭建好了如何测试?

# 先拉取官方镜像
$ docker pull ubuntu:16.04

# 打上标签
$ docker tag ubuntu:16.04 myregistrydomain.com/my-ubuntu

# 推到私有仓库
$ docker push myregistrydomain.com/my-ubuntu

# 从私有仓库获取
$ docker pull myregistrydomain.com/my-ubuntu

Nginx做代理

我的方式和遇到的问题

实际配置中,我采用了 nginx 作为代理,来访问 registry服务。我将TLS支持和登录验证都加到了nginx一层。

nginx 配置文件:

upstream docker-registry {
  server localhost:5000;                          # !转发到registry 监听的5000 端口!
}

## Set a variable to help us decide if we need to add the
## 'Docker-Distribution-Api-Version' header.
## The registry always sets this header.
## In the case of nginx performing auth, the header is unset
## since nginx is auth-ing before proxying.
map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
  '' 'registry/2.0';
}

server {
  listen 443 ssl;
  server_name domain.com;  # !这里配置域名!

  # SSL
  ssl_certificate /path/to/domain.pem;               # !这里配置CA 证书信息!
  ssl_certificate_key /path/to/domain.key;           # !这里配置CA 证书信息!

  # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
  ssl_protocols TLSv1.1 TLSv1.2;
  ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  # disable any limits to avoid HTTP 413 for large image uploads
  client_max_body_size 0;

  # required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
  chunked_transfer_encoding on;

  location /v2/ {
    # Do not allow connections from docker 1.5 and earlier
    # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
    if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
      return 404;
    }

    # To add basic authentication to v2 use auth_basic setting.
    auth_basic "Registry realm";
    auth_basic_user_file /path/to/auth/htpasswd;          # !这里配置auth文件位置!

    ## If $docker_distribution_api_version is empty, the header is not added.
    ## See the map directive above where this variable is defined.
    add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

    proxy_pass                          http://docker-registry;
    proxy_set_header  Host              $http_host;   # required for docker client's sake
    proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
    proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_read_timeout                  900;
  }
}

其中 /path/to/auth/htpasswd 文件是通过 registry 中的或者本地的 htpasswd 工具生成的

$ docker run \
  --entrypoint htpasswd \
  registry:2 -Bbn testuser testpassword > auth/htpasswd

registry的 docker-compose 文件:

version: '2'
services:
  my_registry:
    restart: always
    image: registry:2
    ports:
      - 127.0.0.1:5000:5000
    volumes:
      - ./data:/var/lib/registry

启动后,当我从本地视图login到私有仓库时,发生错误:

➜  ~ docker login domain.com
Username: testuser
Password:
Error response from daemon: login attempt to https://hub.docker.equiz.cn/v2/ failed with status: 500 Internal Server Error

查看日志发现nginx 错误日志里有个编码相关的错误:

# nginx error log

*4 crypt_r() failed (22: Invalid argument)

经过一番研究,发现之前加密时,是采用 Bcrypt 加密方式,看下 htpasswd 的使用说明:

root@data1:~# htpasswd
Usage:
        htpasswd [-cimBdpsDv] [-C cost] passwordfile username
        htpasswd -b[cmBdpsDv] [-C cost] passwordfile username password

        htpasswd -n[imBdps] [-C cost] username
        htpasswd -nb[mBdps] [-C cost] username password
 -c  Create a new file.
 -n  Don't update file; display results on stdout.
 -b  Use the password from the command line rather than prompting for it.
 -i  Read password from stdin without verification (for script usage).
 -m  Force MD5 encryption of the password (default).
 -B  Force bcrypt encryption of the password (very secure).
 -C  Set the computing time used for the bcrypt algorithm
     (higher is more secure but slower, default: 5, valid: 4 to 31).
 -d  Force CRYPT encryption of the password (8 chars max, insecure).
 -s  Force SHA encryption of the password (insecure).
 -p  Do not encrypt the password (plaintext, insecure).
 -D  Delete the specified user.
 -v  Verify password for the specified user.
On other systems than Windows and NetWare the '-p' flag will probably not work.
The SHA algorithm does not use a salt and is less secure than the MD5 algorithm.

可以看到 -B 会使用 bcrypt 的方式来加密,nginx默认不支持。至于如何让nginx支持bcrypt我暂时还未找到方案,留待以后研究了(TODO)

简单的解决方式是换成默认的MD5加密(因为安全等级问题又不推荐不用bcrypt方式的),

docker run --rm --entrypoint htpasswd registry:2 -bn testuser testpassword > auth/htpasswd   # 这里少了 -B 选项

关于 bcrypt 加密方式,这里 有一篇不错的文章介绍。不过好像对于这个加密方式,网上有一些争论,我就不详究了。

不依赖 "apche tools" 的 nginx 加密方式参考 这里, 比如MD5加密:

printf "testuser:$(openssl passwd -1 testpassword)\n" >> .htpasswd # this example uses MD5 encryption

Nginx 作为一个容器

docker 文档也有如何采用nginx容器和registry配合使用的说明,参考这里

"docker-compose.yaml" 如下:

nginx:
  # Note : Only nginx:alpine supports bcrypt.
  # If you don't need to use bcrypt, you can use a different tag.
  # Ref. https://github.com/nginxinc/docker-nginx/issues/29
  image: "nginx:alpine"     # !这里一定要采用alpine镜像,因为它里的nginx支持 bcrypt 加密!
  ports:
    - 5043:443
  links:
    - registry:registry
  volumes:
    - ./auth:/etc/nginx/conf.d
    - ./auth/nginx.conf:/etc/nginx/nginx.conf:ro

registry:
  image: registry:2
  ports:
    - 127.0.0.1:5000:5000
  volumes:
    - ./data:/var/lib/registry

这里nginx容器监听的是5043, 所以使用的私有仓库的地址变成了 registrydomain.com:5043,
我不喜欢后面加端口,但本机又存在其他需要nginx监听的443端口的服务(不同doamin下),所以不能让nginx容器直接监听443端口,故没有采用这种方式。

另外在寻找解决方案的时候发现一个镜像 nginx-proxy, 可以方便地监听新添加的容器,留待以后探索。

其它方案

或许你想用letsencrypts免费证书,不妨看看这个工具:acme.sh

或许你不满足的简易方案,你可能还需要web界面来方便查看和管理你的镜像仓库,那么你可以查看下企业级的容器仓库方案: vmware/harbor

相关链接

查看原文

赞 1 收藏 4 评论 0

Nisen 发布了文章 · 2018-03-19

搭建swagger-mock-server的一个尝试

内容概要

  1. 概要
  2. Mock Server
  3. UI
  4. Editor
  5. 安装使用和工作流
  6. 参考资源

简介

在前后端分离的企业api开发流程中,有时候会面临前端同学等待后端同学实现接口的情况。
为了避免时间上的浪费,可以采取先制定API文档再前后端并行开发的方式。
前端同学可以直接调用接口获得临时性的假数据,而不影响工作流畅性。

Swagger规范是一种通用的api文档格式,本文记录的是根据swagger文档自动生成假数据的一次尝试。

项目Github地址见: imnisen/swagger-mock-server

声明:这个项目还不完善,目前的阶段是:“能用”。距离达到 “清晰合理的架构”,“简单明了的开发部署”,“完善的功能”目标还需要努力。如果你有更好的做法或者意见建议欢迎通过issuse或者email联系我。

该项目实现的功能是:根据swagger文档,mock server 来生成假数据,这样便于实际开发中,定义好api后,前后端并行开发。

该项目除了mock server以外还包含了查看接口ui和开发时使用的editor.

Mock Server

“Mock server”的实现是基于swagger的这个node 项目server目录 下面的内容是执行参考这里 生成的。

为了支持多种假数据生成的要求,对依赖的一个模块进行了hack, 所以安装使用的时候会发现有这么一步: cp swagger-router.js node_modules/swagger-tools/middleware/swagger-router.js, 实际上是替换了 swagger-router.js 里107行 getMockValue 函数。

这个hack的方法是参考了这篇 博文,然后修复实际使用中发现的一些问题。

UI

UI部分是采用的官方 的ui工具,为了方便直接将内容提取到了 ui/dist目录 下,该目录对应于这个 github目录 便于以后“手工升级”(汗)。

Editor

实际使用的体验是,mock server对于swagger语法解析的要求要比ui要求严格,也就是说有些不合swagger规范的写法ui可以辨识,但却会导致mock server 不能正常解析以至于不能正常启动,所以拥有一个严格的swagger语法编辑器显得挺重要。官方提供了swagger editor,但它不能方便地选择默认编辑的文件和自动保存,所以每次使用时得手动选择要编辑的文件,编辑完之后再保存回去,这使得整体流程有些复杂。好在上面的swagger node项目里包含了 swagger editor,所以我在 server/package.json 里简单配置了下,可以使用 npm run edit 命令直接运行一个监听9999端口的editor, 该editor直接编辑 doc/swagger.yaml 文件,并且所有改动会自动保存。

而且编辑swagger文件应该是开发阶段的行为,所以构建服务的时候,swagger editor并没有暴露出来对外使用(也就是说不能直接修改服务端的swagger文档)。另外一点是我还没有找到合适的方法。在服务器端使用docker部署时按需求定制swagger editor。留待以后探究。

安装使用和工作流

分位两个阶段:开发和部署。

开发的时候,因为要使用到editor所以推荐本地安装,依赖 npm ,需要全局安装npm包 swagger,并且在 server 目录下执行 npm install 来安装所需依赖,最后将hack的 swagger-router.js 复制到对应位置,启动的时候通过 npm run servernpm run edit 分别启动 mock server 和 打开编辑器, swagger ui 也可以启动,通过 docker-compose up -d swagger_ui 来启动,并且在7777端口可以查看,但因为有editor,其提供了视图所以不是很必须。

部署的时候不需要使用editor,所以使用docker compsoe可以直接启动mocker 和 ui, 并且通过7777和8888访问, server/Dockerfile 里干掉了大部分上面需要手动做的事情,还是比较方便的。

具体安装还请参考github项目里说明

参考资源

查看原文

赞 1 收藏 0 评论 0

Nisen 关注了用户 · 2017-12-15

jiacai2050 @liujiacai

Lisp 追随者,编译原理爱好者。
13 年接触 SICP 后坠入 Lisp 大坑,Clojure 3+年使用经验,Ruby/Emacs Lisp/Common Lisp 轻度用户
https://github.com/jiacai2050
Telegram 交流群:https://t.me/clojurists

关注 67

Nisen 回答了问题 · 2016-12-19

一个关于python的问题

在python中,True 代表bool型真

关注 4 回答 3

Nisen 关注了问题 · 2016-12-19

一个关于python的问题

图片描述
这个程序为啥true 那里出现了问题呢 该怎么改正呢

关注 4 回答 3

Nisen 发布了文章 · 2016-12-18

Python装饰器探究——装饰器参数

探究装饰器参数

编写传参的装饰器

通常我们见到的简单装饰器这样的:

import json
import functools

def json_output(func):
    @functools.wraps(decorated)
    def inner(*args, **kwargs):
        result = func(*args, **kwargs)
        return json.dumps(result)
    return inner

@json_output
def f():
    return {'status': 'done'}

当装饰器应用于函数 f 上时,它接受 f 作为其参数,返回一个函数 inner ,且将他绑定到变量f上。

示例中我们编写的装饰器 json_output 只接受一个隐式参数——即被装饰的方法,在使用此装饰器时本身看上去是并没有参数的。然而有时候需要让装饰器自身带有一些需要的信息,从而使装饰器可以使用恰当的方式装饰方法。比如上面的例子中,我们想通过向装饰器传入不同的参数来控制输出结果的缩进(indent)和排序(sort)。我们可以这么做:

import json
import functools

def json_output(indent=None, sort_keys=False):
    def actual_decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            result = func(*args, **kwargs)
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
        return inner
    return actual_decorator

@json_output(indent=4)
def f():
    return {'status': 'done'}

理解传参的装饰器

初次看起来会觉得比较绕人,因为函数里嵌套了两个函数定义,然而实际上和之前一个版本的区别在于为了接收json序列化的参数多包装了一层,所以

@json_output(indent=4)
def f():
    return {'status': 'done'}

# 相当于
@actual_decorator
def f():
    return {'status': 'done'}

这样看起来就会明晰很多。

实际上, 装饰器里的 @ 后接收一个函数,该函数以被装饰的函数(例子中是f)为参数,并且返回一个函数。当需要在装饰函数的同时传入参数的话,那么就需要多包装一层,先传入参数(例子中是 indent=4 )返回一个装饰的函数(例子中是 actual_decorator ), 这个返回的的函数 就跟以前一样接受被装饰的函数(f)作为参数并且返回一个函数作为装饰最后的方法供调用。

传参和不传参的兼容

然而当我们像上面那样定义装饰器时,就不能这样调用:

import json
import functools

def json_output(indent=None, sort_keys=False):
    def actual_decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            result = func(*args, **kwargs)
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
        return inner
    return actual_decorator

@json_output
def f():
    return {'status': 'done'}

在实际的项目过程中,有时会出现这样的状况: 一开始写的装饰器时不需要使用时传参数的,后来发现有必要传参数,改好后原来不传参的装饰器不能正常使用了,这是修改原来使用的地方是项痛苦的事情。
这时候就需要对装饰器做一个兼容,使它在以下情况都可用:

@json_output
@json_output()
@json_output(indent=4)

具体做法如下:

import json
import functools

def json_output(decorated_=None, indent=None, sort_keys=False):
    if decorated_ and (indent or sort_keys):
        raise

    def actual_decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            result = func(*args, **kwargs)
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
        return inner
    if decorated_:
        return actual_decorator(decorated_)
    else:
        return actual_decorator


@json_output(indent=4)
def f1():
    return {'status': 'done'}

@json_output
def f2():
    return {'status': 'done'}

@json_output()
def f3():
    return {'status': 'done'}

print f1()
print f2()
print f3()

代码中关键的地方在于 json_output 在最后对参数 decorated 进行了判断,有的话证明是不传参调用,那么直接返回 actual_decorator 的调用;没有的话则代表是传参类型的调用(虽然参数可能不存在),那么返回 actual_decorator 。其中有点需要注意, josn_output 的传参需要使用关键字参数,如果像下面这样直接传一个位置参数,那么根据现在的实现会出现错误(因为它会被当成 decorated_ )。

@json_output(4)  #错误的使用方法
def f4():
    return {'status': 'done'}

参考资料

  • 《Python高级编程》 by Luke Sneeriger

查看原文

赞 1 收藏 5 评论 1

Nisen 发布了文章 · 2016-12-17

Python 装饰器执行顺序迷思

探究多个装饰器执行顺序

装饰器是Python用于封装函数或代码的工具,网上可以搜到很多文章可以学习,我在这里要讨论的是多个装饰器执行顺序的一个迷思。

疑问

大部分涉及多个装饰器装饰的函数调用顺序时都会说明它们是自上而下的,比如下面这个例子:

def decorator_a(func):
    print 'Get in decorator_a'
    def inner_a(*args, **kwargs):
        print 'Get in inner_a'
        return func(*args, **kwargs)
    return inner_a

def decorator_b(func):
    print 'Get in decorator_b'
    def inner_b(*args, **kwargs):
        print 'Get in inner_b'
        return func(*args, **kwargs)
    return inner_b

@decorator_b
@decorator_a
def f(x):
    print 'Get in f'
    return x * 2

f(1)

上面代码先定义里两个函数: decotator_a, decotator_b, 这两个函数实现的功能是,接收一个函数作为参数然后返回创建的另一个函数,在这个创建的函数里调用接收的函数(文字比代码绕人)。最后定义的函数 f 采用上面定义的 decotator_a, decotator_b 作为装饰函数。在当我们以1为参数调用装饰后的函数 f 后, decotator_a, decotator_b 的顺序是什么呢(这里为了表示函数执行的先后顺序,采用打印输出的方式来查看函数的执行顺序)?

如果不假思索根据自下而上的原则来判断地话,先执行 decorator_a 再执行 decorator_b , 那么会先输出 Get in decotator_a, Get in inner_a 再输出 Get in decotator_b , Get in inner_b 。然而事实并非如此。

实际上运行的结果如下:

Get in decorator_a
Get in decorator_b
Get in inner_b
Get in inner_a
Get in f

函数和函数调用的区别

为什么是先执行 inner_b 再执行 inner_a 呢?为了彻底看清上面的问题,得先分清两个概念:函数和函数调用。上面的例子中 f 称之为函数, f(1) 称之为函数调用,后者是对前者传入参数进行求值的结果。在Python中函数也是一个对象,所以 f 是指代一个函数对象,它的值是函数本身, f(1) 是对函数的调用,它的值是调用的结果,这里的定义下 f(1) 的值2。同样地,拿上面的 decorator_a 函数来说,它返回的是个函数对象 inner_a ,这个函数对象是它内部定义的。在 inner_a 里调用了函数 func ,将 func 的调用结果作为值返回。

装饰器函数在被装饰函数定义好后立即执行

其次得理清的一个问题是,当装饰器装饰一个函数时,究竟发生了什么。现在简化我们的例子,假设是下面这样的:

def decorator_a(func):
    print 'Get in decorator_a'
    def inner_a(*args, **kwargs):
        print 'Get in inner_a'
        return func(*args, **kwargs)
    return inner_a

@decorator_a
def f(x):
    print 'Get in f'
    return x * 2

正如很多介绍装饰器的文章里所说:

@decorator_a
def f(x):
    print 'Get in f'
    return x * 2

# 相当于
def f(x):
    print 'Get in f'
    return x * 2

f = decorator_a(f)

所以,当解释器执行这段代码时, decorator_a 已经调用了,它以函数 f 作为参数, 返回它内部生成的一个函数,所以此后 f 指代的是 decorater_a 里面返回的 inner_a 。所以当以后调用 f 时,实际上相当于调用 inner_a ,传给 f 的参数会传给 inner_a , 在调用 inner_a 时会把接收到的参数传给 inner_a 里的 funcf ,最后返回的是 f 调用的值,所以在最外面看起来就像直接再调用 f 一样。

疑问的解释

当理清上面两方面概念时,就可以清楚地看清最原始的例子中发生了什么。
当解释器执行下面这段代码时,实际上按照从下到上的顺序已经依次调用了 decorator_adecorator_b ,这是会输出对应的 Get in decorator_aGet in decorator_b 。 这时候 f 已经相当于 decorator_b 里的 inner_b 。但因为 f 并没有被调用,所以 inner_b 并没有调用,依次类推 inner_b 内部的 inner_a 也没有调用,所以 Get in inner_aGet in inner_b 也不会被输出。

@decorator_b
@decorator_a
def f(x):
    print 'Get in f'
    return x * 2

然后最后一行当我们对 f 传入参数1进行调用时, inner_b 被调用了,它会先打印 Get in inner_b ,然后在 inner_b 内部调用了 inner_a 所以会再打印 Get in inner_a, 然后再 inner_a 内部调用的原来的 f, 并且将结果作为最终的返回。这时候你该知道为什么输出结果会是那样,以及对装饰器执行顺序实际发生了什么有一定了解了吧。

当我们在上面的例子最后一行 f 的调用去掉,放到repl里演示,也能很自然地看出顺序问题:

➜  test git:(master) ✗ python
Python 2.7.11 (default, Jan 22 2016, 08:29:18)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import test13
Get in decorator_a
Get in decorator_b
>>> test13.f(1)
Get in inner_b
Get in inner_a
Get in f
2
>>> test13.f(2)
Get in inner_b
Get in inner_a
Get in f
4
>>>

在实际应用的场景中,当我们采用上面的方式写了两个装饰方法比如先验证有没有登录 @login_required , 再验证权限够不够时 @permision_allowed 时,我们采用下面的顺序来装饰函数:

@login_required
@permision_allowed
def f()
  # Do something
  return

参考资料

  • 我的大脑和好奇心

查看原文

赞 17 收藏 17 评论 11

认证与成就

  • 获得 35 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-14
个人主页被 563 人浏览