Welcome to wangmeng-python’s documentation!

Python 学习资源:

  • awesome-python : 总结了 Python 中广泛使用的类库,论坛等等。
  • pep 文档 : 包括了 Python 的重要更新,编程标准等。
  • `python`_ : python 官方网站,偶尔会去上面看看标准库的文档。
  • pypi 源 : python package index。

刷题平台:

优秀源码:

关注 Github 上有名的 Python 项目。

代码可读性

PEP 8 – the Style Guide for Python Code

正式的 PEP 8 规范由 Kenneth Reitz 编写。

简介

本文档给出 python 标准库遵守的代码规范。

本文档和 PEP 257(文档字符串规范)主要采纳了 Guido 写的 Python 代码规范, 同时也从 Barry 的代码规范中借鉴了一些。

代码规范随着时间会不断改变,因为不断有新的代码规范加进来, 同时旧的代码规范也可能被淘汰。

很多项目有项目自身的代码规范。 当项目代码规范和本文档的代码规范矛盾时,请优先采用项目自身的代码规范。

不要死守规范( A Foolish Consistency is the Hogoblin of Little Minds)

Guido 最重要的发现就是: 代码更多是用来读的。 本文档的代码规范主要是为了提高代码的可读性,让所有 Python 代码都整齐划一。 正如 PEP 20 所说, 可读性不容忽视。

风格规范的重点是一致性。 一定要确保代码风格一致,至少在一个项目,一个模块内部要保证一致性。

有时候,各个代码规范会发生冲突。这时候要靠自己灵活处理。如果拿不定主意,可以参考别人的代码。不要不好意思问别人。

特别地, 不要为了代码风格一致,破坏了代码的后向兼容性。

当出现以下几种情形时,可以无视代码规范:

  1. 当采用代码规范不能提高代码可读性时。
  2. 由于历史原因,采用代码规范会使得代码与历史代码风格迥异时。
  3. 代码在相关代码规范出现之前就写好了,现在再改意义不大。
  4. 代码需要兼容老版本的 python ,老版本 python 不支持准备采用的代码规范时。

代码布局

缩进

每次缩进,使用 4 个空格。

连续代码(一行代码很长,需要分在多行写)中被裹着的代码(可能被 (), [], {} 包裹)应该垂直(左)对齐;或者采用悬挂缩进。使用悬挂缩进时,要注意: 连续代码的第一行不能有参数,后面的几行代码要有更多的缩进以明确显示代码行是连续代码。

Yes:

# Aligned with opening delimiter.
foo = long_function_name(var_one, var_two
                         var_three, var_four)

# More indentation included to distinguish this from the rest.
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# Hanging indents should add a level.
foo = long_function_name(
    var_one, var_two,
    var_three, var_four)

No:

# Arguments on first line forbidden when not using vertical alignment.
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Further indentation required as indentation is not distinguish.
def long_function_name(
    var_one, var_two, var_three,
    var_four)
    print(var_one)

连续代码中的缩进不必非用 4 个空格不行。

Optional:

# Hanging indents *may* be indented to other than 4 spaces.
foo = long_function_name(
  var_one, var_two
  var_three, var_four)

当 if 中的条件表达式很长,必须写成多行代码时,使用括号包裹条件表达式。这时候 if 加上空格再加上左半边括号,刚好四个字符。if 语句内的语句也缩进 4 个空格, 导致条件表达式和 if 语句内的语句看起来很像,容易混淆。 PEP 8 对于这种情况没有明确说明,以下几种处理方法都可以接受:

# No extra indentation.
if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# Add a comment, which will provide some distinction in editors
# supporting syntax highlighting.
if (this_is_one_thing and
    that_is_another_thing):
    # since both conditions are ture, we can frobnicate.
    do_something()

# Add some extra indentation on the conditional continuation line.
if (this_is_one_thing
        and that_is_another_thing):
    do_something()

结束多行结构的小括号/中括号/大括号,可以选择和上一行第一个非空字符对齐:

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

或者和开始结构体的那一行代码的第一个非空字符对齐:

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)
制表符还是空格符

优先使用空格表示缩进。

只有对于已经用制表符表示缩进的历史代码,才能使用制表符表示缩进。

Python3 不允许混用制表符和空格表示缩进。

Python2 混用制表符和空格表示缩进的代码,应该转成只用空格表示缩进的代码。

启动 python2 时带上 -t 参数, python2 会对混用制表符和空格表示缩进的代码发出警告;带上 -tt 参数,这些警告会变成错误。 建议使用这些参数启动 Python2 。

行长度限制

每行最多包含 79 个字符。

对于没有多少结构限制的文本(比如 docstring 或者评论), 每一行最多 72 个字符。

(?? 这一段我没有翻译,感觉也没有用??)

有些团队(比如 Pycharm) 强烈建议使用更长的代码行。对于只有一个团队维护的代码, 只要这个团队对于行长度限制达成一致即可。在确保每行 docstring 最多 72 个字符的情况下,可以把行长度限制提高到 100 个字符(Pycharm 默认行长度限制是 120 个字符)。

Python 标准库保守地限制每行代码最多 79 个字符(doctring/comment 每行最多 72 个字符)。

推荐的把长代码拆成多行代码的方法是使用括号,中括号和大括号。括号中的多行代码会被(编译器)拼接到一起(在编译器看就是一行代码)。使用这种方式比使用 要好。

有时候使用 把一行长代码拆成多行代码是合适的。比如对于 with 语句, 只能用 拆代码:

with open("/path/to/some/file/you/want/to/read') as file_1, \
     open("/path/to/some/file/you/want/to/written', 'w') as file_2:
    file_2.write(file_1.read())

在 assert 语句中,也只能用 /拆代码。

对于连续代码,一定要合理缩进。

代码行应该在二元操作符之前还是之后拆分

几十年来, 推荐做法都是在二元操作符之后拆分代码行。 但是这样做有损代码可读性。首先二元操作符的排布会不整齐, 然后操作符和操作数会分不同的行。 这给代码阅读者的眼睛增加了额外的工作负担。

No:

# No: operators sit far away from their operands
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

为了解决可读性的问题, 数学家和他们出的书采用了相反的规范。 Donald Knuth

采用数学家的做法往往能提高代码的可读性:

# Yes: easy to match operators with operands
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

Python 代码中,在二元操作符之前还是之后拆分代码都可以, 只要保证统一采用一种方式即可。 如果不是历史代码, 建议采用 Knuth 的规范。

空行

顶层函数和类定义之间隔两个空行。

方法定义之间隔一个空行。

保守地使用多个空行隔开不同的函数群。 如果相关的函数都只有一行,相关函数之间可以省略空行。

在函数中,保守地使用空行分开不同的逻辑区。

(??没有翻译,感觉不重要??)

源码编码 ~~~~~~~~~~~~~~~~~~~`

python 源码请使用 UTF-8 编码(python2 中可以使用 ASCII 编码)。

文件采用 ASCII(Python2) 或者 UTF-8(Python 3)不应该有编码声明。(我不同意,好多代码都有编码声明, 当然也有很多不加编码声明)

在标准库中, 非默认编码只会出现在测试中或者是注释或者 docstring 中的包含 non-ASCII 字符的人名中。 其他情况下, 在字符串字面量中使用 x, u, U, N 转义 non-ASCII 字符串。

对于 Python 3.0 以及之后的版本, PEP 3131 规定: 所有 Python 标准库中的标识符都必须使用 ASCII-only 标识符, 只要可使用英文就使用英文(可能会有些英文缩写,专业术语不是英文)。只有在测试用例和人名中可以使用 Non-ASCII 字符。非拉丁语系的人名要给出英文译名。

鼓励面向全球的开源工程采用相似的规则。

imports

引用一个包应独占一行。

Yes:

import os
import yss

No:

import os, sys

从一个包中应用多个字模块是允许的:

from subprocess import Popen, PIPE

imports 要放在文件的顶部, 在模块注释和 docstring 之后, 模块全局变量和常量之前。

imports 应该按照如下顺序分组:

  1. 标准库引用。
  2. 相关第三方包引用。
  3. 本应用/库 包引用。

组之间用空行隔开。

使用绝对路径引用包是推荐的做法, 因为这样写可读性更高同时当引包系统出问题时更容易发现问题:

import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example

不过,也可以显示地使用相对路径引用包, 特别是处理复杂的包结构使得引用绝对路径引用包会非常繁琐时:

from . import sibling
from .sibling import example

标准库避免使用复杂的包结构,全都使用绝对路径来引包。

从包含类的模块中引用类,可以这样写:

from myclass import MyClass
from foo.bar.yourclass import YourClass

如果出现变量名冲突,可以这样写:

import myclass
import foo.bar.yourclass

然后使用 myclass.MyClassfoo.bar.yourclass.YourClass.

精灵不要使用通配符引用( from <module> import * ), 因为通配符引用会让读者和自动化工具搞不清楚都引用了哪些名称。 只有在把内部的接口暴露为外部公开 API 时,应当使用通配符引用。

用这种方式发布名称时, 请遵守下面要说的公开和内部接口的规范。

模块级左右双下划线名称

模块级 dunders (就是左右两边都有两个下滑线的名称) 比如 __name__, __author___, __version__ 等等, 应当出现在模块 docstring 之后, 除了 from __future__ 之外的任何 import 之前。 Python 要求 future 引用必须位于除了模块 docstring 之外的任何代码之前。

比如:

"""This is the example module.

This module does stuff.
"""

from __future__ import barry_as_FLUFL

__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'

import os
import sys
字符串引号

在 Python 中, 单引号和双引号是一样的。 PEP 并没有推荐说要使用哪个。 只要保持一致性即可。 当一个字符串中包含了单引号或者双引号时,使用双引号或单引号(避免使用转义字符能提高可读性)。

使用 三个引号 包裹的字符串时, PEP 257 要求使用双引号。

语句和表达式中的空格

如下情形下,不要使用多余的空格。

Yes:

spam(ham[1], {eggs: 2})

No:

spam( ham[ 1 ], { egss: 2 })

在结尾逗号和括号右半边之间

Yes:

foo = (0,)

No:

bar = (0, )

逗号,分号和冒号前

Yes:

if x == 4: print x, y = y, x

No:

if x == 4 : print x , y ; x , y = y , x

不过冒号作为切片表达式中的二元操作符使用时, 需要在冒号两边各空一格( 把冒号看成是最小优先级的二元操作符 )。 不过,在切片中, 冒号像是二元操作符, 应当在冒号两边保留对称的几个空格(可能是0个)。 在 extended slice (slice 中包括:: 号称为 extended slice), :: 号左右两边应该有对称的几个空格(可能是0个)。如果 : 号有一边的参数为空, 空格可以省去空格。

Yes:

ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset, upper + offset]

No:

ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : upper]
ham[ : upper]

函数调用的左括号之前不要留空格。

Yes:

spam(1)

No:

spam (1)

按下标取值或者切片的左半边括号之前不要留空格。

Yes:

dct['key'] = lst[index]

No:

dct ['key'] = lst [index]

不要为了对齐而在赋值运算符( = )左边(或者右边) 添加超过 1 个的空格。

Yes:

x = 1
y = 2
long_variable = 3

No:

x             = 1
y             = 2
long_variable = 3
其他推荐

在任何地方都不要以空格结束本行代码, 因为行末的空格不可见, 这可能会闹出问题: 比如反斜杠(连字符) 如果后面接空白字符就不再能够当连字符使用。 很多编辑器不允许以空格作为行结束符。

总是在下面这些二元运算符的左右两边留一个空格: =, +=, -=, ==, >, <, <>, <=, >=, in, not in, is, is not, and, or, not.

在表达式中使用不同优先级的运算符, 在优先级最低的运算符的左右两边各留一个空格。 不要使用多个空格, 左右两边的空格数要一致。

Yes:

i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)

No:

i=i+1
submitted +=1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)

= 号用于表示关键字参数默认值时,请不要在 = 号左右留空格。

Yes:

def complex(real, imag=0.0):
    return magic(r=real, i=imag)

No:

def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

函数注解中 : 之后空一格, -> 左右各空一格。

Yes:

def munge(input: AnyStr): ...
def munge() -> AnyStr: ...

No:

def munge(input:AnyStr): ...
def munge()->PosInt: ...

如果注解中有默认值,那么在 = 号左右各留一个空格。

Yes:

def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...

No:

def munge(sep: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...

不鼓励使用复合表达式(多个表达式在一行)

Yes:

if foo == 'blah':
    do_blah_thing()
do_one()
do_two()
do_three()

Rather not:

if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()

有时候会把循环体(if 体)很小的 if/for/while 语句写到一行中。 对于有多个子句的表达式不要这么做(不要写到一行中)。

Yes:

if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()

Definitely not:

if foo = 'blah': do_blah_thing()
else: do_non_blah_thing()

try: something()
finally: cleanup()

do_one(); do_two(); do_three(long, argument,
                             list, like, this)

if foo == 'blah': one(); two(); three()
逗号行结束符怎么用

逗号行结束符通常是可有可无的。 只有在把一个元素整成一个 tuple 时, 逗号行结束符是必须的。 为了表述清晰, 推荐使用括号包裹逗号行结束符。

Yes:

FILES = ('setup.cfg',)

Ok, but confusing:

FILES = ‘setup.cfg’,

用版本控制系统(比如 git) 管理时, 在一列可能将来还会扩展的值,参数或者引入名称之后添加多余的逗号行结束符是有益的。 应该把每一个量单独放在一行, 每个量后面都都加逗号行结束符, 然后另起一行写大括号/中括号/小括号的右半边。 不过, 如果参数和括号右半边在同一行,就不要在最后一个参数后面添加逗号(tuple 中只有一个值是个例外)。

Yes:

FILES = [
        'setup.py',
        'tox.ini',
    ]

initialize(FILES,
           error=True,
           )

No:

FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)
注释

与代码相矛盾的注释不如没有。 当修改文档时, 一定要及时更新注释。

注释应当是完整的句子。 如果一个注释是一个词组或者一句话, 首字母应当大写, 除非收个单词是代码中的以小写字母开头的标识符。(一定不要更改标识符的大小写)。

如果注释非常短, 那么注释后面的句号可以省略。 块注释通常包含了一个或者多个由完整句子构成的段落, 每个句子都应当以句号结束。

在句号后面空两格。

用英语写注释时, 遵守 Strunk and White (写作规范)。

对于非英文母语国家的 Python 程序员: 只要你不是百分之一百二地确定代码一定不会由说其他语言的人读, 请用英文写注释。

块注释

块注释注释在它们之后的一些或者全部代码。 块注释要和它们注释的代码保持相同的缩进。 块注释的每一行都以 # 加空格 开头(除非注释本身有缩进的要求, 比如 restructure text 可能有缩进的要求)。

块注释的不同段落由以单个 # 开头的空行 隔开。

行内注释

节约地使用行内注释。

行内注释指的是与被注释代码在同一行的注释。 行内注释和代码之间至少空两格。行内注释以 # 加单个空格开头。

如果行内注释的代码含义很明显, 就不必写行内注释了。

不需要这样写:

但是下面这样写是有帮助的:

文档字符串

PEP 257 给出了文档字符串的写作规范。

为所有公共模块, 函数, 类和方法写文档字符串。 不必为非公用方法写文档字符串, 但是应当有注释描述方法做了什么, 这个注释出现在 def 行后面。

PEP 257 给出了文档字符串的写作规范。其中最重要的一点, 结束多行文档字符串的 “”” 应该独占一行, 比如:

"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""

对于只有一行的文档字符串, 结束文档字符串的 “”” 不要另起一行。

命名规范

Python 库遵守的命名规范有点乱, 所以我们没法给出统一的规范。 不过, 有些当下推荐的命名标准。 新的模块和包(包括第三方框架)应该遵守这些命名标准, 但是可能会有已有的包遵循不一样的命名风格, 内部一致性更重要,对这样的包做开发,请遵守这些包遵守的命名风格。

重写规定

对用户开放的公有 API 的名称应当能反映 API 的用途而不是 API 的实现。

叙述性: 命名风格

有多种不同的命名风格。 一眼就能分辨出命名风格写出可读性高的代码是有帮助的。

下列是几种有名的命名风格:

注意:在 CapWords 命名风格中,如果使用缩写, 那么缩写的每个字母都要大写。 比如 HTTPServerErrorHttpServerError 要好。

有种命名风格给相关一组名称添加相同的短小而特殊的前缀。 这种命名风格在 Python 中用的不多, 这里只是为了完整提一下。 比如, os.stat() 函数返回 st_mode, st_size, st_mtime 等等名称组成的元组。(这么做是为了帮助熟悉 POSIX 系统调用的程序员理解变量的含义)。

X11 库中的每个公开函数的函数名都以 X 开头。 在 Pyhton 中, 这种命名风格基本上不会用, 因为属性和方法名称前已经有了对象, 函数名之前已经有了模块名。

使用下划线开头或者结尾的风格在 Python 中也很常见(这种风格可以和大小写相关的风格混合在一起使用)

_single_leading_underscore: 只限内部使用的标志。 比如, from M import * 不会引入以下划线开头命名的对象。

single_trailing_underscore_: 通常是为了避免名称冲突。 比如:

__double_leading_underscore: 用于命名类中的属性时, 会引起名称改写(在 FooBar 类中, __boo 会改成 _FooBar__boo)。

__double_leading_and_trailing_underscore__: 用户名称空间中神奇的对象或者属性, 比如 __init__, __import__ 或者 __file__ 。 不要自己创造这样的名称, 只使用文档中的这样的名称。

约定: 命名规范
避免使用的名称

不要用单字母 l, O, I 作为名称。

因为这几个字母太容易混淆。

ASCII 兼容性

Python 标准库中的标识符都必须是 ASCII 兼容的。

包和模块名

模块名必须简短,所有字母全小写。 下划线可以用在模块名中,只要这样写可以提高可读性。 包名也必须简单,全小写字母, 不过包名不建议使用下划线。

C extension 模块名(一般有相应的 Python 模块提供更高层次的接口), C/C++ 模块名是 下划线 + Python 模块名。

类名

类名使用 CapWords 风格。

如果类的文档声明类主要作为 callable 来调用, 那么类名可以使用函数命名的规范。

内建名称自有一套规范: 除了异常名和常量采用 CapWords 命名规范外, 内建类名基本是小写的一个单词或者两个单词。

类型变量名

PEP 484 引入了类型变量, 类型变量名通常使用 CapWords 的命名风格: T, AnyStr, Num 。 推荐添加 _co_contra 声明类型变量是共变的,还是反共变的。

from typing import TypeVar

VT_co = TypeVar('VT_co', covariant=True)
KT_contra = TypeVar('KT_contra', contravariant=True)
异常名

因为异常是类, 所以异常命名遵守类命名的规范。 不过,需要使用 Error 后缀命名异常(如果类确实是个错误的话)。

全局变量名

(假设这些变量只在同一个模块内使用)。 使用和命名函数一样的命名规范。

如果模块将来会使用 from M import * 引用, 那么请使用 __all__ 机制来控制暴露哪些变量;或者可以使用在全局变量名之前加下划线的方式,避免把该全局变量暴露给外部。

函数名

构成函数名的单词应该全小写,使用下划线连接。

为了保持后向兼容性时,才会使用 mixedCase 风格命名。比如 threading.py (我在 threading.py 文件中,并没有找到以 mixedCase 风格命名的函数名)。

函数和方法的参数

实例方法的第一个参数一定用 self 。

类方法的第一个参数一定用 cls 。

如果函数参数名和一个保留字冲突, 通常的做法是在原来的名称后面添加下划线。这样做,比使用缩写或者 spelling corruption 要好。比如 class_clss 要好。(当然也可以采用同义词命名)

方法名和实例变量

使用函数命名的规范。构成方法和实例变量名的单词全小写, 用下划线连接。

非公开实例变量名以下划线开头。

为避免和子类发生名称冲突, 启用名称改写机制, 使用双下划线开头。

Python 使用类名改写: 如果类 Foo 存在名为 __a 的属性, Foo.__a 没法访问 __a 。 (一个钻牛角尖的程序员可以使用 Foo._Foo__a 访问 __a)。 双下划线开头的名称能避免和子类发生名称冲突, 通常用在要被继承的类中。

注意: 使用 __names 有争议(见下文)。

常量

常量一般定义在模块级别。构成模块的单词全大写,且用下划线连接。比如 MAX_OVERFLOWTOTAL

设计继承

考虑并确定类的方法和实例变量(统称 attributes)应该是公开的还是不公开的。 如果疑惑, 就不公开。因为把一个公开的改成非公开很费劲。

公开 attributes 就是那些你预期让其他用户使用的的 attributes, 你必须保证公开 attributes 的后向兼容性。 非公开 attributes 是那些不准备让第三方用户使用的 attributes,不必保证非公开 attributes 的后向兼容性,它们将来可能会被修改甚至删除。

我们不使用 private 这个词,因为在 Python 中没有 attributes 是真正 private 的。

另一类 API 是 subclass API (其他语言中,常常称为 protected)。有的类是为了要被继承而写的, 不管是要扩展还是要修改类的行为。 当定义一个这样的类时, 仔细决定哪些 attributes 应该是公开的, 哪些应该是 subclass API, 那些应该只在该基类中使用。

下面是些建议,能使你的代码更加的 Python 风:

公开的 attributes 不要以下划线开头

如果你的公开 attributes 的名字和保留字同名, 那么可以在你的 attribute 名后面添加一个下划线。 这种做法比缩写和 corrupted spelling 要好。(尽管有这条规则, 不论何时如果变量表示一个类,变量都应该使用 cls 命名, 特别的, 类方法第一个参数必须以 cls 命名)。

注意1: 看类方法第一个参数的命名规范。

对于简单的 attributes, 不必添加 accessor/mutator 方法。 当你发现简单 attribute 需要有些功能行为时, Python 提供了方便的方法去扩展。比如,可以使用 property 给访问 attribute 添加隐藏的功能。

注意1: properties 只在新式类中有用。

注意2: 尽量减少隐藏功能的副作用。虽然有的副作用比如缓存总体来说是好的。

注意3: 不要使用 property 实现计算复杂度高的操作, 用户都认为访问 attribute 是个低成本操作。

如果你定义的 attributes 不想让子类用, 那么 attribute 名应该以双下划线开头,且不以下划线结尾。 这样能启动 Python 的名称改写机制。

注意1: 名称改写只用到类名和 attribute 名,所以如果子类和父类同名, attribute 名也相同,那么还是会发生名称冲突。

注意2: 名称改写使得某些应用(比如调试和 __getattr__())变得不方便。 不过名称改写很简单,很容易手动实现。

注意3: 不是所有人都喜欢名称改写。 综合考虑避免名称冲突的需求和方便使用的需求。

公开和内部接口

只有公开接口需要保证后向兼容性。 相应的, 公开接口和内部接口的写法应该明确的区分开。

有文档字符串的接口是公开的接口, 除非文档字符串明确声明它们是内部接口,不保证后向兼容性。所有没有文档字符串的接口都应当是内部接口。

为了更好的支持内省, 模块应该在 __all__ attribute 中明确列出公开 API。 __all__ 取值为空链表就是说模块没有公开 API。

即使正确设置了 __all__, 内部接口仍然应当以单下划线开头。

接口是内部的,如果包含它的接口是内部的。

被引入的名称应当被当成实现细节。 只有当有文档字符串声明它们是模块 API 时(比如 os.path 或者包中的 __init__ 常常把引用的子模块 中的名称声明为公开名称), 它们才能被第三方用户访问。

编程建议

代码应该在所有 Python 实现(PyPy, Jython, IronPython, Cython, Psyco 等等)上都能正常运行。

比如, 不要指望 Cpython 的本地字符串连接实现提供效率,就是用 a += b 或者 a = a + b 表达式。 这种优化就算在 CPython 上都非常脆弱(只对有些类型有效),而且 在不使用引用计数的 Python 实现上没有效果。 实现注重效率的类库时, ‘’.join() 的表述是应该优先使用的。 这能保证, 不管在 Python 的哪种实现上, 连接字符串的时间复杂度都是线性的。

与单例(比如 None) 做比较, 总是应当使用 is 或者 is not , 不要使用 ==

小心, 如果你要判断一个变量是不是 None, 比如要检验一个默认取值为 None 的变量或者参数被赋值为其他值时, 不要使用 if x, 因为其他值也可能是 falsy (找不到比 falsy 更合适的词)的。

使用 is not 而不要使用 not … is 。 虽然两种表述功能上相同, 但是前一种表述可读性更好。

Yes:

No:

要实现可以各种比较大小的排序操作时, 最好是实现所有 6 个比较操作(__eq__, __ne__, __lt__, __le__, __gt__, __ge__), 而不是依赖其他代码的默认实现而只实现一种比较操作。

为了减小工作量, 可以使用 functools.total_ordering() 装饰器自动产生没有实现的比较操作。

PEP 207 表明 Python 在比较大小时默认满足自反性。 就是说 Python 解释器可能会把 y > x 语句换成 x < y 语句, y >= x 语句换成 x <= y 语句, 以及 x == y 换成 x != y。 sort()min() 操作保证使用 < 操作, 而 max() 函数保证使用 > 操作。 不过, 为了避免出现问题,最好还是实现所有的比较操作。

不要把 lambda 表达式赋值给变量。定义函数应该使用 def 语句, lambda 表达式只用来写匿名函数。

Yes:

No:

第一种表述生成的函数对象的 name 属性取值为 f, 而不是像第二种表述那样取值为 ‘lambda’。 这使得函数的 %d 更有意义, traceback 信息更有意义。把 lambda 表达式赋值给变量 让 lambda 表达式失去了它唯一的好处(那就是能嵌入到一个更大的表达式)。

自定义异常要继承自 Exception 而不是 BaseException。捕获直接从 BaseException 继承的异常几乎总是错的。

设计异常的层次时,更应该考虑异常是怎样被捕获的,而不是说异常是如何抛出的。有条理地回答什么出错了, 而不是仅仅声明出错了。(看 PEP 3151 看了实际的例子)

命名异常和命名类采用相同的规范。 如果异常是错误,那么异常名需要以 Error 做后缀。 非错误异常常被当做信号用于非本地工作流控制等场景中。 非错误异常不需要特殊的后缀。

合理使用异常链。 在 Python 3 中, raise X from Y 被用来明确指明用 X 异常替换 Y 异常,同时又不丢失 Y 异常的 traceback。

要故意替换一个 inner 异常时(在 Python 2 中使用 “raise X”, 在 Python 3 中使用 “raise X from None” ),确保相关细节已经传给了新的异常(比如把 KeyError 的属性名传给 ValueError, 或者把原来的异常的消息传给新异常。)

在 Python2 中抛异常, 使用 raise ValueError(‘message’) ,不要用过去的 raise ValueError, ‘message’

后面那种表述,不符合 Python 3 的语法。

前面那种表述同时意味着, 当异常的参数太长或者包括格式化字符串时, 因为括号的存在, 你不需要使用连字符。

捕获异常时,尽量指明要捕获的异常,而不要单单写 except: clause

比如, 使用:

单单使用 except: clause 将会捕获 SystemExitErrorKeyboardInterrupt 异常, 这使得没法用 Control-C 中断程序, 同时也会导致一些问题。 如果想要捕获所有程序本身的错误,使用 except Exception:`(单单使用 `except 相当于使用 except BaseException:

只有在下面两种情形下会单单使用 except:

  1. 程序作者知道程序有错, 仅仅是为了打印出错误。
  2. 代码仅仅做些扫尾工作, 待会还会使用 raise 把异常抛出来。在这种情况下,使用 try … finally 是更好的表述。

要给异常绑定名称, 优先使用 Python 2.6 中引入的显式名称绑定语法。

Python3 只支持这一种给异常绑定名称的语法。 Python2 之前的使用逗号给异常绑定名称的语法会导致语义不明的问题,不要使用。

要捕获操作系统异常时,使用 Python 3.3 引入的显式的异常层次,不要使用 errno 变量。

还有一点, 对于所有的 try/except 语句, try 子句中的代码要竟可能少, 以免太多的代码把 bug 隐藏了起来(不好调试)。

Yes:

No:

一份资源尽在局部代码段中使用时, 请使用 with 保证资源在使用之后立即且可靠的做了扫尾工作。使用 try/finally 也可以。

上下文管理器应该通过独立的函数或者方法调用,只要它们做了获取和释放资源之外的操作。比如:

Yes:

No:

后一种表述没有暗示 __enter____exit__ 在做完事务时, 除了关闭连接还做了额外的操作。 Being explicit is Important in this case.

return 语句要保持一致。要不所有 return 语句都返回表达式,要不都不返回表达式。 如果一个 return 语句返回表达式,那么对于什么也不返回的 return 语句,应当使用 return None 表述,而且 函数的最后一句应该显示的 return

Yes:

No:

使用字符串方法而不是字符串模块

字符串方法总是比 unicode strings 提供的 API 快很多,除非是要与 Python 2.0 之前的Python 后向兼容,否则不要使用 string 模块。(我从来不使用,也不会使用)

使用 ‘’.startswith()‘’.endswith() 检查前缀后缀,不要使用字符串切片去检查前缀或后缀。

startswith()endswith() 表意更明确更不容易发生错误。比如:

Yes:

No:

比较对象的类型,请使用 isinstance() , 不要直接比较类型。

Yes:

No:

判断一个对象是不是字符串时, 记住在 Python2 中, unicode sting 和 str 都是字符串。 unicode string 和 str 有相同的基类: basestring 。所以你可以这样判断:

在 Python3 中, unicode 和 basestring 不再存在。 字节组对象不再是 string(而是一列整数)。

对于序列(字符串,列表,元组), 空序列是 falsy 的:

Yes:

No:

不要写以空格结尾的字符串字面量。这样的空格让人混淆,甚至有的编辑器会直接删除这样的空格。

布尔值和 True/False 做比较时,不要使用 ==:

Yes:

No:

Worse:

采纳 PEP 484 以来, 函数注解的规范一直在变。

为保证前向兼容, Python3 代码中的函数注解应当采用 PEP 484 规定的注解语法。

PEP 848 之前推荐的注解的试行规范现在已经不再推荐使用了。

不过,在 Python 标准库之外,推荐尝试 PEP 484 代码规范。 比如, 为第三方库或者应用添上类型注解, 看看添加注解是不是很容易, 有没有提高代码的可读性。

Python 标准库保存地使用注解,新代码已经重大的重构允许使用注解。

对于要另有他用的函数注解,建议在 Python 文件顶部添加如下面所示的注释:

这个注释告诉 type checker 忽略所有注解。(更精细的取消 type checker 报警的方法参见 PEP 484)

和 Linter 一样, type checker 是可选的, 独立的工具。Python 解释器默认不会做类型检查,函数注解默认不能改变代码的行为。

用户可以选择使不使用 type checker 。不过,希望第三方包的使用者可以使用 type checker 对第三方包做检查。为此, PEP 848 推荐 使用 stub 文件: .pyi 文件, type checker 会优先读取 .pyi 文件。stub 文件可以通过类库发布,也可以通过 typeshed repo 发布。

需要保证后向兼容性的代码, 可以把类型注解写到注释中。请查看 PEP 848 中的相关章节。

代码检查工具

Author:王蒙
Tags:python 开发,编码规范,持续集成
abstract:Pylint , flake8 等代码检查工具能够帮助我们写出更规范的代码。本文简述了代码检查工具常用的用法,更高级的操作,请查看工具的文档。

Audience

Python 开发

Prerequisites

pep8 代码规范, CI(持续集成),圈复杂度

Problem

  • 为什么需要代码检查工具?
  • Python 主流代码检查工具?
  • Pylint
  • flake8
  • Pycharm

Solution

  • 为什么需要代码检查工具?

    • 能大大提高检查代码的效率。代码(规范)检查重点在于 一致性 ,需要做大量机械化的操作。
    • 能强制贯彻代码规范的执行。比如可以把代码检查放到 CI 流程,不通过代码检查不让合并到 develop 分支,这样就能保证 develop 分支中的代码都是符合规范的代码。
  • Python 主流代码检查工具?

    现在,几乎所有语言都有代码检查工具。Python 中主流的代码检查工具为: Pylintflake8

    不管哪种检查工具,都会报告被检查代码,哪些行的哪些列的代码不合哪些规范。比如如下检查结果:

    # 检查 mock_demo.py 是否符合规范
    $ pylint mock_demo.py
    No config file found, using default configuration
    ************* Module python-test-demo.mock_demo
    C: 46, 4: Missing method docstring (missing-docstring)
    C: 52, 0: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name)
    W: 54, 0: Expression "'complex_add' in dir(a)" is assigned to nothing (expression-not-assigned)
    
    -----------------------------------
    Your code has been rated at 5.95/10
    

    上面检查结果,告诉我们:

    • 代码的第 46 行, 第 4 列的代码,缺少 module docstring 不符合规范。
    • 代码的第 52 行, 第 0 列的代码,常量 a 应该采用 UPPER_CASE 命名规范。

    根据代码检查结果,修改代码,会让代码合乎代码规范。

    Pylint 和 flake8 各有特点,具体来说:

    • Pylint 可以自定义代码规范,非常灵活。
    • flake8 提供了计算 圈复杂度 的功能。
  • Pylint

    • Pylint 会给被检查代码评分,根据评分可以大概估计代码质量。

      采用 Pylint 默认配置检查了 flask 代码以及一些 Python 内建包的代码,发现 Pylint 给出的评分在 7.5 以上。基本上我们写代码,如果评分达到 7 分,说明代码质量可以的(单说格式是否规范)。

    • Pylint 可以灵活配置。

      PEP 8 是 Python 推荐的代码规范。实际一个团队用 Python 开发时,可能会针对 PEP 8 没有明确规定的细节给出组内规范;也可能因为某些原因(历史原因,项目个性需求,领导强制要求等)给出违背 PEP 8 的规范。

      Pylint 可以按照定制化的代码规范检查 Python 代码,只要把规范写到 Pylint 的配置中即可。

      pylint 默认根据工作目录下的 .pylintrc (该文件指明了代码规范)检查代码,如果工作目录下没有 .pylintrc 文件,就会使用默认的代码规范检查代码。

      自定义规范一般是在默认代码规范基础上做定制。使用

      $ pylint --generate-rcfile > .pylintrc
      

      在工作目录生成默认规范的配置文件,修改 .pylintrc 中的选项值,就能定制代码规范(比如把 max-line-length=100 改为 max-line-length=79)。.pylintrc 中每一项配置上方有注释,说明该项配置有什么效果。

  • flake8

    flake8 默认会使用 pep8 第三方包检查代码(经常会安装 mccabe 和 pyflakes,安装之后,flake8 就会提供个性的功能)。 pep8 第三方包只能检查代码是否符合 pep8 代码规范(所以 flake8 默认是使用 pep8 代码规范做检查)。

    相对于 Pylint , flake8 提供了些特有的功能。

    • 检查代码的 圈复杂度 (flake8 会调用 mccabe 计算圈复杂度)。

      • 圈复杂度和 if 语句有关,选择分支越多,圈复杂度越高。

      • 圈复杂度越低越好。圈复杂度高影响代码可读性,代码容易出错。

      • flake8 官网建议圈复杂不要超过 12 。

      • 更多圈复杂度的内容请查考 圈复杂度

        # 检查圈复杂度
        $ flake8 --max-complexity 1 mock_demo.py
        mock_demo.py:62:1: C901 'calculate_inverse' is too complex (2)
        mock_demo.py:66:80: E501 line too long (89 > 79 characters)
        mock_demo.py:70:1: C901 'test_calculate_f' is too complex (2)
        mock_demo.py:74:80: E501 line too long (81 > 79 characters)
        

        flake8 的检查结果,告诉我们 mock_demo.py 第 62 行的 calculate_inverse 的圈复杂度大于2(这里这是个列子,一般认为圈复杂度小于等于 12 都可以接受)。

    • 通过在源码中加注释,通知 flake8 不要检查该行代码。

      -*- coding: utf-8 -*-
      import os
      # noqa(no quality analysis) 告诉 flake8 不要检查该行代码。
      from fabric.api import * # noqa
      from fabric.contrib.files import exists
      
    • flake8 使用 pyflakes(只要Python 解释器安装了 pyflakes,flake8 就会使用 pyflakes) 检查代码。pyflakes 不报告格式不规范的问题,pyflakes 只报告代码语法上的错误(pylint 也会检查代码错误,所以这一点不算特性)。

  • Pycharm

    • Pycharm Ctrl+Alt+L 快捷键会自动规范代码格式。我写代码的时候,经常会点这个键。

    • Pycharm inspect code 功能会检查代码。

    • Pycharm 可以勾选 git -> before commit -> perform code-analysis 。这样在提交代码之前会自动检查代码,确保每次提交都是合乎规范的代码。

      _images/pycharm_analysis_before_commit.png

项目文档

作者:王蒙
标签:文档,python 开发,编码规范,持续集成
简介:没有文档介绍的项目,别人没法使用。所以文档对于项目至关重要。

目标读者

Python 开发

问题

  • 技术文档写作规则
  • 项目通常包含哪些文档
  • Python 推荐的文档格式
  • 持续集成和文档托管

解决办法

技术文档写作7个规则
  1. 开始写的时候,专注于内容;然后整个检查一遍,整理格式。
  2. 每个文档针对明确的读者。
  3. 文档应该写的尽可能简单。比如一句话不要太长,一个段落不要太长等等。
  4. 分主题组织文档,给每篇文档写合适的题目。选择标题时,可以想,如果读者对文档内容感兴趣,他会在 Google 中输入什么。
  5. 使用统一的格式,使用模板。
  6. 不要写过多的文档,特别不要在文档中写废话,开玩笑等等。
  7. 示例代码,选择真实的,拿来就能运行的代码片段。
通常项目要写哪些文档
  • Design

  • Usage
    • Recipe
    • Tutorial
    • Module Helper
  • Operations

    • Installation and deployment documents
    • Administration documents
    • FAQ
    • Documents that explain how people can contribute, ask for help, or provide feedback
A reStructuredText Primer

Python 文档推荐使用 reStructuredText 格式写。

  • reStructuredText Premier

  • Sphinx 是比 Docutils 要强大的工具。

    • Sphinx 编译出的文档支持检索。
    • Sphinx 编译出的文档有多种主题可选,更美观。
    • Sphinx 支持 toctree 命令,能生成 index 页。
    • Sphinx 能根据 Python 源码的 docstring 生成 API 文档。
  • Sphinx 提供的重要的 directives:

    module mod toxtree index

文档构建和项目集成

一般会把编译生成文档,把文档挂到文档托管平台(比如对于开源项目可以使用 readthedocs 服务)的流程放进持续集成配置中。 这样做之后,文档每次更新都能及时反映给文档的读者。 开源项目可以免费使用 Travis 提供的持续集成服务。

圈复杂度

Author:王蒙
Tags:Python 开发,代码规范
abstract:圈复杂度是代码的重要指标。圈复杂度高的代码,可读性更差,代码更容易出错。

Audience

Python 开发,测试

Problem

  • 什么是圈复杂度
  • 如何降低圈复杂度

Solution

  • 什么是圈复杂度
  • 如何降低圈复杂度

测试

测试就是检验软件是否如预期那样工作。具体来说就是抽出(几条)样本数据,执行程序,检验程序输出的数据是否和预期一致。

测试要求全面(要把所有可能的输入数据类型都考虑全),要求可重复可自动化(开发过程中要测试很多很多次,测试不自动化,测试太慢会拖慢开发效率)。

为此出现了 coverage(测代码覆盖率,看测试全不全),项目构建工具和持续集成工具(自动化测试)。

在这里就讨论 Python 中的主流测试工具。

需要做哪些测试

作者:王蒙
标签:测试,分工
简介:开发过程中需要做哪些测试,测试工作如何分工。

目标读者

技术经理

问题

  • 要做哪些测试,每种测试由什么特点,每种测试由谁来执行

  • 敏捷开发

    • TDD
    • BDD

解决办法

软件开发要做哪些测试:

  • Acceptance test(验收测试)

    把软件当做黑盒做测试,确保软件实现了当初设计的功能。这部分测试,不是由开发者做的,是由 QA staff 或者客户去做的。

  • Unit test(单元测试)

    针对类函数功能的测试,单元测试由开发人员来写,是 TDD(Test Driven Development, 测试驱动开发) 的基础。

  • Functional test

    重点测试整体的功能而不是每个代码单元。和 acceptance test 的不同在于 acceptance tests 完全是使用用户接口做测试。Functional tests 可以不必从用户接口做测试。比如测试 HTTP 服务时,使用浏览器访问(模拟用户的使用)就是 acceptance tests。使用 Postman 直接整 HTTP 请求去测试,就是 Functional tests。

  • Integration test(集成测试)

    重点测试不同软件部件之间是否如预期的那样交互。

  • Load and performance testing(性能测试)

    performance testing 很难,不同机器之间 CPU ,Memory 和 IO 性能不同会影响程序的性能。就算是同一台机器 CPU, Memory 和 IO 性能也不是一层不变的。

  • Code quality testing(代码质量测试)

    • PEP8 规范
    • Complexity metrics(复杂度)
    • 代码覆盖率
    • 编译时 warning 的数量
    • 文档是否够全

开发者主要做的是 Unit test(单元测试)

敏捷开发:为了应对快速变化的需求,敏捷开发的方法大行其道(软件工程方法论是众说纷纭,很有争议的)。在敏捷开发中 TDD 和 BDD 是被广泛认可的两种方法。

TDD:先写(单元)测试用例,再写实现。保证所有函数都有测试用例。(实际开发中,100% 覆盖所有代码的测试还是几乎没有)

BDD:BDD 和 TDD 的不同在于,BDD 的测试用例接近于自然语言,理想情况下是由产品经理写测试用例的。当前几乎所有语言都有 BDD 的测试框架,Python 中 BDD 测试框架有:

对于 Python BDD 我所知甚少。

测试过程中很用到很多 工具

单元测试

作者:王蒙
标签:单元测试, unittest, pytest, nosetest
简介:主要介绍 pytest 。

目标读者

Python 开发

预备知识

Python 基础语法

问题

  • Python 测试框架有哪些
  • assert 断言
  • test fixture(测试固件)
  • Doctest
  • run & debug

解决办法

Python 测试框架有哪些

pytest 是 Python 中最好用的单元测试框架,除此之外我时不时会用 unittest 中的 MagicMock 对象去模拟外部资源。

有些第三方框架(Framework)自带了单元测试工具,在这些框架中使用自带的单元测试工具会比较方便。比如:

  • tornado: tornado.testing 提供的 AsyncTestCaseAsyncHTTPTestCase 能方便的测试 tornado 启动的服务,不用 tornado 自带的工具的话,需要自己去异步运行 tornado 服务,关闭 tornado 服务会很麻烦。
  • Django: 使用 Django 自带的工具 测试 view ,就好像测试的时候 Django 服务自动启动了,测试完 Django 服务自动关闭了。

下面的论述主要点出写 pytest 要注意的点。主要针对 pytest ,但是这些点在其他测试框架(比如 nosetest 和 unittest)中,一样要注意。

assert 断言

断言相等 assert a == b

断言会出现异常: pytest.raises

断言会出现警告: pytest.warnings

pytest 对于不同类型的数据有不同的断言失败消息,方便调试

自定义断言失败消息: 重写 pytest_assertrepr_compare(config, op, left, right) 自定义断言失败消息

test fixture

寻找 fixture:

  • fixtures 不需要引入, pytest 按照类内,module 内,conftest.py 内,最后内建或第三库的范围寻找并引入 fixture。
  • 多个 test 文件都需要使用的 fixture, 写入到 conftest.py 文件中。

fixture 的使用:

fixture 的定义:
  • pytest.fixture
  • 使用 yield 实现 setup/teardown 效果。使用 addfinalizer 实现 setup/teardown 效果。
  • fixture scope: function/class/module/session 。
  • fixture 可以在一个 fixture 中调用另外一个 fixture。
  • fixture 如何获得 test context
  • 参数化 fixtures 。

monkeypatch fixture

使用很广的一个 test fixture, 提供方法(比如 setattr)修改 Python 对象(的定义)。

使用 monkeypatch fixture 的单元测试运行结束,monkeypath 对于 Python 对象所做的修改自动撤销。

Doctest

以文档(文本的方式)写 Doctest。比如:

>>> a = 1
>>> b = 2
>>> a + b
3

上述文本就是一段 Doctest 测试用例。这段文本可以出现在任何文本文件(不管是.py, .c, .rst, .md 等等都可以)中,做测试时,执行如下语句:

pytest --doctest-glob='单元测试.rst'

pytest 就会执行以 >>> 开头的三句代码。对于最后一句代码,pytest 会判断 repr(a + b) 是否等于 3(最后一句相当于是 assert 断言),如果等于测试通过,否则测试不通过。

  • run & debug

    test discovery

    函数名以 test 开头,pytest 认为该函数是测试。 模块名以 test 开头,pytest 认为该模块是测试模块。 目录名以 test 开头,pytest 认为该目录是测试目录。

    pytest 会寻找指定目录/模块/函数中的测试,执行这些测试。

    比如:

    执行当前目录下的所有测试(有测试执行测试;有测试目录,进入测试目录中找测试执行;有测试模块,进去测试模块中找测试执行)。

    pytest .
    

    会执行 test_a.py 模块中的所有测试。

    pytest test_a.py
    

    debug

    pdb 是 Python 自带的命令行调试工具。我看过一个 pdb 的视频,但是从没有用过。

    调试的话,明显是使用 Pycharm 的图形化界面的 Debugger 好。

    介绍 Pycharm 的章节会专门介绍 Pycharm Debugger 所以留到哪里再说吧。

    这里想说的是,先写好测试用例,然后 debug 该测试用例,边debug 边实现,编程就变成了交互式的行为(对于某些繁琐的细节问题,这样做可以试出代码)。

项目构建工具

作者:王蒙
标签:Python 开发,测试,构建工具,CI,自动化
简介:Python 项目构建工具有: Buildout 和 tox。这里主要介绍 tox 。

目标读者

Python 开发

问题

  • 学习 tox, 了解 tox 的功能。

解决办法

读 tox 文档

  1. config.html 中的 Advanced settings 没读懂。
  2. example.html 中的 Generate documentation, Using tox with Jenkins Integration Server. 暂时没看。General tips and tricks 中的 Access package artifacts between multiple tox-runs 不理解,不会用。

读完上面的两篇文档,基本就会用 tox。

代码覆盖率

作者:王蒙
标签:code coverage, CI, code quality
简介:代码覆盖率低,说明你应该多做些测试。本节介绍 Python 代码覆盖率工具 coverage 。

目标读者

Python 开发

预备知识

pip, python, pytest

问题

怎样统计代码覆盖率?

解决办法

怎样统计代码覆盖率?

coverage 可以统计代码覆盖率。coverage 常见用法就下面三句,非常简单。

# 使用 pytest 测试 test/test*.py,统计代码的覆盖率。
$ coverage run -m pytest --source test/test*.py

执行上面一句之后,会生成 .coverage 文件。.coverage 文件用于生成关于代码覆盖率的报告。最常用的是生成表格形式的代码覆盖率报告和 html 形式的代码覆盖率报告。

# 生成表格形式的代码覆盖率报告
$ coverage report
Name                                                                                                     Stmts   Miss  Cover
----------------------------------------------------------------------------------------------------------------------------
E:\Documents\source-code-reading\permission\__init__.py                                                     43     16    63%
E:\Documents\source-code-reading\permission\exceptions.py                                                   36     24    33%
E:\Documents\source-code-reading\permission\permission\models.py                                            159     24    85%
test_models.py                                                                                              106      0   100%
----------------------------------------------------------------------------------------------------------------------------
TOTAL                                                                                                      344     64    81%

# 生成 html 形式的代码覆盖率报告。生成的报告在 htmlcov 目录下。
$ coverage html

html 形式的代码覆盖率报告会显示每一行代码是否被覆盖。是可读性,可用性最好的分析报告。

一般在开源项目中,会在源码仓库中保存 .coverage 文件而不会保留某种特定格式的代码覆盖率报告。因为 .coverage 比较小,且可以生成各种代码覆盖率报告。

Mock or Docker

作者:王蒙
标签:test, mock, fake, monkeypatch, test environment, resource, unit test, 单元测试
简介:Docker 提供执行环境做测试,Mock 伪造外部环境做测试。实际做单元测试时,该选择该种方式?

目标读者

Python 开发

预备知识

pytest, docker, monkeypatch, mock

问题

  • 如何使用 Docker 提供测试环境
  • 如何 mock 环境和资源
  • 哪种方式更好

解决办法

如何使用 Docker 提供测试环境?

如果测试需要 Elasticsearch 数据库服务,那么先去 Docker.hub 上找到 Elasticsearch 镜像,找到该镜像的说明文档,按照说明文档在本地启动 Elasticsearch 服务,然后就可以测试(使用 Elasticsearch 服务的程序)。

整个过程非常简单,只要懂的如何使用 Docker ,整个过程没有任何难度。

如何 mock 环境和资源?

工具:
  • unittest.mock 和 unittest.patch: 伪造 Python Object 。
  • pytest monkeypatch : 伪造 Python Object 的属性值,最厉害的是,这种伪造只在使用 monkeypatch 的测试用例中生效。

修改 Python Object 的属性值,使得 Python Object 看起来就像是在使用某种特定的资源(环境)。比如使用 requests.get 访问 Elasticsearch 服务。mock 要做的不是说提供一个 Elasticsearch 服务,而是重定义 request.get 函数,使得 request.get 看起来就像是在访问 Elasticsearch。

哪种方式更好?

使用哪种方式做测试的代码都有。但是我认为,为了测试自动化,最终的代码要使用 mock 的方式做测试(不要使用 docker)。

我一般是这样做测试的。还是举刚才的例子,我的代码要用 requests.get 访问 Elasticsearch 。那么我:

  1. 开始时,使用 Docker 做测试。
  2. 给出测试用例的输入,调试程序时,捕获 requests.get 对于每个输入的输出(收集输入输出,构造函数 f)。
  3. 调用 monkeypath 设置 reqeusts 的 get 属性值为上一步构造出的函数 f。
  4. 上一步中 f 的输出值是 Response 类,这个类的实例不容易构造。在这种情况下可以使用 MagicMock 类伪装出 Response 类的实例。
  5. 到这一步,就完成了mock 资源做测试的操作。从此不必再启动 Docker Elasticsearch 服务做测试(抛弃之前使用 Docker 的方法),测试完全自动化。

补充一句,很多 CI 工具(比如 Jenkins) 对于 Docker 提供了支持 。所以如果你确信一定只会使用 CI agent 做测试,那么就算使用了 Docker,测试也是能够自动执行的。

持续集成

作者:王蒙
标签:持续集成,CI, Travis, Jenkins
简介:介绍为什么做持续集成,持续集成集成的工具。

目标读者

Python 开发

预备知识

版本管理,测试

问题

  • 为什么要做持续集成

  • 怎么做持续集成

    • Travis
    • Jenkins
    • Teamcity

解决办法

为什么要做持续集成

软件工程中共性的问题:版本控制,测试,部署。 持续集成指的是自动化的机制使得团队中每个人的分支可以安全地(一般是通过尽可能多的测试来保证安全)合并到 develop, master分支。

怎么做持续集成

总体来说,持续集成:
  • 提供现成的执行环境(一般称为 Agent)。
  • 用户配置触发规则,让版本控制系统(比如 git) 的操作(一般是 push 和 merge request)能触发脚本在执行环境中执行。
  • 具体触发什么操作,可能需要开发者定义,一般是 Build(构建代码), Test(测试), Deploy(打包发布到 repository 中)。

持续集成工具

  • Travis : 开源项目一般采用 Travis 做持续集成,因为有免费的 Travis 持续集成服务可用,其他比如 Jenkins,Teamcity 都需要自己搭持续集成的服务。

  • Jenkins:

    • Jenkinsfile
    • 在 Jenkins 页面,配置 Pipeline, 指明使用 Jenkinsfile, 指明 Trigger 机制。
    • 配置 gitlab/github 的 Webhook, settings -> Merge request settings 配置。
  • Teamcity: 对于 Teamcity ,我就在 youtube 上看过 9 个视频,知之甚少。

网路编程

WSGI

Author:王蒙
Tags:网络编程
abstract:wsgi 协议是 Application 和 Server 通信的标准接口。要实现 wsgi 的 Application 和 Server 必须遵守 wsgi 协议。遵守这个协议,就可以混合使用各种 wsgi Application 和 wsgi Server。这和让各种牌子的电脑和U盘支持统一的 USB 口,那么任何电脑都可以和任意 U 盘连接是一个道理。

Audience

网络编程

Problem

  • wsgi 的优势
  • wsgi 协议
  • 写 wsgi 的方式
  • wsgi 的常见应用
  • wsgi 与 server 的兼容

Solution

wsgi 的优势
兼容性是 wsgi 的突出优势。 wsgi 接收请求,构造响应,实现了 Application 的处理逻辑。和采用指定 Web Frameworks 方式写 Application 不同,采用 wsgi 协议写的 Application ,可以兼容很多 Server。
wsgi 协议

PEP 3333 详细介绍了 wsgi 协议。

wsgi 协议对于 wsgi Application 的要求:
  • Application 是个 callable 的 object, 函数和类都可以。
  • Application callable 接收两个参数 envrionstart_response
    • environ: environ 包含了 http 请求的所有字段。详细 envrion 中必须包含哪些字段,请查考 PEP 3333
    • start_response: start_response 表示开始返回 response, start_response(status, headers) 接收 status(比如 ‘200 OK’) 和 headers(比如 [‘Content-Type’: 100]), wsgi 在调用 start_response(status, headers) 之后会用 return 或者 yield 把 response 的 body 传递出去。常见 headers 的含义,请参考 http 协议的标准。
  • Application 的返回值是由 bytes 组成的 iterable 的 object。

wsgi 协议对于 Server 的要求:

  • 可以把 Server 想象成之前用 socket 写的服务器,主要负责侦听用户的连接请求。
  • 当有 client 连接到服务器之后,Server 就把请求转成 envrion (附带些 Server 本身提供的信息)传递给 wsgi Application 去处理, wsgi Application 返回的 response 传递给 Server, Server 把这个 response 传递给 client。
写 wsgi 协议的方式

不使用第三方包的方式, 主要分两步:

  • environ 中解析出 http request 中的字段。
  • 根据 http request 的字段,构造 http response。

使用 webob 包:

  • webob 提供了 webob.Requestwebob.Response 方便解析 request 和构造 response。
  • timeapp_webob.py 是使用 webob 定义 wsgi 的例子。

使用 werkzeug 包:

  • 直观的 http 处理逻辑是,收到 http request, 返回 http response。werkzeug 允许我们定义一个接收 request 参数,返回 response 的函数。我们定义的这个函数经 werkzeug.wrappers.Request.application 装饰器装饰,就变成 wsgi。
  • werkzeug 提供的 request 和 response, 要比 wsgi 原生提供的 environ 和 start_response, 方便解析请求和构造响应。
  • timeapp_werkz.py 是使用 werkzeug 定义 wsgi 的例子。
wsgi 写 middleware

request/reponse 在 Server 和 Application 之间传递时,可以通过一个组件处理。这个组件就是中间件。比如 Dispatch 请求的逻辑可以写到中间件了。

wsgi Server

早期 python 性能不行,python 定义的 wsgi application 会装到 Apache Server 上。

后来,出现了一堆 python Server(gunicorn, flask, django 等等), python wsgi 也可以安装到这些 python server 上。gunicorn 运行 wsgi 最为方便。

wsgi 不能兼容到采用异步方式实现的 Server 中,比如 wsgi 和 tornado 不兼容(可以强行让 tornado 使用 wsgi 但是性能会打折扣)。实际上采用异步实现的 Server 兼容性很差,采用了某个异步 Server, 一般也就不得不采用 Server 指定的 Frameworks 去写 Application。

Reference

Restful

作者:王蒙
标签:网络编程
简介:Restful 是种软件设计风格。

目标读者

网络编程,微服务

问题

  • 为什么需要 Restful
  • Restful 风格的要求
  • 最佳实践

解决办法

  • 为什么需要 Restful

    当前开发需要多人协作,有效的沟通需要一套规范(解耦前端开发和后端开发)。Restful 接口就是这样一套规范。

    开发时,把一切数据,服务等等统统看成 Resource。使用 URI 定位 Resource,使用 METHOD 描述操作。力求做到

    • 看 Url 就知道要什么
    • 看 http method 就知道干什么
    • 看 http status code 就知道结果如何

    (这些是我从下面的参考文献中抄的,我认为言简意赅,写的非常好)

  • Restful 风格要求

    Fielding 的论文中写到 Restful 要求:
    • Identification of resources
    • Manipulation of resources through representations
    • Self-descriptive messages
    • Hypermedia as the engine of application state

    上面四句话太抽象。我看了 重新解析REST Service 才明白这四句话的意思。

  • 最佳实践

    • URI 用名词命名,不要用动词命名。比如你要做删除的动作,不要写成 GET /someresouce/delete, 应该写成 DELETE /someresource 。

    • 所有的动作使用 HTTP METHOD 实现。

    • GET, HEAD METHOD 不要对资源做任何修改。

    • 修改资源使用 POST, PUT, DELETE 等操作。

    • 新建资源使用 POST 不要使用 PUT。

    • PUT 方法要求是幂等的,POST 一般不是幂等的。

    • 表示资源集合的URI采用复数。常见的 Restful API 几乎表示资源集合都采用复数。表示资源集合中具体某一个资源,直接使用 /id 的表示。比如 /someresources/1 。

    • 出错时,Restful API 返回的响应中,应当包含 body, 该 body 一定要记录错误的信息(比如记录错误的原因,错误类型,错误发生在哪里等),一般 body 中也会包含错误码。Restful API 返回的响应的状态码以及响应的 body 中包含的错误码都应该采用 HTTP 协议规定的错误码,不要自定义错误码。

    • API 版本可以放在 header 中,也可以放在资源 URL 之前。如果资源有嵌套,请放在具体的资源之前,不要放在 URL 的根路径之前。

    • 尊重 HTTP 返回码规范,不要自定义返回码。常见的返回码,以及返回码的含义如下:

      • 2XX 表示成功
      • 3XX 表示重定向
      • 4XX 表示客户端错误
      • 5XX 表示服务器端错误
      • 200 ok
      • 301 永久重定向
      • 302 临时重定向
      • 304 缓存资源没有改变
      • 401 未认证
      • 403 没有权限
      • 404 找不到资源
  • 优秀 RestAPI 范例

  • 我踩过的坑

    • 资源集合没有用复数。
    • 自定义了错误码。
    • 使用 PUT 创建资源。
    • API 版本号放在 URL 末尾。

参考文献

消息队列

Author:王蒙
Tags:网络编程
abstract:消息队列能够方便地组织多进程的通信方式。

Audience

网络编程,分布式

Problem

  • 常见的消息队列
  • 如何使用消息队列

Solution

  • 常见的消息队列

    RabbitMQ, ZeroMQ, Kafka 都是常用的消息队列。

    消息队列确保:

    • 消息可靠的传输
    • 消息是完整的

    常见消息队列的特点:

    • ZeroMQ

      • 没有中心化的 message broker.
    • RabbitMQ

      • 需要中心化的 message broker.
  • 如何使用消息队列

    使用消息队列最重要的就是要确定,消息网的拓扑结构。

    常见的结构有:

    • pipeline
    • fanout
    • request-reply
    • route

Reference

  • Foundations of Python Networks Programming

socket 编程

网络协议

ARP protocol

Author:王蒙
Tags:网络协议,网络编程
abstract:ARP 是什么? 为什么需要 ARP?

Audience

运维,网络应用开发者

Prerequisites

Write the list of prerequisites for implementing this recipe. This can be additional documents, software, specific libraries, environment settings or just anything that is required beyond the obvious language interpreter.

Problem

  • ARP 是什么?
  • 为什么需要 ARP?

Solution

ARP 是什么?

ARP 把 32 位 IP 地址转成 48 位以太网地址; RARP 把 48 位以太网地址转成 32 位 IP 地址。

为什么需要 ARP?

当一台主机把以太网数据帧发送到同一局域网上的另一台主机时,是根据 48 bit 的以太网地址来确定目的接口的。设备驱动程序从不检查 IP 数据报中的目的IP地址。 就是说目的 IP 负责选择 route 路径,以太网地址负责送以太网数据帧到指定的主机。

ARP 怎么做?

步骤:

  1. 在局域网广播 ARP请求, ARP 请求帧中包含目的主机的 IP 地址,其意思是 “如果你是这个 IP 地址的拥有者,请回答你的硬件地址”。
  2. 匹配 ARP 请求中的 IP 地址的目的主机,发送一个 ARP 应答。这个应答中包含 IP 地址及对应的硬件地址。

ARP 请求和应答:

_images/ARP.png

ARP 告诉缓存?

ARP IP 地址和硬件地址的对应表会被缓存一段时间,以提高效率。 arp -a 显示缓存的对应表。

% arp -a
sun (140.252.13.33) at 8:0:20:3:f6:42
svr4 (140.252.13.34) at 0:0:c0:c2:9b:26

Reference

Put here references, and links to other documents.

  • tunnel

    1. fiddler 能解码 tunnel 中的消息吗?

      下面这段话,摘自《http 权威指南》:

      Because the tunneled data is opaque to the gateway, the gateway cannot make any
      assumptions about the order and flow of packets. Once the tunnel is established,
      data is free to flow in any direction at any time.
      

      按这段话的说法,proxy 没法查看 tunnel 中的数据。proxy 能够看到 tunnel 中的所有 packet, 但是 proxy 没法拼接,解码出来 这些信息。

      我发现,使用 fiddler 可以解码 https tunnel 中的消息。我抓取了如下消息,说明 fiddler 可以抓取 https tunnel 中的消息。

      Fiddler 能不能解码 tunnel 中走其他协议的 TCP 消息,尚未可知。

      HTTP/1.1 200 Connection Established
      FiddlerGateway: Direct
      StartTime: 11:17:39.311
      Connection: close
      
      Encrypted HTTPS traffic flows through this CONNECT tunnel. HTTPS Decryption is enabled in Fiddler, so decrypted sessions running in this tunnel will be shown in the Web Sessions list.
      
      Secure Protocol: Tls12
      Cipher: Aes128 128bits
      Hash Algorithm: Sha256 256bits
      Key Exchange: RsaKeyX 2048bits
      
      == Server Certificate ==========
      [Subject]
        CN=*.jd.com, O="BEIJING JINGDONG SHANGKE INFORMATION TECHNOLOGY CO., LTD.", L=beijing, S=beijing, C=CN
      
      [Issuer]
        CN=GlobalSign Organization Validation CA - SHA256 - G2, O=GlobalSign nv-sa, C=BE
      
      [Serial Number]
        3A755F6565BC2363315084FF
      
      [Not Before]
        2017/12/29 12:52:02
      
      [Not After]
        2018/8/28 17:42:54
      
      [Thumbprint]
        91D298115B56679EBA1ECD38CDFA0368388970C2
      
      [SubjectAltNames]
      *.jd.com, *.3.cn, *.360buy.com, *.360buyimg.com, *.7fresh.com, *.baitiao.com, *.caiyu.com, *.chinabank.com.cn, *.jd.co.th, *.jd.hk, *.jd.id, *.jd.ru, *.jdpay.com, *.jdx.com, *.joybuy.com, *.joybuy.es, *.jr.jd.com, *.kmall.jd.com, *.m.jd.com, *.m.paipai.com, *.m.yhd.com, *.paipai.com, *.toplife.com, *.wangyin.com, *.yhd.com, *.yihaodianimg.com, *.yiyaojd.com, 3.cn, 360buy.com, 360buyimg.com, 7fresh.com, baitiao.com, caiyu.com, chinabank.com.cn, jd.co.th, jd.hk, jd.id, jd.ru, jdpay.com, jdx.com, joybuy.com, joybuy.es, paipai.com, toplife.com, wangyin.com, yhd.com, yihaodianimg.com, yiyaojd.com, jd.com
      
    2. 如果 tunnel 是为了与 non-http server 连接,那么为什么非要走 http 的 connect 请求?

  • TCP 三次握手,四次挥手。

  • TCP 重发机制。

  • 走 TCP 协议的

frame

Author:王蒙
Tags:网络协议,网络编程
abstract:网络是一帧一帧数据传输的。常见的抓包工具,抓到的一个包,其实就是一帧数据。这个小节,介绍下帧的组成。

Audience

运维,网络开发者等。

Problem

帧(Frame)有哪些部件组成。

Solution

帧(Frame)包含:

  • 以太网头部

  • IP 头

  • TCP 头或者 UDP 头

  • 应用数据

  • 以太网尾部

帧的组成,刚好对应四层网络结构。

Reference

Put here references, and links to other documents.

IP packet

Author:王蒙
Tags:网络协议,网络编程
abstract:介绍 IP packet 中包含哪些字段,这些字段有什么用

Audience

运维,网络应用开发者

Problem

IP packet 中有哪些字段,这些字段有什么用

Solution

IP packet 中包括 IP 头数据。数据就是要传输的消息。

IP 头长为 20 字节,除非有选项字段。

IP 头中包含的字段有:

  • 4 位版本: 表明是 IPv4 还是 IPv6。

  • 4 位头长度: 表明 IP 头长度(32 bit 为单位),因为 IP 可能包含选项字段,所以是变长的。因为只有4位,所以TCP首部最长60字节。

  • 16位总长度: 表明包含数据在内的总长度(字节数)。

  • 16位标识:IP packet 的唯一标识,因为 IP packet 在传输过程中,可能会分片,可能需要重新组装。所以需要 IP packet 的唯一标识。

  • 3 位标志:

    • 一个位表示是否还有 更多片段,如果还有就该该比特位设成 1, 否则设成 0 。
    • 不分片位,设为1时,强制不分片。如果必须分片,就丢弃该 IP packet, 并发送 ICMP 报文。
  • 13 位片偏移:

    • 该 IP packet 片相对于原来 IP packet 的偏移量。
  • 8 位生存时间(TTL):IP packet 可以经过的最多路由器数目。TTL 的初始值由源主机设置,一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,IP packet 就会被丢弃。

  • 8 位协议:传输采用的一些,比如 ICMP, IGMP, TCP, UDP 等等。

  • 16 位头部校验和:校验IP packet 是否被篡改。如果发现被篡改了,那么 IP packet 会被丢弃,一般还会被重传。

  • 32 位源IP地址。

  • 32 位目的IP地址。

  • 选项(可选):略

Reference

Put here references, and links to other documents.

Route

Author:王蒙
Tags:网络协议
abstract:IP 路由

Audience

运维,网络应用开发

Prerequisites

IP,子网等。

Problem

IP 如何根据路由表路由? 如何修改 IP 路由表?

Solution

vagrant@precise64:~$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         localhost       0.0.0.0         UG    0      0        0 eth0
default         localhost       0.0.0.0         UG    100    0        0 eth0
10.0.2.0        *               255.255.255.0   U     0      0        0 eth0
  1. frame 中目的IP 地址始终不变,整

Reference

Put here references, and links to other documents.

TCP 三次握手四次挥手

Author:王蒙
Tags:网络协议,网络编程
abstract:TCP 使用三次握手四次握手构建安全有效率的链接。

Audience

构建网络服务的开发者,想要了解TCP链接的开发者。

Prerequisites

Write the list of prerequisites for implementing this recipe. This can be additional documents, software, specific libraries, environment settings or just anything that is required beyond the obvious language interpreter.

Problem

为什么要使用三次握手来构建连接,为什么要使用四次挥手来关闭连接。简单两次握手,一次挥手不好吗?

Reference

Put here references, and links to other documents.

tcpdump

Author:王蒙
Tags:Linux, 网络协议,网络编程
abstract:tcpdump 是 linux 中的抓包工具。使用这个工具,能直观地理解 TCP/IP 中的很多概念。

Audience

Linux 运维,网络应用开发者

Prerequisites

简单的 linux 操作。

Problem

如何使用 tcpdump 抓包?

  • 抓取
  • 过滤
  • 保存和分析

Solution

# tcpdump 需要 sudo 权限,所有之后的脚本都会带上 sudo
# 查看所有网络接口, any 接口表示所有接口
$ sudo tcpdump -D
1.eth0
2.any (Pseudo-device that captures on all interfaces)
3.lo
# 抓取通过指定网络接口(比如 eth0 网络接口)的包
# -c 5 表示抓到 5 个包,就停止抓取
$ sudo tcpdump -i eth0 -c 5
...
# 抓取通过任意网络接口的包
$ sudo tcpdump -i any -c 5
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:31:13.146724 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565789445:2565789613, ack 21252087, win 22032, length 168
13:31:13.146919 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 168, win 65535, length 0
13:31:13.147110 ARP, Request who-has 10.0.2.3 tell 10.0.2.15, length 28
13:31:13.147189 ARP, Reply 10.0.2.3 is-at 52:54:00:12:35:03 (oui Unknown), length 46
13:31:13.147197 IP 10.0.2.15.51011 > 10.0.2.3.domain: 58957+ PTR? 2.2.0.10.in-addr.arpa. (39)
5 packets captured
12 packets received by filter
0 packets dropped by kernel

# capture size 65535 bytes, 表示每个包最多抓取 65536 byte。IP 包最大 65536 bytes 所以这意味着全抓。可以使用 -s 指定抓取大小。
# 每个包抓取头 96 bytes。
$ sudo tcpdump -i any -c 5 -s 96
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 96 bytes
13:35:40.944878 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565790245:2565790285, ack 21252295, win 22032, length 40
13:35:40.945084 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 40, win 65535, length 0
13:35:40.945482 IP 10.0.2.15.52160 > 10.0.2.3.domain: 36116+ PTR? 2.2.0.10.in-addr.arpa. (39)
13:35:40.985494 IP 10.0.2.3.domain > 10.0.2.15.52160: 36116 NXDomain 0/1/0 (89)
13:35:40.985671 IP 10.0.2.15.49613 > 10.0.2.3.domain: 47324+ PTR? 15.2.0.10.in-addr.arpa. (40)
5 packets captured
19 packets received by filter
0 packets dropped by kernel
# 抓取结果中,第一列是抓取时间,第二列是该包采取的协议,第三列是源 socket(主机地址和端口号),第四列是目标socket(地址和端口号)。
# 上面结果中主机地址是域名,端口号是应用名。如果想使用IP 地址和数字形式的端口号,需使用 -n 选项。
$ sudo tcpdump -i any -c 5 -n
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:39:14.240890 IP 10.0.2.15.22 > 10.0.2.2.64407: Flags [P.], seq 2565791277:2565791301, ack 21252919, win 22032, length 24
13:39:14.241081 IP 10.0.2.2.64407 > 10.0.2.15.22: Flags [.], ack 24, win 65535, length 0
13:39:14.241202 IP 10.0.2.15.22 > 10.0.2.2.64407: Flags [P.], seq 24:112, ack 1, win 22032, length 88
13:39:14.241337 IP 10.0.2.2.64407 > 10.0.2.15.22: Flags [.], ack 112, win 65535, length 0
13:39:14.241470 IP 10.0.2.15.22 > 10.0.2.2.64407: Flags [P.], seq 112:168, ack 1, win 22032, length 56
5 packets captured
6 packets received by filter
0 packets dropped by kernel
# 第五列表示消息的Flag 信息,具体来说 P 表示 PSH, . 表示 ACK, S 表示 SYN, F 表示 FIN, R 表示 RST, U 表示 URG(刚好是 IP 头中的 6 个保留位)。
# 第六列表示 sequence number: acknowledge number, 除了第一列完整显示之外,下面的列都是用相对值。第七列表示 window size。第八列表示包的大小,
# 刚好等于 acknowledge number - sequence number。
# todo: window size 我不理解,需要我再研究下。


# 使用过滤,可以使我们专注观察我们感兴趣的包。不然那么多包,把关注点给隐藏了。
# host, 选通过某个 host 的包
# src host, 选从某个 host 发出的包
# des host, 选被某个 host 接受的包
$ sudo tcpdump -i any -c 5
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
14:00:58.192522 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565794341:2565794365, ack 21254391, win 22032, length 24
14:00:58.192638 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 24:48, ack 1, win 22032, length 24
14:00:58.192693 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 24, win 65535, length 0
14:00:58.192702 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 48, win 65535, length 0
14:00:58.192774 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 48:72, ack 1, win 22032, length 24
5 packets captured
31 packets received by filter
0 packets dropped by kernel
$ sudo tcpdump -i any -c 5 host 119.75.213.61
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
14:01:08.443447 IP 10.0.2.15 > 127.0.0.1: ICMP echo request, id 1338, seq 58, length 64
14:01:08.453229 IP 127.0.0.1 > 10.0.2.15: ICMP echo reply, id 1338, seq 58, length 64
14:01:09.444495 IP 10.0.2.15 > 127.0.0.1: ICMP echo request, id 1338, seq 59, length 64
14:01:09.452179 IP 127.0.0.1 > 10.0.2.15: ICMP echo reply, id 1338, seq 59, length 64
14:01:10.445870 IP 10.0.2.15 > 127.0.0.1: ICMP echo request, id 1338, seq 60, length 64
5 packets captured
5 packets received by filter
0 packets dropped by kernel

# 使用 port 选项,捕获通过指定端口的包
$ sudo tcpdump -i any -c 5 -n port 80
$ sudo tcpdump -i any -c 5 port 80 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
14:05:28.479440 IP 10.0.2.15.39821 > 119.75.213.61.80: Flags [S], seq 2125492756, win 14600, options [mss 1460,sackOK,TS val 1012393 ecr 0,nop
,wscale 3], length 0
14:05:28.489360 IP 119.75.213.61.80 > 10.0.2.15.39821: Flags [S.], seq 538112001, ack 2125492757, win 65535, options [mss 1460], length 0
14:05:28.489387 IP 10.0.2.15.39821 > 119.75.213.61.80: Flags [.], ack 1, win 14600, length 0
14:05:28.489624 IP 10.0.2.15.39821 > 119.75.213.61.80: Flags [P.], seq 1:114, ack 1, win 14600, length 113
14:05:28.489805 IP 119.75.213.61.80 > 10.0.2.15.39821: Flags [.], ack 114, win 65535, length 0
5 packets captured
5 packets received by filter
0 packets dropped by kernel

# 选择 Flag
$ sudo tcpdump -i any -c 10 "tcp[tcpflags] & tcp-syn != 0"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
15:04:47.472034 IP 10.0.2.15.51509 > 10.136.7.81.ssh: Flags [S], seq 4251015673, win 14600, options [mss 1460,sackOK,TS val 1902142 ecr 0,nop,
wscale 3], length 0
15:04:48.471786 IP 10.0.2.15.51509 > 10.136.7.81.ssh: Flags [S], seq 4251015673, win 14600, options [mss 1460,sackOK,TS val 1902392 ecr 0,nop,
wscale 3], length 0
15:04:50.476360 IP 10.0.2.15.51509 > 10.136.7.81.ssh: Flags [S], seq 4251015673, win 14600, options [mss 1460,sackOK,TS val 1902893 ecr 0,nop,
wscale 3], length 0
15:04:54.488758 IP 10.0.2.15.51509 > 10.136.7.81.ssh: Flags [S], seq 4251015673, win 14600, options [mss 1460,sackOK,TS val 1903896 ecr 0,nop,
wscale 3], length 0
15:05:02.504585 IP 10.0.2.15.51509 > 10.136.7.81.ssh: Flags [S], seq 4251015673, win 14600, options [mss 1460,sackOK,TS val 1905900 ecr 0,nop,
wscale 3], length 0


# 使用 ascii 码显示包中的内容
$ sudo tcpdump -i any -c 5 -A
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
15:07:05.941319 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565812773:2565812797, ack 21263031, win 24624, length 24
E..@..@.@.d.
...
.........2%.Dr.P.`0.C..S,......O&{...c.%....S..
15:07:05.941525 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 24, win 65535, length 0
E..(.5..@.O.
...
........Dr...2=P...\.........
15:07:05.941674 IP 10.0.2.15.41571 > 10.0.2.3.domain: 40153+ PTR? 2.2.0.10.in-addr.arpa. (39)
E..C.w@.@..!
...
....c.5./.R.............2.2.0.10.in-addr.arpa.....
15:07:05.947887 IP 10.0.2.3.domain > 10.0.2.15.41571: 40153 NXDomain 0/1/0 (89)
E..u.6..@.O1
...
....5.c.a...............2.2.0.10.in-addr.arpa......10.IN-ADDR.ARPA............'.......p.... .   :...Q.
15:07:05.948125 IP 10.0.2.15.55537 > 10.0.2.3.domain: 11910+ PTR? 15.2.0.10.in-addr.arpa. (40)
E..D.y@.@...
...
......5.0.S.............15.2.0.10.in-addr.arpa.....
5 packets captured
10 packets received by filter
0 packets dropped by kernel

# 使用 ascii + hex 的方式展示内容
$ sudo tcpdump -i any -c 5 -XX
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
15:08:02.905565 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565813213:2565813237, ack 21263151, win 24624, length 24
        0x0000:  0004 0001 0006 0800 2788 0ca6 0000 0800  ........'.......
        0x0010:  4500 0040 bdbb 4000 4006 64ec 0a00 020f  E..@..@.@.d.....
        0x0020:  0a00 0202 0016 fb97 98ef 33dd 0144 732f  ..........3..Ds/
        0x0030:  5018 6030 1843 0000 ddaa 9999 0604 c552  P.`0.C.........R
        0x0040:  3f2f 0b49 ce07 bab8 4b9a 0f9f 72ac adbc  ?/.I....K...r...
15:08:02.905785 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 24, win 65535, length 0
        0x0000:  0000 0001 0006 5254 0012 3502 0000 0800  ......RT..5.....
        0x0010:  4500 0028 1346 0000 4006 4f7a 0a00 0202  E..(.F..@.Oz....
        0x0020:  0a00 020f fb97 0016 0144 732f 98ef 33f5  .........Ds/..3.
        0x0030:  5010 ffff 5abe 0000 0000 0000 0000       P...Z.........
15:08:02.905945 IP 10.0.2.15.33627 > 10.0.2.3.domain: 37078+ PTR? 2.2.0.10.in-addr.arpa. (39)
        0x0000:  0004 0001 0006 0800 2788 0ca6 0000 0800  ........'.......
        0x0010:  4500 0043 c518 4000 4011 5d80 0a00 020f  E..C..@.@.].....
        0x0020:  0a00 0203 835b 0035 002f 1852 90d6 0100  .....[.5./.R....
        0x0030:  0001 0000 0000 0000 0132 0132 0130 0231  .........2.2.0.1
        0x0040:  3007 696e 2d61 6464 7204 6172 7061 0000  0.in-addr.arpa..
        0x0050:  0c00 01                                  ...
15:08:02.914533 IP 10.0.2.3.domain > 10.0.2.15.33627: 37078 NXDomain 0/1/0 (89)
        0x0000:  0000 0001 0006 5254 0012 3502 0000 0800  ......RT..5.....
        0x0010:  4500 0075 1347 0000 4011 4f20 0a00 0203  E..u.G..@.O.....
        0x0020:  0a00 020f 0035 835b 0061 9def 90d6 8183  .....5.[.a......
        0x0030:  0001 0000 0001 0000 0132 0132 0130 0231  .........2.2.0.1
        0x0040:  3007 696e 2d61 6464 7204 6172 7061 0000  0.in-addr.arpa..
        0x0050:  0c00 0102 3130 0749 4e2d 4144 4452 0441  ....10.IN-ADDR.A
        0x0060:  5250 4100 0006 0001 0000 2237 0017 c027  RPA......."7...'
        0x0070:  0000 0000 0000 0070 8000 001c 2000 093a  .......p.......:
        0x0080:  8000 0151 80                             ...Q.
15:08:02.914768 IP 10.0.2.15.46318 > 10.0.2.3.domain: 9065+ PTR? 15.2.0.10.in-addr.arpa. (40)
        0x0000:  0004 0001 0006 0800 2788 0ca6 0000 0800  ........'.......
        0x0010:  4500 0044 c51a 4000 4011 5d7d 0a00 020f  E..D..@.@.]}....
        0x0020:  0a00 0203 b4ee 0035 0030 1853 2369 0100  .......5.0.S#i..
        0x0030:  0001 0000 0000 0000 0231 3501 3201 3002  .........15.2.0.
        0x0040:  3130 0769 6e2d 6164 6472 0461 7270 6100  10.in-addr.arpa.
        0x0050:  000c 0001                                ....
5 packets captured
10 packets received by filter
0 packets dropped by kernel


# 选择子网, net, src net, des net


# 过滤协议,直接在后面写上协议名就好
$ sudo tcpdump -i any -c 5 -n icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
14:27:19.751355 IP 10.0.2.15 > 119.75.213.61: ICMP echo request, id 1369, seq 1, length 64
14:27:19.760170 IP 119.75.213.61 > 10.0.2.15: ICMP echo reply, id 1369, seq 1, length 64
14:27:20.752639 IP 10.0.2.15 > 119.75.213.61: ICMP echo request, id 1369, seq 2, length 64
14:27:20.760858 IP 119.75.213.61 > 10.0.2.15: ICMP echo reply, id 1369, seq 2, length 64
14:27:21.754795 IP 10.0.2.15 > 119.75.213.61: ICMP echo request, id 1369, seq 3, length 64
5 packets captured
5 packets received by filter
0 packets dropped by kernel


# 支持使用 and or 来表示符合的过滤条件
$ sudo tcpdump -i any -c 5 -n "icmp and (port 80)"


# 把捕获的包写入文件, -w
$ sudo tcpdump -i any -c 10 -w debug.pcap

# 读取刚刚写出的 debug.pcap 文件
$ sudo tcpdump -r debug.pcap
reading from file debug.pcap, link-type LINUX_SLL (Linux cooked)
14:29:20.997275 IP 10.0.2.15.ssh > 10.0.2.2.64407: Flags [P.], seq 2565798981:2565799021, ack 21257223, win 22032, length 40
14:29:20.997521 IP 10.0.2.2.64407 > 10.0.2.15.ssh: Flags [.], ack 40, win 65535, length 0
14:29:21.005056 IP 127.0.0.1 > 10.0.2.15: ICMP echo reply, id 1369, seq 122, length 64
14:29:21.005215 IP 10.0.2.15.44211 > 10.0.2.3.domain: 4253+ PTR? 61.213.75.119.in-addr.arpa. (44)
14:29:21.013607 IP 10.0.2.3.domain > 10.0.2.15.44211: 4253*- 1/0/0 PTR 127.0.0.1. (67)

开发和部署

Python 发布应用:

  1. 以脚本发布,适用于较小的项目
  2. 以 Python 安装包发布
  3. 以 X As a Service 的方式,发布服务

Python 开发和部署一般使用独立虚拟的 Python 环境,这样使得所有开发者使用相同的环境做开发,使得开发和部署的环境尽可能一致。

虚拟 python 环境

Author:王蒙
Tags:python 环境,开发环境
abstract:项目比较复杂的时候,可能需要构建虚拟的 Python 环境。虚拟Python环境不仅使项目可以有独立的包环境;而且方便重现项目需要的Python环境。

Audience

Python 开发

Prerequisites

Python, shell 脚本的执行

Problem

  • 什么时候需要虚拟 Python 环境
  • 如何构建虚拟 Python 环境
  • 进入/退出虚拟 Python 环境
  • 重现 Python 环境
  • pyenv 管理多个版本的 Python 解释器

Solution

  • 什么时候需要虚拟 Python 环境

    虚拟 Python 环境带来的最大好处是:

    • 项目独立的包环境。
    • 方便重现项目的 Python 环境。

    包依赖比较复杂时,需要虚拟的 Python 环境。

    Pycharm 使得操作虚拟 Python 环境非常简单,有 Pycharm 你可以一直用虚拟Python 环境。

  • 构建虚拟 Python 环境

    pyvenvvirtualenv 是构建虚拟 Python 环境的工具。而且两种工具的使用几乎完全一样。文本以 pyvenv 为例做介绍。

    # 构建名为 wangmeng-python Python 环境,名称可以随便取
    $ pyvenv wangmeng-python
    

    构建的结果是一个和之前 Python 版本相同,但只装了包管理工具(pip, setuptools 等),没有其他第三方包的干净的 Python 环境。

  • 进入/退出虚拟解释器环境

    $ cd wangmeng-python
    # 下一步有坑
    $ . ./bin/activate
    (pyvenv-python) $ which python
    /root/wangmeng-python/virtualenv-python/bin/python
    # 已经进入虚拟 Python 环境
    # 退出虚拟 Python 环境
    (pyvenv-python) $ deactivate
    $
    

    踩过的坑:

    • 执行 activate 脚本时,一定要使用 source 或者 . 的方式执行该脚本。

      切记必须以 . 或者 source 来执行 activate 脚本。sh activate 是没法让 linux shell 进入虚拟 Python 环境的。

      因为要修改 PATH 等等环境变量,且必须在当前 shell 进程中生效,所以必须使用 source 或者 . 来执行 activate 脚本。

  • 重现 Python 环境

    # 把当前环境的包依赖写入 requirements.txt。
    $ pip freeze > requirements.txt
    
    # 找个干净的(除了装包工具之外,没再装任何第三方包) Python 环境(Python 解释器版本要和之前的版本一致)。
    # 一般使用 **pyvenv** 或者 **virtualenv** 构建这样的环境。
    $ pyvenv restore-wangmeng-python
    $ . ./bin/activate
    # 在干净的环境中,使用 requirements.txt 装包,就能还原之前的 wangmeng-python 解析器环境。
    $ pip install -r requirements.txt
    
  • pyenv 管理多个版本的 Python 解释器。

    参考 https://github.com/pyenv/pyenv-installer 安装 pyenv, 在类 Unix 环境安装非常简单,在 windows 上我没有安装成功。

    下载安装新版本的 Python

    $ pyenv install 2.7.3
    Downloading Python-2.7.3.tgz...
    -> https://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz
    Installing Python-2.7.3...
    patching file ./Modules/readline.c
    Hunk #1 succeeded at 200 (offset -6 lines).
    Hunk #2 succeeded at 735 (offset -14 lines).
    Hunk #3 succeeded at 845 (offset -14 lines).
    Hunk #4 succeeded at 893 with fuzz 2 (offset -25 lines).
    Ignoring potentially dangerous file name ../Python-2.7.3/Lib/site.py
    patching file ./Lib/site.py
    patching file ./Lib/ssl.py
    Hunk #2 succeeded at 424 (offset -11 lines).
    patching file ./Modules/_ssl.c
    Hunk #1 succeeded at 65 (offset -2 lines).
    Hunk #2 succeeded at 304 (offset -4 lines).
    Hunk #3 succeeded at 1725 (offset -87 lines).
    WARNING: The Python readline extension was not compiled. Missing the GNU readline lib?
    WARNING: The Python bz2 extension was not compiled. Missing the bzip2 lib?
    WARNING: The Python sqlite3 extension was not compiled. Missing the SQLite3 lib?
    Installing pip from https://bootstrap.pypa.io/get-pip.py...
    Installed Python-2.7.3 to /home/wangmeng/.pyenv/versions/2.7.3
    

    查看当前系统可用的 Python 版本

    $ pyenv versions
    * system (set by /home/wangmeng/.pyenv/version)
      2.7.3
    

    在当前目录下,选择使用特定版本的 Python 解释器

    $ pyenv local 2.7.3
    $ pyenv versions
      system
    * 2.7.3 (set by /home/wangmeng/.python-version)
    

    在全局,选择使用特定版本的 Python 解释器

    $ pyenv global 2.7.3
    
    $ pyenv shell 2.7.3
    

    卸载 Python 解释器

    $ pyenv uninstall 2.7.3
    $ pyenv versions
    * system (set by /home/wangmeng/.python-version)
    

Vagrant

作者:王蒙
标签:Python 开发,构建开发环境
简介:vagrant 能够方便地为开发提供虚拟执行环境。比如你使用 windows 的系统,但是开发必须基于 linux 环境(比如可能是因为某个 python 包只能运行在 linux 环境下)。这时候,可以方便地使用 vagrant 启动一个虚拟机来做开发。

目标读者

Python 开发

问题

  • vagrant 和 docker 相比有哪些优势和劣势
  • vagrant 基本使用方法
  • 配置 VagrantFile 文件

解决办法

vagrant 和 docker 相比有哪些优势和劣势?

vagrant 用于提供开发环境,还是比 Docker 要方便一点:

  • Pycharm 使用 Vagrant 提供的 Python Interpreter, 代码能够自动补全。Pycharm 使用 Docker Interpreter 时,代码不能自动补全。
  • vagrant init 自动建立宿主机和虚拟机的目录对应关系,使得在虚拟机中直接就有当前工程的所有代码。如果使用 Docker 的话,需要自己去配置 Volume (配置目录对应关系)。
  • Pycharm 使用 Docker Interpreter 执行代码,每次都会创建,启动,停止 Docker Container, 这些步骤拖慢了程序的执行,影响了开发效率。Pycharm 使用 vagrant Interpreter 执行代码,就和在本地执行代码一样,没有任何 overhead(不必重启虚拟机),所以速度比 Docker 快。

在部署方面,Docker 几乎完胜 vagrant。不过 vagrant 的虚拟机可以在 windows 操作系统上运行 linux 操作系统的虚拟机。但是 Docker 启动的 container 的操作系统内核必须和宿主机的操作系统内核大致相同。

在 Win10 操作系统下,要运行 docker 必须使用 Hyper-V , 而使用了 Hyper-V 导致 VirtualBox 不能正常使用,导致 vagrant 必须也使用 Hyper-V。vagrant 使用 Hyper-V 特别别扭,完全抵消了 vagrant 带来的便利。所以在 Win10 下,在当前(未来可能有改进),还是采用 Docker 提供虚拟化的环境比较合适。

Vagrant 基本使用方法?
  • vagrant 最常用的命令

    # 在工程的 root 目录下执行如下语句,会生成 VagrantFile 配置文件
    $ vagrant init {box_name}
    # 执行如下语句,会根据刚才生成的配置文件,启动一个虚拟机
    $ vagrant up
    # vagrant up 的时候,可以使用 Virtualbox 启动虚拟机,也可以使用 Hyper-V 启动虚拟机.
    # 执行如下语句,进入启动的虚拟机
    $ vagrant ssh
    
  • 搜索 box, 下载 box, 把 box 添加到本地仓库

    可以到 VagrantCloud 搜索你需要的 box。

    理论上 vagrant init {box_name} 就能下载box, 但是实际在中国,下载 box 非常慢。我发现充了会员的迅雷,下载 vagrant box 非常快。你可以用迅雷先把 box 下载到本地。

    下面的代码把迅雷下载的 box 文件,添加到本地 box 仓库。然后就可以使用 vagrant init, vagrant up 来构建开发环境。

    # 把保存在 {local_box_path} 的 box 添加到本地 box 仓库中,在本地 box 仓库中这个 box 的名为 {box_name} 。
    $ vagrant box add {box_name} {local_box_path}
    
    # 有了上一步你可以 执行 vagrant init  和 vagrant up 了。
    $ vagrant init {box_name}
    $ vagrant up
    
  • Pycharm 使用 vagrant Interpreter

    Pycharm 可以在 settings -> python interpreter -> Add remote Interpreter 中选择使用 Vagrant Interpreter。 Pycharm 能够根据 vagrant 虚拟机的 python 环境自动补全代码。比如虚拟机中安装了 pandas 包,那么在 pycharm 中写 import pa, pycharm 会自动提示要输入 ndas。

配置 VagrantFile 文件?

vagrant init 命令生成的 VagrantFile 每行配置之前都有详细的注释,看注释就能明白这行配置是干什么的。

VagrantFile 中重要的配置有:

  • 同步目录(类似于 Docker 中的 Volume)

    config.vm.synced_folder  "/Users/helei/www", "/vagrant"
    
  • 端口转发(类似于 Docker 中的 port)

    config.vm.network :forwarded_port, guest: 80, host: 80
    
  • 是否能够访问互联网

    # 不能访问互联网
    config.vm.network "private_network", ip: "192.168.33.10"
    # 能访问互联网
    #config.vm.network "public_network"
    

打包发布

作者:王蒙
标签:Python 打包,发布代码
简介:介绍 python 如何打包发布。

目标读者

Python 开发

预备知识

Python 基础语法

解决办法

打包

PyPA(Python Package Authorization) 推荐使用 setuptools 给 Python 打包。PyPA 还提供了 Python Packing User Guide 文档 作为 Python 打包的权威指南。

PyPA 推荐的包管理工具如下

  1. pip 装包
  2. setuptools 打包
  3. wine 上传包
  4. virtualenv or venv 构建项目级别的独立开发环境
  5. wheel 能打成 wheel 包尽量导成 wheel 包,至少打个针对 windows 环境的包(如果包要在 windows 环境上运行)。
  6. pypi(warehouse 有取代 pypi 的可能)
setup.py
  1. setup.py 一般会读取 README.rst 的内容,把 README.rst 作为 setup 函数中的 long_description 的值。
  2. setup.py 一般会读取 project 下的 __init__.py 文件中的 __VERSION__ 变量值,以这个值作为 setup 函数的 version 参数值。
  3. 项目的 dependencies 写到 setup.py 文件的 install_requires 参数中。
  4. setup.py 中的 entry_points 选项可以自定义 command。自定义 command 在打包后可以直接使用。
Source package versus built packages
sdist

以源码形式打包,就是说包解压之后是文本形式的源代码。

python setup.py sdist

bdist

以二进制形式打包,就是说包解压之后是二进制。

python setup.py bdist

# 推荐的做法,打 wheel 包 python setup.py bdist_wheel

开发过程引用正在开发的包

pip install -e <project-path>

python setup.py develop

是以开发模式安装包,对包的改动,可以立即反应到当前 Python 环境中。

Namespace Packages

为什么要用 Namespace Packages?

Namespace Package 相当于是比 package 更高一级的抽象。如果包下面包含子包,而子包需要独立发布。这时候,你需要 Namespace Package。Namespace package 中的每一个 package 可以单独发布和安装。

《expert python programming》 原话是这么说的:Namespace packages are especially useful if you have your application components developed, packaged, and versioned independently but you still want to access them from the same package.

定义 Namespace Packages

Python3 只有目录下有 python 源码,且该目录下没有 __init__.py,那么这个目录就是个 Namespace。

Python2 定义 Namespace Package 需要在 setup.py 文件中,指明 namespace_packages, 而且就算是 namespace package, 该目录下也 需要有 __init__.py 文件。Python2 中定义 namespace package 的 setup.py 文件,大致如下:

from setuptools import setup

setup(
    name='acme.templating',
    packages=['acme.templating'],
    # python2 必须指明 namespace_packages
    namespace_packages=['acme'],
)

不过简单地在 setup.py 中指明 namespace_package 可能被人遗忘,所以在 Python2 中,最好在 namespace 下的 __init__.py 文件中写上

__import__('pkg_resources').declare_namespace(__name__)
上传包到 index

先打包,打完包后使用

# 上传到 index 中
twine upload dist/*
# 在 index 中 register(这一句现在貌似不需要了)
twine register dist/*
.pypirc
[distutils]
index-server =
    pypi
    other

[pypi]
repository: <repository-url>
username: <username>
password: <password>

[other]
repository: <repository-url>
username: <username>
password: <password>

.pypirc is supported by pip, twine, distutils, and setuptools.

Standalone executables

Popular Tools:

  1. PyInstaller
  2. cx_Freeze
  3. py2exe and py2app
Security of Python code in executable packages

最安全的方式是使用 RESTFUL 接口提供服务。

linux 包管理工具

作者:王蒙
标签:linux, DevOps, Docker, Vagrant, yum, apt-get
简介:介绍 linux 上的 yum 和 apt-get 装包工具。

准备知识

简单的 bash 命令,Docker

问题

  • 安装源
  • 常用命令
  • 在 Docker 如何使用

解决办法

  • 安装源

    为了提高下载包的速度,需要更换为国内源。

  • 常用命令

    yum
    • yum makecache

      缓存源服务器的包信息(都有哪些包,包依赖关系)到本地。之后查询,查询的是本地缓存的包信息;安装时,也会查询本地缓存的包信息,可能会出现源服务器有某个包,本地缓存没有的情况。

    • yum upgrade, yum update

      根据本地缓存的包信息,更新所有已安装的包。yum upgrade 和 yum update 的功能几乎一样,推荐使用 yum upgrade。

    • yum clean

      下载的 rpm 会占磁盘,清理这些 rpm 安装包,使用 yum clean 命令。

    • yum install

      安装 rpm 包。

    • -y

      yum install, yum upgrade, yum update 时,可能会弹出对话框,需要填 yes/no 才能继续操作。-y 的意思就是一路 yes,使得安装过程不需要交互。-y 在 Dockerfile 经常用。

    apt-get

    • apt-get update

      缓存服务器的包信息,并根据缓存的包信息,更新已安装的包。

    • apt-get install

      装包

    • -y

      一路 yes, 使得安装过程不需要交互。-y 在 Dockerfile 中经常用。

  • 在 Dockerfile 中如何使用

    Dockerfile 使用 -y 选项(比如 apt-get -y install, apt-get -y update, yum -y upgrade, yum -y install ),使得安装过程不需要交互。

参考文献

部署服务

作者:王蒙
标签:Python, Service, On-Demand Remote Execution, Configuration Management Tools, The Twelve-Factor App, setup.py
简介:介绍 python 如何部署 Saas 应用。

目标读者

网络服务,API

解决办法

The Twelve-Factor App

开发易于部署的 App 的 12 个关键要素:

  1. 基准代码。一份基准代码,多份部署。
  2. 依赖: 显示声明依赖关系。
  3. 配置:在环境中存储配置。
  4. 后端服务:把后端服务当做附加资源。
  5. 构建发布运行:严格分离构建和运行。
  6. 进程: 以一个或者多个无状态进程运行应用。
  7. 端口绑定: 通过端口绑定提供服务。
  8. 并发: 通过并发模型(todo: 具体并发模型有哪些,简述这些并发模型)进行扩展。
  9. 易处理:快速启动和优雅终止。
  10. 开发环境和线上环境等价。尽可能的保持开发,预发布,线上环境相同。
  11. 日志。把日志当做事件流。
  12. 管理进程。后台管理任务当作一次性进程运行。
使用 Fabric 自动部署

Fabric 能把代码远程部署到多台服务器。Fabric 能执行 shell 命令,能通过 ssh 在远程执行 shell 命令。 Fabric 还能交互式地输入密码验证。Fabric 中 run 和 local 是其中最重要的函数。

构建自己的 package index 和 PyPI 镜像

推荐使用 devpi 构建公司专属的 package index, 保证 package index 的可用性,方便发布代码。

PyPI mirroring

推荐使用 devpi 构建 package index:

  • 能缓存你需要的 Python 官方 PyPI 包。
  • 能周期更新你需要的 Python 官方 PyPI 包。

其他的搭建 PyPI 工具,要不不同步 Python 官方 PyPI 包,要不就是全备份导致磁盘占用特别高。相对来说 devpi 提供了 Python 官方 PyPI 包,同时又不占用过多的资源,唯一的缺点是 devpi 包缓存的官方 PyPI 包可能不是最新的。

使用包发布应用

setup.py 中能够自定义操作,这些操作可用于自动化部署应用。这些步骤一般会在开发环境执行。

MANIFEST.in 可以用于 bundle 很多文件,比如 SASS 或者 LESS 等等。

Using process supervision tools

应用经常会启动多个进程,所以需要使用进程管理工具。

在 Python 中,常用 SupervisorCircus 来管理用户应用进程。 Supervisor 暂时不支持 Python3, 所以可以使用 Circus。

不要使用超级用户部署

常常用和应用同名的用户,部署应用。

绝对不要以超级用户权限部署应用,那样的话,应用之间会相互影响。

反向代理

Nginx 还是很有必要学一下的。

使用反向代理的好处有:

  • TLS/SSL 一般会由 nginx 来做。
  • HTTP 一般服务于 80 端口,HTTPS 一般服务于 433 端口。普通用户没有权限绑定 80/443 端口。但是超级用户(反向代理一般使用超级用户权限执行)有权限。
  • Nginx 能高效地提供静态文件。
  • Nginx 可以伪装出多个 host。DNS 中一个 IP 可以对应多个域名。nginx 服务器中每个 server 通过 server_name 选项匹配域名,这样就实现了一台主机用多个域名,提供多种服务的功能。
  • nginx 可以提供缓存和负载均衡的功能。
Reloading processes gracefully
  1. 当接到 TERM 信号时,应用应该停止接受新的请求,并且处理完所有已经接受的请求,然后退出。
  2. 当接到 HUP 信号时,应用旧节点优雅退出(不再接受请求,但是会处理已经接受到的请求),新节点应该开始接受新的请求,确保服务没有间断。
  3. Python 中的 GunicornuWSGI 都支持不间断服务的重载方式。可以参考这两个框架的做法设计自己的应用。

todo: 我挺好奇 Gunicorn 是怎么实现这种优雅退出的。

监控维护应用
  1. 查看日志,比如不同返回码的日志有多少条。
  2. 错误日志和警告日志。
  3. 资源占用(比如 CPU, memory 和带宽等)。
  4. 和商业价值有关的指标(客户占有率,投资回报等)。
Logging Errors

不管程序写得多完美,都保不齐会发生错误。要做到及时发现错误。

一般从日志中发现错误,Sentry 是报告系统错误最有名的工具。

Sentry 开源的,可以自己维护 Sentry 服务,也可以选择付费使用 Sentry 服务。

使用 Sentry 服务,需要 DSN(Data Source Name)。 我去 Sentry 网注册了个 DSN(https://13c5b7dbdb544a7ea37177997fbc8e66:d68d511723e147f4bbdb132af22f04aa@sentry.io/1197614), 有了 DSN,就可以把错误日志写入到 DSN 中。

对于 Python,Sentry 提供了 raven 包。python 装上这个包之后,很容易与 Sentry 集成。具体的参见 Sentry

Monitoring system and application metrics

monitor performance 的工具非常多。比较常用的有:

  1. Munin
  2. StatsD
  3. Graphite
Dealing with application logs

根据 12 factor app 的建议,application code 以 event stream 的形式抛出日志。application code 本身不会收集和记录日志。收集和记录日志是当前执行环境要负责的。

Python logging 可以使用配置文件,配置应用的日志该怎么收集和记录。我认为这种方式已经是非常好了,使用配置文件,相当于是让当前执行环境负责收集和记录日志。

Circus 和 supervisor 等进程管理工具,也提供了收集和记录日志的功能。

Linux 中的 logrotate 提供了收集和记录日志的功能,《expert python programming》 一书推荐使用 logrotate 收集和记录日志。不过我认为这可能是因为该书作者用习惯 logrotate 了。

Tools for log process

日志中不仅有报错,还有与业务相关的很多信息。很多商业智能的工具从日志中挖掘商业信息。从日志中挖掘信息,最有名的工具是 ELK(Elasticsearch, Logstash 和 Kibana)。

数据库

ORM

作者:王蒙
标签:网络开发,数据库,ORM
简介:介绍 Python 以 ORM 操作数据库的方式。

目标读者

Python 开发

预备知识

数据库基本操作

问题

  • SQLAlchemy
  • Django ORM

解决办法

Django Model 在介绍 Django 时有论述。这里重点介绍 SQLAlchemy。

SQLAlchemy 与数据库交互有以下两种方式:

  • 使用 SQL Statements and Expressions API

    SQLAlchemy 用这套 API 构建出 SQL 语句,交给 Connection 执行。

    参考 代码,学习使用 SQLAlchemy SQL Statements and Expression API 方式与数据库交互。

    SQLAlchemy 直接使用 SQL 语句查询数据库。

  • ORM

    • 参考 使用 SQLAlchemy 快速上手 SQLAlchemy ORM 使用方式。

    • 使用过程中,我认为 ORM 处理外键(relation)的方式,让编程变得简单。使用 relation 时,有一点不方便,就是

      # 传给 relationship 的第一个参数是类名字符串(注意不是表名),如果以后可以直接传入类,在重构代码时,会比现在方便很多。 books = relationship(‘Book’)

      我使用 relationship 时,一般会加上 backref 参数。这样,在另一张表中,也可以用 . 访问属性的方式,访问外键。

      members = relationship(‘OrganizeStructure’, backref=backref(‘group’))

    • 按照 PEP 249 要执行 commit,才会把数据写入数据库。

创建 Connection

SQLAlchemy 提供了 MetaData, engineSession 来创建连接。MetaData 代表数据库

创建 Metadata:

from sqlalchemy import MetaData

metadata = MetaData()
from sqlalchemy.ext.declarative import declarative_base
# 在 ORM 中使用,该 Base 除了代表数据库之外,还会进行 ORM 变换。
Base = declarative_base()

创建表

# 创建表
metadata.create_all(engine)
# 在 ORM 中创建表
Base.metadata.create_all(engine)

创建 engine

这是 SQLAlchemy 非常方便的点。要更改数据库,比如要从 postgresql 改成 mysql, 改下 engine 就可以了。

还有就是测试的时候,使用 memory 数据库会非常方便(这样做测试的时候,不需要启动外部数据库,使得测试流程完全自动化)。下面的代码就创建了 memory 数据库 engine。

from sqlalchemy import create_engine

engine = create_engine('sqlite:///:memory:', echo=False)

创建 connection 和 session

使用 ORM 方式需要创建 session

session = Session(bind=engine)

使用 SQL Statements and Expressions API 方式,需要创建 Connection。

connector = engine.connect()

Transaction

SQLAlchemy Transaction SQLAlchemy Transaction official document

Transaction 就是一系列的 SQL 操作,这一系列操作要不全部执行成功,要不一个也不成功。SQLAlchemy 通过 rollback() 来实现 Transaction。 常见的使用方式如下

transaction = connection.begin()
try:

    # 一系列操作

except IntegrityError as e:
    # 出现错误,把数据库状态整回一系列 SQL 操作之前
    transaction.rollback()

有的 engine(数据库)不支持在 connection 中指定 save_point(rollback() 回到的状态),如何设 save_point 查看上面提到的文档。

todo: 看完 https://github.com/oreillymedia/essential-sqlalchemy-2e 的代码。

postgresql

作者:王蒙
标签:数据库,postgresql
简介:介绍 postgresql, hive 等数据库的基本用法。

目标读者

后端开发

问题

Postgresql 的特点

解决办法

Python 语法

Python 难懂的语法

Author:王蒙
Tags:语法,数据类型,dict, tuple, list, string, collections
abstract:介绍 Python 中重要,Pythonic ,与类关系不大的语法。

Audience

Python 开发

Prerequisites

数据结构,算法基础

Problem

  • Python 数据类型
  • 列表表达式,字典表达式,集合表达式
  • collections
  • iterator
  • generator
  • unpack
  • zip
  • decorator

Solution

Python 数据类型

string 和 bytes

  • Python2 和 Python3 关于字符串有重大的区别

    • Python2 中 unicode, str(bytes 就是 str) 和 basestring 类型。 unicode 不是 str 类型,unicode 和 str 不是同一个类型,但都是 basestring 的子类。
    • Python3 有 bytes, str(unicode 就是 str) 类型。bytes 和 str 不同。
    • Python2/Python3 都是用 b 前缀表示 bytes 字面量, 是用 u 前缀表示 unicode 字面量。
    • Python2 源码默认密码是 ascii 码, Python3 源码默认编码是 utf-8,为了兼容可以在源码开头加上编码声明。
  • string 和 bytes 相互转化

    # errors 表示该如何处理编码解码错误,比如 strict,ignore 等。
    bytes(source, encoding, errors)
    str.encode(encoding, errors)
    
    
    str(source, encoding, errors)
    bytes.decode(encoding, errors)
    
  • 拼接字符串

    • % 或者 format 拼接方式可读性最好。
    • join 方法效率较高。
      • 字符串的方法,尽量不要用。
  • str 是 hashable 和 immutable 的。

  • __repr__ 方法和 __str__ 方法。

    • 最明显的区别是: obj 能够通过 eval(repr(obj)) 生成,但是 obj 通常不能由 str(obj) 生成。
    • __repr__ 用于调试;__str__ 是简单地打印结果。

list

list 是 mutable 的,不是 hashable 的。

list append 元素的摊还复杂度(Amortized Complex)是 O(1)。

list 查找元素的时间复杂度是 O(n),n 表示 list 的长度。

list 从头插入元素的时间复杂度是 O(n), n 表示 list 的长度。

list 不是使用链表实现的,list 是用连续一段内存实现的。list 保存 list 实际长度,和连续内存长度作为属性值。如果 list 的实际长度超过分配的内存长度,那么会重新分配内存,比如分配 2x 内存长度的内存。如果List 的实际长度小于分配长度的 1/2。就会收回1/2 内存长度的内存。

tuple

tuple 是 immutable 的, hashable 的。

dict & set

CPython 中, dict 和 set 都是使用 hash table 实现的。所以要求 dict 和 set 的 key 必须是 hashable 的。

CPython 中,dict 和 set 都是使用 Open Address 的手段,来解决冲突(Collision) 的问题。

因此,在 dict 中寻找 key, 在 set 中寻找元素的平均时间复杂度都是 O(1)(更准确的说是 O(1 + alpha), alpha 称为加载系数)。

在 dict 中寻找 key, 在 set 中寻找元素的最差时间复杂度是 O(n), n 是 dict 曾经达到的最大长度。

关于为什么最差时间复杂度中的 n 是 dict 曾经达到的最大长度。我有如下猜测:

CPython 是如何实现删除 key 的操作的?我猜是这样处理的,就是每个 hashTable 的 slot 都添个 flag 表示是否还有效。删除某个key,就是修改flag,说明这个 key 无效了。以此实现删除。我猜的这种做法,刚好解释了最差时间复杂度O(n) 中的 n 为什么是添加过的所有 key 的个数,而不是当前 key 的个数。

dict 的 keys(), values() 和 items() 方法

Python2 中 dict 的 keys(), values() 和 items() 返回的是当前 dictionaries 的 keys, values 和 items。

Python3 中 dictionaries 的 keys(), values() 和 items() 返回的是 dictionaries 的 keys 的view, values 的 view 和 items 的view。可以认为是指向 dictionaries 的 keys, values 和 items 的指针。这样的好处是: 比如 a = dict_item.keys() 之后 dict_item 添加了很多 key, 此时 a 中包含了新添加的 keys;而 Python2 中是不包含的。下面这段代码,Python2 和 Python3 的执行结果是不同的。

>>> words = {'foo': 'bar', 'fizz': 'bazz'}
>>> items = words.items()
>>> words["spam"] = 'egg'
>>> items

Python3 中 keys() 中key 出现的顺序刚好和 values() 中 dict[key] 出现的顺序一致(不管何时,不管做了什么操作)。

Python 中的 set 是 mutable 的, franzenset 是 immutable, hashable 的。

一般 immutable 的数据是 hashable 的, hashable 的一定是 immutable 的。定义了 __hash____eq__ 方法的数据类型是 hashable 的。

collection 模块

  • namedtuple(可以用 attribute name 访问数据,不必使用 index 访问)
  • OrderedDict(能保持添加 key 的顺序)
  • defaultdict(能给 key 赋默认的 value)
  • deque(这是用双向链表实现的,如果用队列,请使用 deque, 不要用 list)
  • Counter(很有意思,会统计 list 中每个 element 出现的次数)
  • ChainMap(todo: 没用过,不理解,为什么要用这玩意)

列表表达式

[i for i in range(10) if i % 2 == 0]

字典表达式

squares = {number: number**2 for number in range(100)}

集合表达式

squares = {number**2 for number in range(100)}

enumerate

for i, element in enumerate(['one', 'two', 'three']):
    print(i, element)

zip

for item in zip([1, 2, 3], [4, 5, 6]):
    print(item)

for item in zip(*zip([1, 2, 3], [4, 5, 6])):
    print(item)

dict(zip([1, 2, 3], [4, 5, 6]))
dict(zip((1, 2, 3), (4, 5, 6)))

unpack 赋值

iterators(迭代器)

实现下面两个方法的就是 iterator:

  1. __next__ return next item of the container.
  2. __iter__ return the iterator itself.

generator(生成器)

yield 用于写 generator。

generator 除了 next 和 send 常见用法外,还有

  • throw 抛异常
  • close 停止迭代

Context managers(上下文管理器) - the with statement

只要一个类实现了

  1. __enter__
  2. __exit__

方法,这个类就是 context manager。

不过一般都是用 contextlib.contextmanager decorator 定义 context manager ,因为这样定义更简洁。

contextlib 除了最常用的 contextmanager decorator, 还提供了 closing(element)`(生成 contextmanager ,离开 contextmanager 会自动执行 element.close(),可以防止你忘记执行 close()), `supress(*exceptions)`(生成 contextmanager , 并自动吞掉 contextmanager 中出现的 exceptions), `redirect_stdout(new_target)`(能把原本要写到 stdout 的字符串,写到 new_target(file-like object) 中),`redirect_stderr(new_target)`(和 `redirect_stdout(new_target) 用法类似)。

decorators(装饰器)

Decorators 就是在要包裹的 code 前后包装些代码去执行。

decorator 可以写成函数,可以写成类,可以带参数。

decorator 一般都需要保持被包装函数的 metadata, 一般使用 functions.wraps 保持函数 metadata。(decorator 可以装饰类,但是没有现成的工具保持类的 metadata。所以类的 metadata 编程还是使用 metaclass 比较方便)。

Reference

  • Expert Python Programing

类级语法

Author:王蒙
Tags:Python 开发,语法
abstract:本章简述 Python 中类级的复杂语法,包括 super 函数,descriptor,装饰器和元类等等。

Audience

Python 开发

Prerequisites

Python 基础语法

Problem

  • super 函数

  • descriptor

  • property

  • classmethod

  • abstractmethod

  • meta programming

    • decorator
    • metaclass
  • Mixin

Solution

继承 built-in types

built-in types 支持些特别的运算符。继承时,重写 built-in type 的某些方法,就可以重定义这些运算符。

常见的,__getitem__ 对应 a[k] 运算符, __setitem__ 对应 a[k] = 11 运算符。

定义序的时候,优先使用 @totalordering 运算符。

在重写运算符,继承 built-in types 时,应该先去 collections 包中找找有没有现成的实现。大多数情况写,用不着重写运算符,继承 built-in types 。

使用父类的方法

super() 用于获得父类的方法。 super() 可以用在任何上下文中,不一定非要在类方法中出现。 super() 常见的用法有如下三种:

  • super(instance_type, instance_value): instance_value 必须属于 instance_type 否则会报错。instance_value 属于 instance_type 的情况下,返回 instance_value 对应的 type 的 mro 中 instance_type 后一个 type。
  • super(instance_type): 返回 instance_type 的 mro 中 instance_type 后一个 type。
  • super(): python3 的语法(Python2 不支持),出现在类方法中。如果 super() 出现在普通方法中,相当于 super(defined_class, self);如果出现在普通的类方法中相当于 super(defined_class);如果出现在 staticmethod 中会报 RuntimeError。

除了使用 super() 获得父类方法,还可以使用 superclass 的类名直接获得 superclass 的方法。尽量不要同时使用 super 和 superclass 类名两种访问父类的方法。

Python 子类默认不会调用父类的 __init__ 函数(初始化函数),如果需要,需要开发者自行调用,调用时,如果不清楚父类 __init__ 函数的 signature, 可以查文档或者读源码。

Python3 所有的类都继承自 object 类, Python2 中有继承自 object 的类,也有不是继承自 object 的类。继承自 object 的类称为新式类(new style class),不是继承自 object 的类称为旧式类(old style class)。新式类使用 C3 算法解决多重继承问题,旧式类使用深度优先解决多重继承问题。新式类有 mro 方法和 __mro__ 属性;旧式类没有。

Python3 中所有类都是新式类,不管怎么定义类。 Python2 中只有明确继承自 object 或者继承自其它新式类的类才是新式类。除此之外是旧式类。比如如下 Python2 代码:

# 旧式类
class A:
    pass

# 旧式类
class B():
    pass

# 旧式类
class C(A):
    pass

# 新式类
class D(object):
    pass

# 新式类
class E(D):
    pass

如果代码要运行在 Python2 和 Python3 环境,那么定义类时,一定要指明继承于那个类。一定都用新式类。

即使旧式类没有 mro ,但是仍然可以使用 super 。最好不要用旧式类。

mro(method resolve order), Python 新式类使用 C3 算法计算 mro。对于多重继承,C3 给出给类的线性排列。C3 算法计算公式为:

class MyClass(Base1, Base2):
    pass

L(MyClass) = MyClass + merge(L[Base1], L[Base2], Base1, Base2)

下面是摘抄的 C3 算法的解释:

The liberalization of C is the sum of C plus the merge of the liberalizations of the parents and the list of the
parents.

Taken the head of the first list, that is, L[Base1][0]; if this head is not in the tail of any of the other
lists, then add it to the liberalization of MyClass and remove it from the lists in the merge, otherwise look at
the head of the next list and take it, if it is a good head.

Then, repeat the operation until all the classes are removed or it is impossible to find good heads. In this
case, it is impossible to construct the merge, Python 2.3 will refuse to create the class MyClass and will raise
an exception.
Best Practices
  1. 尽量不要使用多重继承
  2. super 和 explicit class calls 不要混在一起使用
  3. 如果你的代码要兼容 Python2,定义类时一定要继承自 object
  4. 调用父类方法时,确认下父类是谁(比如用 mro() 方法)
Descriptor

descriptor 是获取和设置属性的中间件。descriptor 又称自定义 property,大多数情况下定义个 property 就够用了。

descriptor 协议:

  1. __get__(self, obj, type=None)
  2. __set__(self, obj, value)
  3. __delete__(self, obj)

实现了 __get__() and __set__() 的 descriptor 称为 *data descriptor 。如果 descriptor 只实现了 __get__(), 这个 descriptor 称为 non-data descriptor

不管是 instance.attribute 或者是 getattr(instance, ‘attribute’), 都是调用 __getattribute__() 去寻找属性, __getattribute__() 寻找属性的默认顺序为:

  1. It verifies if the attribute is a data descriptor on the class object of the instance.
  2. If not, it looks to see if the attribute can be found in the __dict__ of the instance object.
  3. Finally, it looks to see if the attribute is a non-data descriptor on the class object of the instance.

我认为这段描述没有谈到 MRO 对于 __getattribute__() 的影响。我补充一点就是父类的 data-descriptor 也是子类的 data-descriptor,父类的 non-descriptor 也是子类的 non-descriptor。

metaclass

metaclass 原理和例子

Python3 中用如下的方式使用元类(metaclass)

class B(metaclass=MetaClass):
    ...

class C(B):
    ...

Python 2 中使用如下的方式使用元类(metaclass)

class B(object):

    __mataclass__ = MetaClass

可以使用 six 写出`兼容 python2 和 python3 的元类`_ 。

metaclass 常见的应用

元类(metaclass)很少用。使用 metaclass 请慎重,多想想有没有更简单的方法。

实际使用元类(metaclass)的例子:

todo: django 代码比较多,有时间的话,慢慢看。

interface

可以使用 zope.interface 中的 Interface, implements 定义接口。虽然 zope 已经风光不再,但是 zope.interface 仍然常用。静态语言能在编译时,警告你要实现 Interface 中的方法,Python 是动态语言,需要写测试用例,运行测试用例时,会提示你实现Interface 中的方法。

使用 abc 包中的 abstractmethod 也可以定义接口。

重写 __subhook__ 方法,可以定义 isinstance 行为。

Reference

反射

作者:王蒙
标签:反射
简介:常用的反射相关的函数。

目标读者

Python 开发

预备知识

descriptor,__dict__ 等

问题

__getattr__

__getattribute__

dir()

getattr()

hasattr()

setattr()

isinstance()

inspect 包

解决办法

__getattribute____getattr__

访问对象的属性(a.x 或者 getattr(a, ‘x’))一定会调用 __getattribute__ 方法。

__getattribute__ 默认(如果没有自定义的话)会按照 data-descriptor, dict attribute, non-data-descriptor 的顺序寻找属性值,如果没有找到会调用 __getattr__ 返回属性值。

__getattr__ 默认是抛出 AttributeError 异常。不过时不时可以重定义 __getattr__ 方法(几乎不会自定义 __getattribute__ 方法)。

hasattr

getattr

setattr

isintance

dir

inspect 包:这是 python 反射用的包,用的不多,就算 github 上有名的类库用的也不多。

设计模式

Author:王蒙
Tags:Python, 设计模式
abstract:python 非常灵活,有的设计模式没有必要(可以看成 Python 自带这些模式)。

Audience

Python 开发

Prerequisites

熟悉常见的设计模式

Problem

  • 单例(Singleton)
  • 适配器(Adapter)
  • Facade
  • Proxy
  • Visitor
  • Template
  • Mixin

Solution

Singleton

相对于实现 Singleton, Python 更适合使用 Borg/MonoState 模式(就是让所有该类的对象,都有相同的状态)。Singleton 和 Borg 都可以使用类的 __new__ 方法实现。

在 Python 中,为什么 Borg 比 Singelton 常用?

要实现可以继承的 Singleton 比较麻烦(需要用到 metaclass)。而要实现可继承的 Borg 直接实现 __new__ 即可,所以 Borg 用的比较多。

如果不用于继承,又要实现 Singelton, 直接使用模块级的变量和函数,即可。根本用不着实现 Singleton。这两个原因导致,Python几乎用不着 Singleton。

Composite

不管哪种编程语言,与继承相比优先选择 Composite 模式。

Mixin

Python 中多重继承优先选择 Mixin。

Adapter

可以认为 Python 自带了 Adapter, 因为 Python 采用了 Ducking Type 的模式,只要你有这个动作,就认为你是这个类。

所以我们要把 A 类配成 B 类,只要实现相应的方法就行,别的啥不用做(比如不用继承自 B 类)。

Ducking Type 的解释是: If you walks like a duck and talks like a duck, then it’s a duck!

Proxy

提供一个中间件,管控访问。比如之前的 url_cache。

Facade

__init__.py 中常见 import 语句,通过这些 import 语句,python 包定义了自己对外的接口。这就相当于是实现了 Facade 模式。

Observer

Visitor

Template

Reference

  • effect python
  • Expert Python Programming

Pycharm

作者:王蒙
标签:IDE
简介:介绍 Pycharm 。

目标读者

Pycharm 用户

解决办法

Pycharm 超好用功能

调试器(debugger)

断点

除了基本的操作(参见参考文献中的视频),可以设置 条件断点

调试多线程的程序,可以设置断点是仅仅 suspend 当前线程的执行,还是 suspend 所有线程的执行。

_images/断点.png

Alt + 2 能查看所有的断点,书签。在调试程序和阅读源码时,会很方便。

单步执行

step over

把调用函数看成一步。

step into

把调用函数看成多步,会进入到函数定义中(进入函数定义中后,可以接着单步执行函数定义中的每一步)。

step out

一步执行完当前函数(或者模块)剩余的所有语句并返回。

step into my code

不进入第三方包的函数定义。但是会进入自己定义的函数定义。

Resume Program

运行到下一个断点处。

watcher & evaluator

evaluator 可以在断点处执行代码,调试程序。 watcher 可以观察各个变量的值。

Run Config: 参见视频 Running Python Code

  • Interpreter: vagrant, ssh, docker, local interpreter。
  • Run configuration。
编辑器(editor)

具体看 Code Navigation 视频。

  • 查找

具体看 Productive Coding 视频。

  • 重构(refactor)。
  • 自动补全, Alt + Enter 键。
  • 模板(template)。
  • language injection。
git

Pycharm 使用 git 相比于直接使用命令行的优势在于:

  • 可视化的界面显示了提交了哪些更改。
  • 方便和 Pycharm 的其他功能集成。比如可以设置在 git 提交代码之前先检查代码,代码不规范不能提交等。
数据库,科学计算和网络开发

Pycharm 提供了图像化的数据库使用界面。

提供了科学计算模式,画图表时,会略微方便一点。

对 Django 的支持相当好。

具体的参考 What’s New in Pycharm 2017.3

部署

vagrant, docker, remote interpreter Pycharm 都支持。

Tools -> Deploy upload 功能,可以方便地把代码部署到服务器上。

检查代码
我一般用 pylint, flake8 不怎么用 Pycharm 的 Inspect code 功能。

Terminal

Pycharm 几乎提供了开发相关的所有功能。而且 Pycharm 可以配置 Tools -> External Tools , 让 Pycharm 集成命令行工具。但除了上一节介绍的功能之外,我选择直接使用命令行。

如图配置 terminal 为 bash.exe(安装了 git 就会有 bash.exe, 该 bash.exe 会启动类似 unix 的命令行环境)。这样配置之后,就可以以 unix 命令行的方式使用各种工具(比如 git,fabric,pylint,pytest,sphinx,coverage,profile)做开发。

_images/terminal.png

Pycharm 通过安装 plugins (比如 docker, maven, markdown, redis 等等插件)扩展功能。坑爹的是,JetBrains 的 plugins 源有时候访问不了,下不来插件。

常用快捷键

  • Ctrl + Shift + A: 寻找 Pycharm 提供的功能
  • Alt + Enter: 自动补全
  • Ctrl + F7: 查看 Usage
  • Shift + Shift: 搜索类,字符串等等。
  • Ctrl + R: 替换
  • Ctrl + Z: 撤销
  • Ctrl + /: 注释/撤销注释
  • Ctrl + Alt + L : Reformat code。
  • Ctrl + Q: 查看文档。
  • Ctrl + P: 查看函数的 signature。
  • Ctrl + K: 提交代码。
  • Ctrl + W: 选择代码块。
  • Ctrl + -: 折叠代码块。
  • Ctrl + =: 展开代码块。
  • Ctrl + B: 查看函数,类的实现。
  • Ctrl + O: 查看基类的方法。
  • Alt + 1: 查看 project 结构。
  • Alt + 2: 查看书签和断点。
  • Alt + 6: 查看 todo。

其他

学会使用 Pycharm, 基本就会使用 JetBrains 公司出的其他 IDE。包括 Intelij IDEA, WebStorm, Clion 等等。

不使用 IDE, 纯手写(比如用纸和笔写代码)更提高代码水平。比如:

  • 实现算法的时候,手写代码。
  • 代码不依赖第三方包时,手写代码(更能体会编程语言本身的特性)。
  • 预先写伪代码。简单的代码看不出伪代码的好处,但是如果项目复杂,提前写好伪代码,能避免很多坑。
  • 最后,手写代码对面试帮助非常大。

单说开发效率,我坚持认为 Pycharm 能大大提高开发效率。

订阅 youtube 上的 JetBrainsTV 号,这个号里有大量的视频教程。

看 Pycharm help 文档,文档中详细写了每个功能的用法。

Django

Django 有强大的社区,提供了丰富的 apps。

介绍 Django, 主要介绍 Django 重要组件(View, Template, Model, Form, Signal 等等) 和 Django 中重要的 apps (Admin, session, message, restful, celery 等等)。

概览

作者:王蒙
标签:Django,apps
简介:Django 如此强大,源于Django 有丰富的 apps 可用。很多网站开发中需要的功能,Django 有现成 apps 提供。本节介绍这些 app 的通用特点,方便上手这些插件。

目标读者

Python 网站开发

解决办法

Django 有丰富的插件,比如 rest_framework, auth_framework, django-brace, django-guardian,django-message 等等。

这些插件,提供了现成的 view, template, models,minxin, template tag 供用户使用。

使用这些插件,首先需要配置 settings.py 文件。

  • INSTALLED_APPS 中添上相关的插件。
  • 除此之外,也可能需要添加其他的配置,比如 MIDDLEWARE 以及 apps 自定义的 BACKEND 配置。

使用这些插件,还需要配置 urlpattern。

  • 如果是拿来即用,一般 apps 提供了一套 urlpattern 规则,直接使用 include() 函数,给 apps 定义的 urlpattern 添加个前缀就 ok。比如:

    url(r'^admin/', admin.site.urls)
    
  • 更细化的,可能你只需要 apps 提供的几个 view, 不需要整体都拿过来。这时候,就引用该 view,定义该 view 的 urlpattern 即可。这种方式,一般需要你对 apps 有了解,知道 apps 中有哪些 view, 一般需要读文档或者源码。

  • 可能 apps 提供的 view 不能完全满足你的需求,你需要重写该 view。这时候需要详细阅读文档或者源码。

  • apps 提供的 view 一般都有两个版本,函数版本和类版本。类版本更方便定制(很多时候定制,就是设置的属性值,少数时候,需要重写方法)。为了组合功能,很多 apps 提供的不是 View 类,而是 Mixin 类。

使用第三方插件时,经常需要自定义 template,在写 template 时,经常不知道第三方 apps 提供的 context 是什么样的,也不知道应该把 template 放到哪个目录下。

  • 什么都不定义,直接运行代码,程序会报告在某某路径找不到 html 文件。这时候,就可以确定,第三方 apps 的默认 template 是保存在哪个路径下。就知道要在哪个目录下写 template 文件。
  • 当前 Django 模板一定支持,你输入 request, 因为 render 方法的第一个参数是 request。但是其他的 context, 就各不相同的。todo: 怎样用 Pycharm 调试模板
  • 查文档,一般文档有个示例代码。该示例代码会把 template 放到 template 刚放的地址,示例代码的 template 中的参数,就是第三方 apps 提供的 context。

有时候,需要自定义 Model。

参考文献

Put here references, and links to other documents.

Django Signal

作者:王蒙
标签:django, signal
简介:Django 中的 signal 类似于 hook。signal 能够让一个事件,触发多个操作。

目标读者

Django 网站开发

问题

Django signal

signal handlers

解决办法

Django signal

Django 自带的 signal:

Model: pre_save, post_save, pre_delete, post_delete, m2m_changed

requests: request_started, request_finished

可以自定义 signal, 触发信号,使用如下代码:

from .signals.signals import my_signal

my_signal.send(sender="some function or class",
               my_signal_arg1="something", my_signal_arg_2="something else"])
signal handlers

django.dispatch.receiver 绑定 singal handler 函数。

Django Form

作者:王蒙
标签:Django, Web Frameworks, Python
简介:本节介绍 Django 如何处理 form(表单)。

目标读者

Python 开发,网站开发

预备知识

Django Model, Python

问题

  • 什么是表单
  • Django 如何处理表单

解决办法

什么是表单

表单用于接收用户输入参数。

表单是 html5 中的 form 元素。

html5 form 元素中 method 属性表示向服务器发送的 HTTP 方法,一般都是 POST ,也可能是 GET。

表单会向服务器传送用字典表示的数据,Django 可以方便得解析 form 获得用户输入。

Django 如何处理表单

定义 form, 和定义 Model 类似,给出表单的字段,就定义了表单。与 Model 中的字段不同, form 字段的 widget 会设置该字段在前端的展现形式。

在模板中 render form。 form.as_p, form.as_table 等等方法可以把 form 转成 html 语句(进而显示在前端),一般会把 form.as_p, form.as_table 等等语句直接写到模板中。

Form 解析用户输入数据:

  1. 首先调用构造器,生成 form, 比如 EmailForm(request.POST)
  2. 然后调用 is_valid 验证输入是否合法,调用 .clean_data(dict 类型数据) 得到用户输入。
  3. 如果是 ModelForm,可能会调用 save 保存结果。
from django import forms
# define a form is forms.py.
class EmailPostForm(forms.Form):
    name = forms.CharField(max_length=25)
    email = forms.EmailField()
    to = forms.EmailField()
    comments = forms.CharField(required=False,
                                widget=forms.Textarea)

# handle form in views.

def post_share(request, post_id):
    # Retrieve post by id
    post = get_object_or_404(Post, id=post_id, status='published')
    sent = False

    if request.method == 'POST':
        # Form was submitted
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Form fields passed validation
            cd = form.cleaned_data
            post_url = request.build_absolute_uri(post.get_absolute_url())
            subject = '{} ({}) recommends you reading "{}"'.format(cd['name'], cd['email'], post.title)
            message = 'Read "{}" at {}\n\n{}\'s comments: {}'.format(post.title, post_url, cd['name'], cd['comments'])
            send_mail(subject, message, 'admin@myblog.com', [cd['to']])
            sent = True
    else:
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post,
                                                    'form': form,
                                                    'sent': sent})
ModelForm

ModelFrom 直接使用关联的 model 中定义的字段作为 form 的输入字段。

   from .models import Comment
   # define modelform.
   class CommentForm(forms.ModelForm):
       class Meta:
       model = Comment
       fields = ('name', 'email', 'body')


Formset

多个 form 组成 formset。详细参见 Formsets

Django Model

读者:王蒙
标签:网络编程,Web Framework
简介:Model 使得 Django 操作数据库变得非常简单。

目标读者

Python 开发,网站开发

预备知识

Python, ORM

问题

  • Model 带来了那些好处?

  • Model 的使用:

    • Model 定义
    • django migrate
    • Model 增删改查
    • Model 和 ModelAdmin
    • 自定义字段
    • Model 继承
      • Multi
      • Proxy
      • Abstract
    • 优化查询效率

解决办法

Model 带来了那些好处?
  • 很容易改用别的数据库去实现。比如代码之前使用 mysql, 后来想改用 postgesql , 只要去 settings.py 文件中修改配置即可。
  • Model 建立了数据库记录与对象的映射,使得可以使用处理对象的方式处理数据库,特别是在处理外键的时候非常方便。
  • Model 提供了很多抽象层次很高的字段,比如 EmailField 会自动确保输入的字符串是符合邮箱格式的字符串。除此之外,还可以自定义字段。
  • Model 提供了很多非常简洁的筛选过滤的操作。
Model 的使用
Model 的定义
  • 给出该 Model 需要的字段。

  • Django 提供了丰富的字段(比数据库提供的字段要丰富,比如提供了 FileField 等字段)。Django 有哪些字段请参考 Model Field Reference

  • 注意 relationship fields 字段。

  • Model 中的嵌套类 class Meta 定义了该 Model 的 metadata。具体参考 Model meta options 。举个例子:class Meta: ordering = (‘title’, ‘-created’) 表示查询结果以 title 升序, created 降序排列。

  • Model class Meta 中要特别注意 Model 的三种**继承模式**。

  • Abstract: 被继承的 Model, 不会建立对应的表。继承的 Model 会在被继承 Model 的基础上添加字段。

    from django.db import models
        class BaseContent(models.Model):
        title = models.CharField(max_length=100)
        created = models.DateTimeField(auto_now_add=True)
        class Meta:
            # abstract inherit
            abstract = True
    
    class Text(BaseContent):
        body = models.TextField()
    
  • Multi-table: 被继承的 Model, 也会建立对应的表。继承的 Model 会在被继承 Model 的基础上添加字段。

    from django.db import models
    class BaseContent(models.Model):
        title = models.CharField(max_length=100)
        created = models.DateTimeField(auto_now_add=True)
    
    class Text(BaseContent):
        body = models.TextField()
    
  • Proxy:被继承的 Model 会建立对应的表,但是继承的 Model 对应的表就是被继承 Model 的表。继承的 Model 是添加了新的方法(不更改字段),方便使用。

    from django.db import models
    from django.utils import timezone
    class BaseContent(models.Model):
        title = models.CharField(max_length=100)
        created = models.DateTimeField(auto_now_add=True)
    
    
    class OrderedContent(BaseContent):
        class Meta:
            proxy = True
            ordering = ['created']
    
        def created_delta(self):
            return timezone.now() - self.created
    
在数据库中创建 model 对应的数据

下面两句命令,会在数据库中创建以及更新 model 对应的数据:

$ python manage.py makemigrations {app_name} $ python manage.py migrate
Model 的增删改查

增加记录

# 新建 model 时,注意一点,就是外键取值是个对象,不是键值
m = Module(course=course, title='title', description='description')
# 不执行 save 不会把该对象保存到数据库中
m.save()

# Model manager 的 create 方法也能新建对象。而且 create 创建对象,不必再调用 save 方法。
Module.objects.create(title='title', description='description')

删除记录:

# m is an instance of a kind of Model.
m.delete()

# 批量删除一批
User.objects().all().delete()

更新记录:

# m is an instance of a kind of Model.
m.title = 'change_title'
# you have to call save method, to update change to database.
m.save()
  • 查询,Django 查询需要了解 QuerySet , 详细的 QuerySet API, 参见 Make Query

    • 双下划线查询字段。
    • Q 函数,组合查询条件。
    • F 函数,选择自身的字段。
    • aggregate 聚合查询,annotate per-object 聚合查询。
自定义字段

Django model 可以自定义字段,下面是个自定义字段的例子。更多内容查看 custom-model-fields

class OrderField(models.PositiveIntegerField):

    def __init__(self, for_fields=None, *args, **kwargs):
        self.for_fields = for_fields
        super(OrderField, self).__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        if getattr(model_instance, self.attname) is None:
            # no current value
            try:
                qs = self.model.objects.all()
                if self.for_fields:
                    # filter by objects with the same field values for the fields in "for_fields"
                    query = {field: getattr(model_instance, field) for field in self.for_fields}
                    qs = qs.filter(**query)
                # get the order of the last item
                last_item = qs.latest(self.attname)
                value = last_item.order + 1
            except ObjectDoesNotExist:
                value = 0
            setattr(model_instance, self.attname, value)
            return value
        else:
            return super(OrderField, self).pre_save(model_instance, add)
ModelAdmin

Django 自身提供了 Admin 管理界面。ModelAdmin 定义了 Model 在 Admin 管理界面如何展示。

class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')
    list_filter = ('status', 'created', 'publish', 'author')
    search_fields = ('title', 'body')
    prepopulated_fields = {'slug': ('title', )}
    raw_id_fields = ('author',)
    date_hierarchy = 'publish'
    ordering = ['status', 'publish']

admin.site.register(Post, PostAdmin)
优化查询效率

Django 中 QuerySet 负责实际查询数据库(把ORM访问方式翻译成 SQL 语句,访问数据库)。QuerySet 是 lazy evaluated(延迟计算的,只有到实际用的时候才会运算)。除此之外 QuerySet 在某些条件会下缓存查询结果,尽可能多地使用缓存的结果,少访问数据库,是可以提高效率的。 下面举例说明几个提高查询效率的细节:

lazy evaluated: QuerySet 是 lazy evaluated。如下代码其实就访问了数据库一次,是 print 触发了数据访问。

       q = Entry.objects.filter(headline__startswith="What")
       q = q.filter(pub_date__lte=datetime.date.today())
       q = q.exclude(body_text__icontains="food")
       print(q)

特别注意,外键也是 lazy evaluated。就是说 QuerySet 首先获得外键的键值(id 号),到真需要外键对应的对象时,才会查询数据库得到外键对应的对象。

比如对于如下代码,只有到 print(b.a) 才会访问数据库,得到 a instance 。

select_related:是针对 ForeignKey 和 OneToOne 字段的优化。比如

actions = actions.filter(user_id__in=following_ids)
                .select_related('user', 'user__profile')
actions = actions.filter(user_id__in=following_ids)

for action in actions:
        # action.user 会执行一次 SQL 查询,多次执行会使性能变差。
        user = action.user
        # user.profile 会执行一次 SQL 查询,多次执行会使性能变差。
        user_profile = user.profile

select_related 的优势在于,把多次数据库查询整成了一次数据库查询(通过 SQL 中的 join 完成),提高了效率。

select_related 用于 ForeignKey 和 OneToOne 字段。

prefetch_related:

prefetch_related 用户 ManyToMany 字段和反向 ForeignKey 关系。

prefetch_related 能提高性能的原因在于会缓存查询结果,减少数据库访问次数。

https://docs.djangoproject.com/zh-hans/2.0/ref/models/querysets/#prefetch-related 中的例子为例。

prefetch_related 会提交执行 Toppings.objects.all() ,把所有的 toppings 缓存到本地。然后用 Python 对每个 Pizza instance 和 toppings 做 join。得到每个 Pizza instance 对应的 toppings 。

todo: 我常常想,如果数据库比较大,prefetch_related 会不会导致内存错误。

Pizza.objects.all().prefetch_related('toppings')
Manager

manager 是访问 Model 的接口。每个 Model 至少有一个 manager。

继承 models.Manager 类,重写 get_queryset 方法,返回 QuerySet 就能自定义 manager。

objects 是 Model 的默认的 manager。

我认为自定义 manager,主要为了少写代码。比如很多 sql 都需要做某些过滤操作,那么可以把这些过滤操作放到自定义 manager 中,使用该自定义 manager, 就相当于预先做了过滤。

Django View

读者:王蒙
标签:Django, Web Frameworks, Python
简介:view 负责处理解析 http request ,构造 http response。

目标读者

Python 开发,网络开发

预备知识

Python, 简单了解 http 协议

问题

定义 view

把 view 绑定到指定 url

解决办法

定义 view 函数

使用函数定义 view

from django.http import HttpResponse

def hello(request):
    # 与 flask 不同, django 的 view 函数必须返回 HttpResponse, 返回字符串是不合法的。
    return HttpResponse("Hello World")


# view 函数还可以带参数,这个参数一般是取自与 url path 中的参数。
def get_resource(request, resource_id):
    return HttpResponse("Resource Id is: %s" % resource_id)
使用类定义 view

使用类最大的好处是可以通过类继承重用代码。django.views.generic 有现成的 View 类型,继承这些 View 类型,可以定义 View 类。

from django.views.generic.base import TemplateResponseMixin, View


class ContentDeleteView(View):

    def post(self, request, id):
        content = get_object_or_404(Content,
                                    id=id,
                                    module__course__owner=request.user)
        module = content.module
        content.item.delete()
        content.delete()
        return redirect('module_content_list', module.id)

把 view 绑定到指定 url

from django.conf.urls import url
from django.contrib import admin

from .views import get_template, hello, get_resource

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^templates/', get_template),
    url(r'^greet/', hello),
    # 通过如下的方式传递参数
    url(r'(?P<resource_id>\d{4})', get_resource)
]

举个例子说明上面urlpatterns 中最后一步 url(r’(?P<resource_id>d{4})’, get_resource) 的用法: 如果输入的 path 为 1234 那么会向 get_resource 传递 {“resource_id”: “1234”}。?P 是 Python 正则表达式的用法,re.search(“(?P<resource_id>d{4})”, “1234abcd”).groupdict() 是使用这种用法的例子。

include(): include url conf

resolve(): 得到 url pattern

reverse():根据 view 名称和参数值,得出 path

reverse_lazy():

url 和 path 的区别; https://stackoverflow.com/questions/47947673/is-it-better-to-use-path-or-url-in-urls-py-for-django-2-0

Django Template

作者:王蒙
标签:Django, Template, Web Framework, Python
简介:介绍 Django 的模板。

目标读者

Python 开发,网站开发

问题

  • 模板继承
  • tags
  • filters
  • context processor
  • Pycharm 对于 Django 模板的支持

解决办法

  • 模板继承

    模板继承,就是通过 {% block {some_name} %}..{% endblock %} 定义 base template。再定义模板时,使用 {% extends “{base template path}”%} 继承 base template。继承模板只能重写 base template 中用 {% block {some_name}%}{% enbblock%} 圈出来的内容, 通过 some_name 连接要替换和被替换的内容。

  • tag: 除了模板继承,其他的 tag 都容易理解,基本只要看到 tag 的名字,就能理解。

    • 如果需要某个特别的功能,可以自定义 tag

      • simple_tag

        from django import template
        register = template.Library()
        from ..models import Post
        
        @register.simple_tag
        def total_posts():
            return Post.published.count()
        
      • inclusion_tag

        @register.inclusion_tag('blog/post/latest_posts.html')
        def show_latest_posts(count=5):
            latest_posts = Post.published.order_by('-publish')[:count]
            return {'latest_posts': latest_posts}
        
      • assignment_tag

        from django.db.models import Count
        @register.assignment_tag
        def get_most_commented_posts(count=5):
            return Post.published.annotate(total_comments=Count('comments')).order_by('-total_comments')[:count]
        
  • filter: filter 就是在模板中对某些量做处理,比如 uppercase 等。filter 几乎看下名字就知道是什么含义。

    • 对于特别的功能,可以自定义 filter

      from django.utils.safestring import mark_safe
      import markdown
      
      @register.filter(name='markdown')
      def markdown_format(text):
          return mark_safe(markdown.markdown(text))
      
  • context processor 从 request 中构造 dict, 这个 dict 可以用在模板中。

    from .cart import Cart
    
    def cart(request):
        return {'cart': Cart(request)}
    

    context processor 要生效,需要写到 settings.py 中。

    TEMPLATES = [
                    {
                    'BACKEND': 'django.template.backends.django.DjangoTemplates',
                    'DIRS': [],
                    'APP_DIRS': True,
                    'OPTIONS': {
                        'context_processors': [
                        'django.template.context_processors.debug',
                        'django.template.context_processors.request',
                        'django.contrib.auth.context_processors.auth',
                        'django.contrib.messages.context_processors.messages',
                        'cart.context_processors.cart',
                    ],
                    },
                    },
                ]
    
  • Pycharm

    实际用 Pycharm 写 template 时,会发现 Pycharm 能够自动补全 tag 和 filter。

参考文献

  • Django By Example

Recipe Name

Author:王蒙
Tags:Django, Web Frameworks, Python
abstract:Django 权限控制

Audience

Python 开发,网站开发

Problem

  • 认证
  • Model 级权限控制
  • object 级权限控制
  • view 级权限控制

Solution

  • 认证

    django admin, django-brace 等都提供了认证用户的功能。

  • model 级权限控制

    权限的控制粒度是整张表(而不是表中的记录)。django admin 是 django 提供的管理界面。django admin 就提供了 Model 级的权限控制。

  • object 级权限控制

    表中不同的记录对同一用户的权限不同。django-guardian 提供了 object 级的权限控制。

  • view 权限控制

    用户需要特定的权限才能执行 view, django-brace 提供了 access 级的权限控制。

Reference

  • Django By Example

Restful API

作者:王蒙
标签:Django, Web Frameworks, Rest ful, API
简介:不用任何插件,django 就可以实现 restful API。但是在 rest-framework 的帮助下,更容易写出标准,规范的 Restful API。

目标读者

Restful API 相关

预备知识

python, Restful API

问题

  • Restful 常见误区以及 rest-framework 的优势
  • Serializer
  • Parsers/Renderers
  • Generic APIView
  • authentication and permission
  • viewset and router

解决办法

  • Restful 常见误区以及 rest-framework 的优势

    • http 服务,返回 json 就是 restful 接口;返回 html 网页就不是 restful 接口。

      Restful 是为方便前后端沟通而设计的规范。具体特征是: Resource-Oriented, Stateless。 http 协议是网络协议,http 服务可能遵守 Restful API 的规范,也可能不遵守 Restful API 的规范。 http 协议很容易符合 Restful 规范。使用 http 协议注意实现如下几点就能够实现 Restful 规范。

      • Resource-Oriented,尽量用名词设计 API。对于资源的操作,使用 http 方法来完成。
      • 使用 Content-Type 控制资源的表现形式。json,xml 是常用的表现形式,标准的 Restful API 一般会提供多种表现形式,用 Content-Type 确定到底如何展现。
      • GET, HEAD 不修改资源。POST 修改资源。
      • 返回码,返回信息要自说明(self-document)。力求做到用只要看返回,就知道请求的操作怎么样了。
      • 状态转移通过 url 连接跳转。
    • rest-framework 的优势

      刚刚的论述,已经说明不是简单写个返回值是 json 字符串的 view,就能整出标准的 restful 接口(之前这样写的 restful API 是不太规范的)。

      rest-frameworks 为写 Restful API 提供了 GenericView, Parser and Render, Permission Control 。使用这些现成的组件,方便写出规范的 Restful API。

  • Serializer

    Restful API 要做的事儿,可能是去查询数据库。对于数据库定义 Serializer/Deserializer 可以方便的把数据库查询结果整成 Restful 接口的返回值。

    rest-framework 提供了三个 serializer 基类:

    • Serializer: Provides serialization for normal Python class instances
    • ModelSerializer: Provides serialization for model instances
    • HyperlinkedModelSerializer: The same as ModelSerializer, but represents object relationships with links rather than primary keys
    from rest_framework import serializers
    from ..models import Subject
    class SubjectSerializer(serializers.ModelSerializer):
        class Meta:
            model = Subject
            fields = ('id', 'title', 'slug')
    

    对于有外键的 model , serializer 既可以选择呈现外键的值(integer),也可以选择呈现外键对应的对象。

    class CourseSerializer(serializers.ModelSerializer):
        # 选择呈现外键的值(integer)
        modules = ModuleSerializer()
    
        class Meta:
            model = Course
            fields = ('id', 'subject', 'title', 'slug',
                      'overview', 'created', 'owner', 'modules')
    
    class CourseSerializer(serializers.ModelSerializer):
        # 选择呈现外键对应的对象
        modules = ModuleSerializer(many=True)
    
        class Meta:
            model = Course
            fields = ('id', 'subject', 'title', 'slug',
                      'overview', 'created', 'owner', 'modules')
    
  • parsers and renderers

    在 django project 的 settings.py 文件配置 parsers and renders,restful api 就会根据设计根据 Content-Type header 来表现返回结果。上面的 serializer 是把数据转成字典,而 parsers/renders 是把返回结果整成二进制串。

    可能会配置 settings.py 中的 REST_FRAMEWORK -> DEFAULT_RENDERER_CLASSES 项。这样这个 project 默认会用这些 renders。

    parsers 和 renderers 更多细节,参见:

  • Generic API View

    from rest_framework import generics
    from ..models import Subject
    from .serializers import SubjectSerializer
    
    # 如果Restful API 的返回结果是从 QuerySet 中取出来的。那么继承 ListAPIView 和 RetrieveAPIView 可以方便地构造Restful API。
    class SubjectListView(generics.ListAPIView):
        queryset = Subject.objects.all()
        serializer_class = SubjectSerializer
    
    class SubjectDetailView(generics.RetrieveAPIView):
        queryset = Subject.objects.all()
        serializer_class = SubjectSerializer
    
    
    # 如果Restful API 的返回结果不是从 QuerySet 中取出来的。可以继承 APIView 自定义返回值。
    from django.shortcuts import get_object_or_404
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from ..models import Course
    class CourseEnrollView(APIView):
        def post(self, request, pk, format=None):
            course = get_object_or_404(Course, pk=pk)
            course.students.add(request.user)
            return Response({'enrolled': True})
    
  • authentication and permissions

    rest-framework 提供了 BasicAuthentication, TokenAuthenticationSessionAuthentication 类实现认证。

    rest-frameworks 提供了 AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly, DjangoModelPermissionsDjangoObjectPermissions 类实现权限控制。

    from rest_framework.authentication import BasicAuthentication
    from rest_framework.permissions import IsAuthenticated
    class CourseEnrollView(APIView):
        # 指明采用哪个类,做认证
        authentication_classes = (BasicAuthentication,)
        # 指明是什么什么样的权限控制,比如这里 IsAuthenticated 表示只有认证用户才能使用这个 view(API)
        permission_classes = (IsAuthenticated,)
        # ...
    

    继承 rest_framework.permission.BasePermission,重写下面两种方法,可以自定义 permission。

    • has_permission(): View-Level permission check。
    • has_object_permission(): Object-Level permission check。
    from rest_framework.permissions import BasePermission
    class IsEnrolled(BasePermission):
    
    def has_object_permission(self, request, view, obj):
        return obj.students.filter(id=request.user.id).exists()
    
  • viewset and router

    Viewset 可以使用 router 绑定 url。在 Viewset 中 view 比较多时,会很有用。

    from django.conf.urls import url, include
    from rest_framework import routers
    from . import views
    
    
    router = routers.DefaultRouter()
    router.register('courses', views.CourseViewSet)
    
    class CourseViewSet(viewsets.ReadOnlyModelViewSet):
        queryset = Course.objects.all()
        serializer_class = CourseSerializer
    
        @detail_route(methods=['post'],
                      authentication_classes=[BasicAuthentication],
                      permission_classes=[IsAuthenticated])
        def enroll(self, request, *args, **kwargs):
            course = self.get_object()
            course.students.add(request.user)
            return Response({'enrolled': True})
    
        @detail_route(methods=['get'],
                      serializer_class=CourseWithContentsSerializer,
                      authentication_classes=[BasicAuthentication],
                      permission_classes=[IsAuthenticated, IsEnrolled])
        def contents(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)
    

参考文献

追踪用户

作者:

王蒙

标签:

Django, Web Frameworks, Python, session, cookies

简介:

用户在一个网站连续执行的操作称为是一个 session。在一个 session 中,我们希望服务器记录我们在该 session 的行为。比如我们登录了网站之后,去浏览网站时,我们希望网站知道我们已经登录,不希望网站不断重复地要求我们输入账号密码。

http 协议中,session/cookies 保存用户一次会话的历史。本节总结 Django 是如何处理 session/cookies 的。

目标读者

Python 开发,网站开发

预备知识

Python, cookies, session

问题

Django 如何处理 session/cookies ?

解决办法

客户端会把用户本次会话的信息保存到 http request 的 cookies 中发送给服务器处理。服务器端缓存接收到用户本次会话信息(在 http request 的 cookies 中)到 session 中,然后把服务器端的 session 保存到 http response 的 set-cookies 中发送回客户端。每次客户端接到服务器端的 http response,也会根据 http response 中的 cookies 更新客户端的 cookies。这样不管是客户端和服务器端都缓存了用户本次会话的信息。

django 提供了 django.contrib.sessions.middleware.SessionMiddleware 来处理上面这套逻辑。该中间件,使得开发人员在写 view 的时候可以通过 request.session(是个字典) 访问 http request 请求的 cookies。

可以使用不同的引擎来保存session, 为此使用 session 时,需要在 settings.py 配置

  • SESSION_ENGINE: 使用数据库,缓存(Memcached),文件还是什么来保存session。Django 默认使用数据库保存 session,推荐使用缓存保存 session ,因为使用缓存效率更高。
  • SESSION_COOKIES_AGE: 过期时间。在写 views.py 时,可以通过调用 set_expiry() 方法操作过期行为。
  • SESSION_COOKIES_DOMAIN
  • SESSION_COOKIES_SECURE: 不接受 http 的cookies,只有 https 的 cookies 是有效的。
  • SESSION_EXPIRE_AT_BROWSER_CLOSE: 客户端关闭浏览器会使 cookies 过期。
  • SESSION_SAVE_EVERY_REQUEST: 每次请求都会更新服务器端的 session。
class Cart(object):

    def __init__(self, request):
        """
        Initialize the cart.
        """
        self.session = request.session
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            # save an empty cart in the session
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart

    def __len__(self):
        """
        Count all items in the cart.
        """
        return sum(item['quantity'] for item in self.cart.values())

    def __iter__(self):
        """
        Iterate over the items in the cart and get the products from the database.
        """
        product_ids = self.cart.keys()
        # get the product objects and add them to the cart
        products = Product.objects.filter(id__in=product_ids)
        for product in products:
            self.cart[str(product.id)]['product'] = product

        for item in self.cart.values():
            item['price'] = Decimal(item['price'])
            item['total_price'] = item['price'] * item['quantity']
            yield item

    def add(self, product, quantity=1, update_quantity=False):
        """
        Add a product to the cart or update its quantity.
        """
        product_id = str(product.id)
        if product_id not in self.cart:
            self.cart[product_id] = {'quantity': 0,
                                      'price': str(product.price)}
        if update_quantity:
            self.cart[product_id]['quantity'] = quantity
        else:
            self.cart[product_id]['quantity'] += quantity
        self.save()

    def save(self):
        # update the session cart
        self.session[settings.CART_SESSION_ID] = self.cart
        # mark the session as "modified" to make sure it is saved
        self.session.modified = True

参考文献

Django 单元测试

读者:王蒙
标签:Django, Web Framework, Python, unittest
简介:介绍对于 Django 如何做单元测试。

目标读者

Django 开发

解决问题

没啥好说的,看参考文献

message

作者:王蒙
标签:Django, Web Frameworks, Python, message, notification
简介:Django 中 django.contrib.messages 提供了消息通知机制。

目标读者

Python 开发,网络开发

预备知识

Python, Django View

问题

使用消息通知用户操作成功了,操作失败了等等。

解决办法

django.contrib.messages 提供了

  • success(): 表示操作成功了
  • info(): 普通消息
  • warning(): 报警
  • error(): 表示操作失败了
  • debug(): 调试信息
from django.contrib import messages
@login_required
def edit(request):
    if request.method == 'POST':
        # ...
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Profile updated '\
            'successfully')
        else:
            messages.error(request, 'Error updating your profile')
    else:
        user_form = UserEditForm(instance=request.user)

Celery

Author:王蒙
Tags:Django, Web Frameworks
abstract:Django 可以通过消息队列和任务队列来实现异步。Python 中常用的任务队列是 Celery,Django 对 Celery 提供了支持。

Audience

Python 开发,网站开发

Prerequisites

Celery, Python

Problem

Django 如何通过 Celery 实现异步?

Solution

TODO: 我感觉主要是消息队列 Celery 的内容。不过可能 Django 需要怎么配置一下。这个我还没有研究。

Reference

  • Django By Example.

注册认证登出

作者:王蒙
标签:Django, authenticate, login, logout, oauth2
简介:介绍 Django 认证,登录,登出的用法。

目标读者

Python 网站开发

问题

  • Django 自带的认证机制
  • 自定义用户
  • 自定义认证
  • 自定义权限