普通视图

发现新文章,点击刷新页面。
昨天以前Yuechuan Blog

<流畅的Python>:可迭代的对象、迭代器和生成器

作者 Yuechuan Xiao
2020年7月22日 16:25

当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深——这通常意味着自己正在手动完成事情,本应该通过写代码来让宏的扩展自动实现。

——Paul Graham, Lisp 黑客和风险投资人

Python 内置了迭代器模式,用于进行惰性运算,按需求一次获取一个数据项,避免不必要的提前计算。

迭代器在 Python 中并不是一个具体类型的对象,更多地使指一个具体协议

  • 所有的生成器都是迭代器。它们都实现了迭代器接口,区别于迭代器用于从集合中取出元素,生成器用来生成元素。

迭代器协议

Python 解释器在迭代一个对象时,会自动调用 iter(x)
内置的 iter 函数会做以下操作:

  1. 检查对象是否实现了 __iter__ 方法(abc.Iterable),若实现,且返回的结果是个迭代器(abc.Iterator),则调用它,获取迭代器并返回;
  2. 若没实现,但实现了 __getitem__ 方法(abc.Sequence),若实现则尝试从 0 开始按顺序获取元素并返回;
  3. 以上尝试失败,抛出 TypeError,表明对象不可迭代。

判断一个对象是否可迭代,最好的方法不是用 isinstance 来判断,而应该直接尝试调用 iter 函数。

注:可迭代对象和迭代器不一样。从鸭子类型的角度看,可迭代对象 Iterable 要实现 __iter__,而迭代器 Iterator 要实现 __next__. 不过,迭代器上也实现了 __iter__,用于返回自身

迭代器的具体实现

《设计模式:可复用面向对象软件的基础》一书讲解迭代器设计模式时,在“适用性”一 节中说:
迭代器模式可用来:

  • 访问一个聚合对象的内容而无需暴露它的内部表示

  • 支持对聚合对象的多种遍历

  • 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)

为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用 iter(my_iterable) 都新建一个独立的迭代器。

序列可迭代的原因:iter函数

解释器需要迭代对象 x 时,会自动调用 iter(x):

  1. 检查对象是否实现了 __iter__ 方法并调用,获取到迭代器

  2. 如果没有实现__iter__, 检查是否有 __getitem__ 函数,尝试按顺序下标获取元素

  3. 如果上述状况都不符合, 抛出 “C object is not iterable” 异常

这就是为什么这个示例需要定义 SentenceIterator 类。所以,不应该把 Sentence 本身作为一个迭代器,否则每次调用 iter(sentence) 时返回的都是自身,就无法进行多次迭代了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 通过实现迭代器协议,让一个对象变得可迭代
import re
from collections import abc


class Sentence:
def __init__(self, sentence):
self.sentence = sentence
self.words = re.findall(r'\w+', sentence)

def __iter__(self):
"""返回 iter(self) 的结果"""
return SentenceIterator(self.words)


# 推荐的做法是对迭代器对象进行单独实现
class SentenceIterator(abc.Iterator):
def __init__(self, words):
self.words = words
self._index = 0

def __next__(self):
"""调用时返回下一个对象"""
try:
word = self.words[self._index]
except IndexError:
raise StopIteration()
else:
self._index += 1

return word



sentence = Sentence('Return a list of all non-overlapping matches in the string.')
assert isinstance(sentence, abc.Iterable) # 实现了 __iter__,就支持 Iterable 协议
assert isinstance(iter(sentence), abc.Iterator)
for word in sentence:
print(word, end='·')
Return·a·list·of·all·non·overlapping·matches·in·the·string·

上面的例子中,我们的 SentenceIterator 对象继承自 abc.Iterator 通过了迭代器测试。而且 Iterator 替我们实现了 __iter__ 方法。
但是,如果我们不继承它,我们就需要同时实现 __next__ 抽象方法和实际迭代中并不会用到的 __iter__ 非抽象方法,才能通过 Iterator 测试。

可迭代对象与迭代器的比较

  • 可迭代对象

    使用 iter 内置函数可以获取迭代器的对象。

  • 迭代器

    迭代器是一种对象:实现了 __next__ 方法,返回序列中的下一个元素,并在无元素可迭代时抛出 StopIteration 异常。

生成器函数

生成器函数的工作原理

  • 只要函数的定义体中有 yield 关键字,该函数就是生成器函数。

  • 调用生成器函数会返回生成器对象。

如果懒得自己写一个迭代器,可以直接用 Python 的生成器函数来在调用 __iter__ 时生成一个迭代器。

注:在 Python 社区中,大家并没有对“生成器”和“迭代器”两个概念做太多区分,很多人是混着用的。不过无所谓啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用生成器函数来帮我们创建迭代器
import re


class Sentence:
def __init__(self, sentence):
self.sentence = sentence
self.words = re.findall(r'\w+', sentence)

def __iter__(self):
for word in self.words:
yield word
return

sentence = Sentence('Return a list of all non-overlapping matches in the string.')
for word in sentence:
print(word, end='·')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用 re.finditer 来惰性生成值
# 使用生成器表达式(很久没用过了)
import re


class Sentence:
def __init__(self, sentence):
self.re_word = re.compile(r'\w+')
self.sentence = sentence

def __iter__(self):
return (match.group()
for match in self.re_word.finditer(self.sentence))

sentence = Sentence('Return a list of all non-overlapping matches in the string.')
for word in sentence:
print(word, end='·')

案例:使用 itertools模块生成等差数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 实用模块
import itertools

# takewhile & dropwhile
print(list(itertools.takewhile(lambda x: x < 3, [1, 5, 2, 4, 3])))
print(list(itertools.dropwhile(lambda x: x < 3, [1, 5, 2, 4, 3])))
# zip
print(list(zip(range(5), range(3))))
print(list(itertools.zip_longest(range(5), range(3))))

# itertools.groupby
animals = ['rat', 'bear', 'duck', 'bat', 'eagle', 'shark', 'dolphin', 'lion']
# groupby 需要假定输入的可迭代对象已经按照分组标准进行排序(至少同组的元素要连在一起)
print('----')
for length, animal in itertools.groupby(animals, len):
print(length, list(animal))
print('----')
animals.sort(key=len)
for length, animal in itertools.groupby(animals, len):
print(length, list(animal))
print('---')
# tee
g1, g2 = itertools.tee('abc', 2)
print(list(zip(g1, g2)))
1
2
3
4
5
6
7
8
9
10
# 使用 yield from 语句可以在生成器函数中直接迭代一个迭代器
from itertools import chain

def my_itertools_chain(*iterators):
for iterator in iterators:
yield from iterator

chain1 = my_itertools_chain([1, 2], [3, 4, 5])
chain2 = chain([1, 2, 3], [4, 5])
print(list(chain1), list(chain2))
[1, 2, 3, 4, 5] [1, 2, 3, 4, 5]

iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。

1
2
3
4
5
6
7
8
# iter 的神奇用法
# iter(callable, sentinel)
import random

def rand():
return random.randint(1, 6)
# 不停调用 rand(), 直到产出一个 5
print(list(iter(rand, 5)))

<流畅的Python> 接口:从协议到抽象基类

作者 Yuechuan Xiao
2020年7月9日 16:24

抽象类表示接口。
——Bjarne Stroustrup, C++ 之父

本章讨论的话题是接口:

鸭子类型的代表特征动态协议,到使接口更明确、能验证实现是否符合规定的抽象基类(Abstract Base Class, ABC)。

接口的定义:对象公开方法的子集,让对象在系统中扮演特定的角色。
协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。
允许一个类上只实现部分接口。

接口与协议

  • 什么是接口

    对象公开方法的子集,让对象在系统中扮演特定的角色。

  • 鸭子类型与动态协议

  • 受保护的类型与私有类型不能在接口中

  • 可以把公开的数据属性放在接口中

案例:通过实现 getitem 方法支持序列操作

1
2
3
4
5
6
7
8
9
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]

f = Foo()
print(f[1])

for i in f:
print(i)

Foo 实现了序列协议的 __getitem__ 方法。因此可支持下标操作。

Foo 实例是可迭代的对象,因此可以使用 in 操作符

案例:在运行时实现协议——猴子补丁

FrenchDeck 类见前面章节。

FrenchDeck 实例的行为像序列,那么其实可以用 random 的 shuffle 方法来代替在类中实现的方法。

1
2
3
4
5
6
from random import shuffle
from frenchdeck import FrenchDeck

deck = FrenchDeck()
shuffle(deck)
# TypeError: 'FrenchDeck' object does not support item assigment

FrenchDeck 对象不支持元素赋值。这是因为它只实现了不可变的序列协议,可变的序列还必须提供 __setitem__ 方法。

1
2
3
4
5
6
7
def set_card(deck, pos, card):
deck._cards[pos] = card

FrenchDeck.__setitem__ = set_card
shuffle(deck)
print(deck[:5])
print(deck[:5])

这种技术叫做猴子补丁:在运行是修改类或程序,而不改动源码。缺陷是补丁代码与要打补丁的程序耦合紧密。

抽象基类(abc)

抽象基类是一个非常实用的功能,可以使用抽象基类来检测某个类是否实现了某种协议,而这个类并不需要继承自这个抽象类。
collections.abcnumbers 模块中提供了许多常用的抽象基类以用于这种检测。

有了这个功能,我们在自己实现函数时,就不需要非常关心外面传进来的参数的具体类型isinstance(param, list)),只需要关注这个参数是否支持我们需要的协议isinstance(param, abc.Sequence)以保障正常使用就可以了。

但是注意:从 Python 简洁性考虑,最好不要自己创建新的抽象基类,而应尽量考虑使用现有的抽象基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 抽象基类
from collections import abc


class A:
pass

class B:
def __len__(self):
return 0

assert not isinstance(A(), abc.Sized)
assert isinstance(B(), abc.Sized)
assert abc.Sequence not in list.__bases__ # list 并不是 Sequence 的子类
assert isinstance([], abc.Sequence) # 但是 list 实例支持序列协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 在抽象基类上进行自己的实现
from collections import abc

class FailedSized(abc.Sized):
pass


class NormalSized(abc.Sized):
def __len__(self):
return 0


n = NormalSized()
print(len(n))
f = FailedSized() # 基类的抽象协议未实现,Python 会阻止对象实例化

有一点需要注意:抽象基类上的方法并不都是抽象方法。
换句话说,想继承自抽象基类,只需要实现它上面所有的抽象方法即可,有些方法的实现是可选的。
比如 Sequence.__contains__,Python 对此有自己的实现(使用 __iter__ 遍历自身,查找是否有相等的元素)。但如果你在 Sequence 之上实现的序列是有序的,则可以使用二分查找来覆盖 __contains__ 方法,从而提高查找效率。

我们可以使用 __abstractmethods__ 属性来查看某个抽象基类上的抽象方法。这个抽象基类的子类必须实现这些方法,才可以被正常实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 自己定义一个抽象基类
import abc

# 使用元类的定义方式是 class SomeABC(metaclass=abc.ABCMeta)
class SomeABC(abc.ABC):
@abc.abstractmethod
def some_method(self):
raise NotImplementedError


class IllegalClass(SomeABC):
pass

class LegalClass(SomeABC):
def some_method(self):
print('Legal class OK')


l = LegalClass()
l.some_method()
il = IllegalClass() # Raises

虚拟子类

使用 register 接口可以将某个类注册为某个 ABC 的“虚拟子类”。支持 register 直接调用注册,以及使用 @register 装饰器方式注册(其实这俩是一回事)。
注册后,使用 isinstance 以及实例化时,解释器将不会对虚拟子类做任何方法检查。
注意:虚拟子类不是子类,所以虚拟子类不会继承抽象基类的任何方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 虚拟子类
import abc
import traceback

class SomeABC(abc.ABC):
@abc.abstractmethod
def some_method(self):
raise NotImplementedError

def another_method(self):
print('Another')

@classmethod
def __subclasshook__(cls, subcls):
"""
在 register 或者进行 isinstance 判断时进行子类检测
https://docs.python.org/3/library/abc.html#abc.ABCMeta.__subclasshook__
"""
print('Subclass:', subcls)
return True


class IllegalClass:
pass

SomeABC.register(IllegalClass) # 注册
il = IllegalClass()
assert isinstance(il, IllegalClass)
assert SomeABC not in IllegalClass.__mro__ # isinstance 会返回 True,但 IllegalClass 并不是 SomeABC 的子类
try:
il.some_method() # 虚拟子类不是子类,不会从抽象基类上继承任何方法
except Exception as e:
traceback.print_exc()

try:
il.another_method()
except Exception as e:
traceback.print_exc()

用 Prettytable 在终端输出漂亮的表格

作者 Yuechuan Xiao
2020年6月5日 15:07

转载来源:https://linuxops.org/blog/python/prettytable.html

一、前言

最近在用python写一个小工具,这个工具主要就是用来管理各种资源的信息,比如阿里云的ECS等信息,因为我工作的电脑使用的是LINUX,所以就想着用python写一个命令行的管理工具,基本的功能就是同步阿里云的资源的信息到数据库,然后可以使用命令行查询。

因为信息是展现在命令行中的,众所周知,命令行展现复杂的文本看起来着实累人,于是就想着能像表格那样展示,那看起来就舒服多了。

prettytable库就是这么一个工具,prettytable可以打印出美观的表格,并且对中文支持相当好(如果有试图自己实现打印表格,你就应该知道处理中文是多么的麻烦)

说明:本文使用Markdown语法编写,为了展示方便,以及复制方便,所以本文中没有使用截图,因为格式控制的问题,文章中的运行结果会出现一些分割线的偏移,在终端中呈现并此问题,请各位手动去操作验证。

二、安装

prettytable并非python的内置库,通过 pip install prettytable即可安装。

三、一个小示例

我们先来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
print(table)

以上示例运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
linuxops@deepin:~$ python p.py 
+------+----------+----------+------------+
| 编号 | 云编号 | 名称 | IP地址 |
+------+----------+----------+------------+
| 1 | server01 | 服务器01 | 172.16.0.1 |
| 2 | server02 | 服务器02 | 172.16.0.2 |
| 3 | server03 | 服务器03 | 172.16.0.3 |
| 4 | server04 | 服务器04 | 172.16.0.4 |
| 5 | server05 | 服务器05 | 172.16.0.5 |
| 6 | server06 | 服务器06 | 172.16.0.6 |
| 7 | server07 | 服务器07 | 172.16.0.7 |
| 8 | server08 | 服务器08 | 172.16.0.8 |
| 9 | server09 | 服务器09 | 172.16.0.9 |
+------+----------+----------+------------+

在以上的示例中,我们通过form导入了表格库。 table实例化了一个表格库,并且添加了['编号','云编号','名称','IP地址']为表头,如果没有添加表头,那么会以默认的Field+编号显示,例如:

1
2
3
+---------+----------+----------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 |
+---------+----------+----------+------------+

所以为更直观看出每一列的意义,还是要添加表头的。

四、添加数据

prettytable提供了多种的添加数据的方式,最常用的应该就是按行按列添加数据了。

A、按行添加数据 table.add_row

在上面简单的示例中,我们就是按行添加数据的。

添加的数据必须要是列表的形式,而且数据的列表长度要和表头的长度一样。在实际的使用中,我们应该要关注到添加的数据是否和表头对应,这一点很重要。

B、按列添加数据 table.add_column()

看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable()
table.add_column('项目', ['编号','云编号','名称','IP地址'])
table.add_column('值', ['1','server01','服务器01','172.16.0.1'])
print(table)

运行结果如下:

1
2
3
4
5
6
7
8
+-------+--------+------------+
| index | 项目 | 值 |
+-------+--------+------------+
| 1 | 编号 | 1 |
| 2 | 云编号 | server01 |
| 3 | 名称 | 服务器01 |
| 4 | IP地址 | 172.16.0.1 |
+-------+--------+------------+

以上示例中,我们通过add_column来按列添加数据,按列添加数据不需要在实例化表格的时候制定表头,它的表头是在添加列的时候指定的。

table.add_column('项目', ['编号','云编号','名称','IP地址']) 这一行代码为例,项目指定了这个列的表头名为"项目",['编号','云编号','名称','IP地址']为列的值,同样为列表。

C、从csv文件添加数据

PrettyTable不仅提供了手动按行按列添加数据,也支持直接从csv文件中读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_csv
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable()
fp = open("res.csv", "r")
table = from_csv(fp)
print(table)
fp.close()

如果要读取cvs文件数据,必须要先导入from_csv,否则无法运行。上面的示例运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------+----------+----------+------------+
| 编号 | 云编号 | 名称 | IP地址 |
+------+----------+----------+------------+
| 1 | server01 | 服务器01 | 172.16.0.1 |
| 2 | server02 | 服务器02 | 172.16.0.2 |
| 3 | server03 | 服务器03 | 172.16.0.3 |
| 4 | server04 | 服务器04 | 172.16.0.4 |
| 5 | server05 | 服务器05 | 172.16.0.5 |
| 6 | server06 | 服务器06 | 172.16.0.6 |
| 7 | server07 | 服务器07 | 172.16.0.7 |
| 8 | server08 | 服务器08 | 172.16.0.8 |
| 9 | server09 | 服务器09 | 172.16.0.9 |
+------+----------+----------+------------+

csv文件不能通过xls直接重命名得到,会报错。如果是xls文件,请用另存为csv获得csv文件

D、从sql查询值添加

从数据库查询出来的数据可以直接导入到表格打印,下面的例子使用了sqlite3,如果使用的是mysql也是一样的,只要能查询到数据就能导入到表格中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_db_cursor
import sqlite3
reload(sys)
sys.setdefaultencoding('utf8')

conn = sqlite3.connect("/tmp/aliyun.db")
cur = conn.cursor()
cur.execute("SELECT * FROM res")
table = from_db_cursor(cur)
print(table)

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------+----------+----------+------------+
| 编号 | 云编号 | 名称 | IP地址 |
+------+----------+----------+------------+
| 1 | server01 | 服务器01 | 172.16.0.1 |
| 2 | server02 | 服务器02 | 172.16.0.2 |
| 3 | server03 | 服务器03 | 172.16.0.3 |
| 4 | server04 | 服务器04 | 172.16.0.4 |
| 5 | server05 | 服务器05 | 172.16.0.5 |
| 6 | server06 | 服务器06 | 172.16.0.6 |
| 7 | server07 | 服务器07 | 172.16.0.7 |
| 8 | server08 | 服务器08 | 172.16.0.8 |
| 9 | server09 | 服务器09 | 172.16.0.9 |
+------+----------+----------+------------+

E、从HTML导入数据

支持从html的表格中导入,请看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_html
reload(sys)
sys.setdefaultencoding('utf8')

html_string='''<table>
<tr>
<th>编号</th>
<th>云编号</th>
<th>名称</th>
<th>IP地址</th>
</tr>
<tr>
<td>1</td>
<td>server01</td>
<td>服务器01</td>
<td>172.16.0.1</td>
</tr>
<tr>
<td>2</td>
<td>server02</td>
<td>服务器02</td>
<td>172.16.0.2</td>
</tr>
</table>'''

table = from_html(html_string)

print(table[0])

运行结果如下:

1
2
3
4
5
6
+------+----------+----------+------------+
| 编号 | 云编号 | 名称 | IP地址 |
+------+----------+----------+------------+
| 1 | server01 | 服务器01 | 172.16.0.1 |
| 2 | server02 | 服务器02 | 172.16.0.2 |
+------+----------+----------+------------+

如上示例中,我们可以导入html的表格,但是不一样的地方是print语句,使用html表格导入数据的时候print的必须是列表中的第一个元素,否则有可能会报[<prettytable.PrettyTable object at 0x7fa87feba590>]这样的错误。

这是因为table并不是PrettyTable对象,而是包含单个PrettyTable对象的列表,它通过解析html而来,所以无法直接打印table,而需要打印table[0]

五、表格输出格式

正如支持多种输入一样,表格的输出也支持多种格式,我们在上面中的例子中已经使用了print的方式输出,这是一种常用的输出方式。

A、print

直接通过print打印出表格。这种方式打印出的表格会带边框。

B、输出HTML格式的表格

print(table.get_html_string())可以打印出html标签的表格。

在上面的例子中,使用print(table.get_html_string())会打印出如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<table>
<tr>
<th>编号</th>
<th>云编号</th>
<th>名称</th>
<th>IP地址</th>
</tr>
<tr>
<td>1</td>
<td>server01</td>
<td>服务器01</td>
<td>172.16.0.1</td>
</tr>
<tr>
<td>2</td>
<td>server02</td>
<td>服务器02</td>
<td>172.16.0.2</td>
</tr>
</table>

六、选择性输出

prettytable在创建表格之后,你依然可以有选择的输出某些特定的行.

A、输出指定的列

print table.get_string(fields=["编号", "IP地址"])可以输出指定的列

B、输出前两行

通过print(table.get_string(start = 0, end = 2))的可以打印出指定的列,当然startend参数让我可以自由控制显示区间。当然区间中包含start不包含end,是不是很熟悉这样的用法?

根据输出指定行列的功能,我们可以同时指定行和列来输出,这里就不说明了。

C、将表格切片

从上面的输出区间,我们做一个大胆的假设,既然区间包含start不包含end这种规则和切片的一样,我们可以不可通过切片来生成一个新的表格然后将其打印。

事实上是可以的。

1
2
new_table = table[0:2]
print(new_table)

如上代码段中,我们就可以打印出0到1行共2行的表格,python的切片功能异常强大,配合切片我们可以自由的输入任意的行。

D、输出排序

有时候我们需要对输出的表格进行排序,使用print table.get_string(sortby="编号", reversesort=True)就可以对表格进行排序,其中reversesort指定了是否倒序排序,默认为False,即默认正序列排序。

sortby指定了排序的字段。

七、表格的样式

A、内置样式

通过set_style()可以设置表格样式,prettytable内置了多种的样式个人觉得MSWORD_FRIENDLYPLAIN_COLUMNSDEFAULT 这三种样式看起来比较清爽,在终端下显示表格本来看起就很累,再加上一下花里胡哨的东西看起来就更累。

除了以上推荐的三种样式以外,还有一种样式不得不说,那就是RANDOM,这是一种随机的样式,每一次打印都会在内置的样式中随机选择一个,比较好玩。

具体内置了几种样式,请各位参考官网完整自己尝试输出看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import MSWORD_FRIENDLY
from prettytable import PLAIN_COLUMNS
from prettytable import RANDOM
from prettytable import DEFAULT

reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.set_style(DEFAULT)

print(table)

B、自定义样式

除了内置的样式以外,PrettyTable也提供了用户自定义,例如对齐方式,数字输出格式,边框连接符等等

C、设置对齐方式

align提供了用户设置对齐的方式,值有lrc方便代表左对齐,右对齐和居中 如果不设置,默认居中对齐。

D、控制边框样式

在PrettyTable中,边框由三个部分组成,横边框,竖边框,和边框连接符(横竖交叉的链接符号)

如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable

reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.align[1] = 'l'

table.border = True
table.junction_char='$'
table.horizontal_char = '+'
table.vertical_char = '%'

print(table)
table.border`控制是否显示边框,默认是`True

table.junction_char控制边框连接符

table.horizontal_char控制横边框符号

table.vertical_char控制竖边框符号

上例运行如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$++++++$++++++++++$++++++++++$++++++++++++$
% 编号 % 云编号 % 名称 % IP地址 %
$++++++$++++++++++$++++++++++$++++++++++++$
% 1 % server01 % 服务器01 % 172.16.0.1 %
% 3 % server03 % 服务器03 % 172.16.0.3 %
% 2 % server02 % 服务器02 % 172.16.0.2 %
% 9 % server09 % 服务器09 % 172.16.0.9 %
% 4 % server04 % 服务器04 % 172.16.0.4 %
% 5 % server05 % 服务器05 % 172.16.0.5 %
% 6 % server06 % 服务器06 % 172.16.0.6 %
% 8 % server08 % 服务器08 % 172.16.0.8 %
% 7 % server07 % 服务器07 % 172.16.0.7 %
$++++++$++++++++++$++++++++++$++++++++++++$

以上简单介绍了表格常用的一些样式设置,具体的请参考官方网站。

https://github.com/jazzband/prettytable

https://code.google.com/archive/p/prettytable/wikis/Tutorial.wiki

<流畅的Python> 函数装饰器与闭包

作者 Yuechuan Xiao
2020年4月29日 16:14

有很多人抱怨,把这个特性命名为“装饰器”不好。主要原因是,这个名称与 GoF 书使用的不一致。装饰器这个名称可能更适合在编译器领域使用,因为它会遍历并注解语法书。
—“PEP 318 — Decorators for Functions and Methods”

本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。

讨论内容:

  • Python 如何计算装饰器语法
  • Python 如何判断变量是不是局部的
  • 闭包存在的原因和工作原理
  • nonlocal 能解决什么问题
  • 实现行为良好的装饰器
  • 标准库中有用的装饰器
  • 实现一个参数化的装饰器

装饰器基础

装饰器是可调用的对象,其参数是一个函数(被装饰的函数)。

装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

装饰器两大特性:

  1. 能把被装饰的函数替换成其他函数
  2. 装饰器在加载模块时立即执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 装饰器通常会把函数替换成另一个函数
def decorate(func):
def wrapped():
print('Running wrapped()')
return wrapped

@decorate
def target():
print('running target()')
target()

# 以上写法等同于
def target():
print('running target()')
target = decorate(target)
target()

# 这里真正调用的是装饰器返回的函数

def deco(func):
def inner():
print('running iner()')
return inner

@deco
def target():
print('running target()')
target()
# target 现在是 inner 的引用
target

Python 何时执行装饰器

装饰器在导入时(模块加载时)立即执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# registration.py

registry = []
def register(func):
print('running register {}'.format(func))
registry.append(func)
return func

@register
def f1():
print('running f1()')

@register
def f2():
print('running f2()')

def f3():
print('running f3()')

def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()

if __name__=='__main__':
main()

# python3 registration.py
# output:
# running register <function f1 at 0x10b4194d0>
# running register <function f2 at 0x10b419ef0>
# running main()
# registry -> [<function f1 at 0x10b4194d0>, <function f2 at 0x10b419ef0>]
# running f1()
# running f2()
# running f3()

# import registration
# running register <function f1 at 0x10d89e950>
# running register <function f2 at 0x10d89e050>

通过上面的例子,强调装饰器函数在导入模块式立即执行,而普通函数在被调用时运行。导入时和运行时的区别。

  • 装饰器函数通常与被装饰函数不在同一模块。
  • register 装饰器返回的函数没有变化

上面的装饰器会原封不动地返回被装饰的函数,而不一定会对函数做修改。
这种装饰器叫注册装饰器,通过使用它来中心化地注册函数,例如把 URL 模式映射到生成 HTTP 响应的函数上的注册处。

使用装饰器

1
2
3
4
5
6
7
8
9
10
promos = []

def promotion(promo_func):
promos.append(promo_func)
return promo_func

@promotion
def fidelity(order):
"""积分 >= 1000 提供 5% 折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0

变量作用域规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 比较两个例子

b = 6
def f1(a):
print(a)
print(b)
f1(3)


def f2(a):
print(a)
print(b)
b = 9 # b 此时为局部变量
f2(3)

Python 假定在函数体内部的变量为局部变量。如果未在局部变量中找到,会逐级向上查找变量。

如果想在函数中赋值时让解释器把 b 当做全局变量,用 global 关键字

1
2
3
4
5
6
def f3(a):
global b
print(a)
print(b)
b = 9
f3(3)

闭包

闭包和匿名函数常被弄混。只有涉及到嵌套函数时才有闭包问题。

闭包指延伸了作用域的函数,其中包含函数定义体中的引用,但非定义体中定义的非全局变量。和函数是否匿名无关。关键是能访问定义体之外定义的非全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Averager():
def __init__(self):
self.series = []

def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)

avg = Averager()
avg(10)
avg(11)
avg(12)

def make_averager():
series = [] # 自由变量

def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)

return averager

avg = make_averager()
avg(10)
avg(11)
avg(12)

avg.__code__.co_varnames
avg.__code__.co_freevars
avg.__closure__
avg.__closure__[0].cell_contents

在 averager 函数中,series 是自由变量,指未在本地作用域绑定的变量。

通过 __code__.co_freevars __closure__ 查看自由变量和闭包

闭包是一种函数,保留定义函数时存在的自由变量的绑定。调用函数时,虽然定义作用域不可用了,但仍能使用那些绑定

只有嵌套在其他函数中的函数才可能需要处理不在全局作用域的外部变量

nonlocal 声明

下面一个例子有缺陷:

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_averager():
count = 0
total = 0

def averager(new_value):
count += 1
total += new_value
return total / count

return averager

avg = make_averager()
avg(10)

注意 count, total 的赋值语句使它们成为局部变量,在赋值是会隐式创建局部变量,这样它们就不是自由变量了,因此不会保存在闭包中。

为解决这个问题,Python3 引入了 nonlocal 声明,作用是吧变量标记为自由变量,即使在函数中为变量新值了,也会变成自由变量。在闭包中的绑定也会更新

对于没有 nonlocal 的 Python2 PEP3104

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_averager():
count = 0
total = 0

def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count

return averager

avg = make_averager()
avg(10)

实现一个简单的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time

def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> % r' %(elapsed, name, arg_str, result))
return result
return clocked

@clock
def snooze(seconds):
time.sleep(seconds)

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n-1)

if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装装饰函数本该返回的值,同时做一些额外操作

1
factorial.__name__

上述实现的 clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的 __name__, __doc__ 属性

functools.wraps 装饰器把相关属性从 func 复制到 clocked 中,还能正确处理关键字函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import functools

def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_str = ', '.join(repr(arg) for arg in args)
if kwargs:
pairs = ['%s=%s' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> % r' %(elapsed, name, arg_str, result))
return result
return clocked

if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

标准库中的装饰器

Python 内置的三个装饰器分别为 property, classmethodstaticmethod.

但 Python 内置的库中,有两个装饰器很常用,分别为 functools.lru_cachefunctools.singledispatch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)

print(fibonacci(6))

@functools.lru_cache() # () 是因为 lru_cache 可以接受配置参数
# functools.lru_cache(maxsize=128, typed=False)
@clock # 叠放装饰器
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)

print(fibonacci(6))

单分派反函数

Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的辩题,也无法使用不同的方式处理不同的数据类型。

一种常见的方法是把 htmlize 编程一个分派函数,使用 if-elif-else 分别调用专门的函数。但这样不便于模块的拓展,而且臃肿

functoos.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。
使用 functoos.singledispatch 装饰的普通函数会变成反函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 生成 HTML 显示不同类型的 python 对象
import html

def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)

# htmlize({1, 2, 3})
# htmlize(abs)
# htmlize('hwimich & Co.\n- a game')
# htmlize(42)
# print(htmlize(['alpha', 66, {3, 2, 1}]))

from functools import singledispatch
from collections import abc
import numbers

@singledispatch # 标记处理 object 类型的基函数
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral) # Integral 是 int 的虚拟超类
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n<ul>'

htmlize({1, 2, 3})
htmlize(abs)
htmlize('hwimich & Co.\n- a game')
htmlize(42)
print(htmlize(['alpha', 66, {3, 2, 1}]))

只要可能,注册的专门函数应该处理抽象基类(numbers.Integral, abc.MutableSequence), 不要处理具体实现(int,list)

这样代码支持的兼容类型更广泛。

使用 singledispatch 可以在系统的任何地方和任何模块注册专门函数。

叠放装饰器

1
2
3
4
5
6
7
@d1
@d2
def func():
pass

# 等同于
func = d1(d2(func))

参数化装饰器

为了方便理解,可以把参数化装饰器看成一个函数:这个函数接受任意参数,返回一个装饰器(参数为 func 的另一个函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 参数化的注册装饰器
registry = set()

# 这是一个工厂函数,用来构建装饰器函数
def register(active=True):
# decorate 是真正的装饰器
def decorate(func):
print('running register(active=%s)->decorate(%s)' % (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate

@register(active=False)
def f1():
print('running f1()')

@register()
def f2():
print('running f2()')

def f3():
print('running f3()')

f1()
f2()
f3()
register()(f3)
registry
register(active=False)(f2)

参数化 clock 装饰器

为 clock 装饰器添加一个功能,让用户传入一个格式化字符串,控制被装饰函数的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate

# @clock()
# @clock('{name}: {elapsed}s')
@clock('{name}{args} dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

小结

本节先编写了一个没有内部函数的 @register 装饰器。 然后实现了有两层嵌套函数的参数化装饰器 @clock()

参数化装饰器基本上设计至少两层嵌套函数。

标准库 functools 提供两个非常重要的装饰器 @lru_cache() 和 @singledispatch

理解装饰器,需要区分导入时、运行时、变量作用域,闭包等。

推荐阅读:decorator 第三方库

1
2


<流畅的 Python> 一等函数

作者 Yuechuan Xiao
2020年4月22日 16:38

不管别人怎么说或怎么想,我从未觉得 Python 受到来自函数式语言的太多影响。我非常熟悉命令式语言,如 C 和 Algol 68,虽然我把函数定为一等对象,但是我并不把 Python 当作函数式编程语言。
—— Guido van Rossum: Python 仁慈的独裁者

在 Python 中,函数是一等对象。
编程语言理论家把“一等对象”定义为满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

函数即为对象

1
2
3
4
5
6
7
def factorial(n):
"""returns n!"""
return 1 if n < 2 else n * factorial(n-1)

factorial(42)
factorial.__doc__
type(factorial)

通过 type(factorial) 可以看到 function 是一种类型,或者说,函数也是对象,可以通过__doc__ 去访问它的属性。

那么作为对象的函数,也能作为参数被传递。函数式风格编程也基于此

1
2
3
4
5
fact = factorial
fact
fact(5)
map(factorial, range(11))
list(map(fact, range(11)))

高阶函数

输入或者输出是函数的即为高阶函数(higher order function)。例如:mapsorted

1
2
3
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
# function len() as key
sorted(fruits, key=len)

map、filter 和 reduce 的替代品

函数式语言通常提供 mapfilterreduce 三个高阶函数。
Python 中引入了列表推导和生成式表达式,可以替代它们且更容易阅读。

1
2
3
4
list(map(fact, range(6)))
[fact(n) for n in range(6)]
list(map(factorial, filter(lambda n : n % 2, range(6))))
[factorial(n) for n in range(6) if n % 2]

mapfilter 返回生成器,可用生成器表达式替代
reduce 常用求和,目前最好使用 sum 替代

1
2
3
4
5
from functools import reduce
from operator import add

reduce(add, range(100))
sum(range(100))

sumreduce 把操作连续应用在序列元素上,得到返回值

all(iterable), any(iterable) 也是规约函数

  • all(iterable): 每个元素为真,返回真
  • any(iterable): 存在一个元素为真,返回真

匿名函数

Python 支持 lambda 表达式。 它是函数对象,在句法上被限制只能用存表达式。

参数列表中最适合使用匿名函数。

1
2
3
4
# 根据单词末尾字符排序

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])

Python 的可调用对象

  • 用户定义的函数:使用 deflambda 创建
  • 内置函数:如 lentime.strfttime
  • 内置方法:如 dict.get
  • 类:先调用 __new__ 创建实例,再对实例运行 __init__ 方法
  • 类的实例:如果类上定义了 __call__ 方法,则实例可以作为函数调用
  • 生成器函数:使用 yield 关键字的函数或方法,调用生成器函数会返回生成器对象

判断对象是否能调用,使用内置的 callable() 函数

1
2
abs, str, 13
[callable(obj) for obj in (abs, str, 13)]

用户定义的可调用类型

任何 Python 对象都可以表现得像函数,只需实现实例方法 __call__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random 

class BingoCage:
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)

def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')

def __call__(self):
return self.pick()

bingo = BingoCage(range(3))
bingo.pick()
bingo()
callable(bingo)

实现 __call__ 方法的类是创建函数类对象的简便方式。
函数类对象有自己的状态,即为实例变量。装饰器函数也可以有.

函数内省

内省(introspection)可以查看函数内部的细节,函数有许多属性。使用 dir 函数可以查看,或使用 code 属性

1
2
dir(factorial)
# factorial.__code__.co_varnames
1
2
3
4
5
6
7
# eg:5-9
# 列出常规对象没有而函数有的属性

class C: pass
obj = C()
def func(): pass
sorted(set(dir(func)) - set(dir(obj)))

函数属性说明
// 插入表格

从定位参数到仅限关键字参数

本节讨论 python 参数处理机制。py3 提供了仅限关键字参数(keyword-only argument)
调用函数使用 * 和 ** 展开可迭代对象。

  • positional argument 位置参数
  • keyword-only argument 仅限关键字参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def tag(name, *content, cls=None, **attrs):
"""生成一个或多个 HTML 标签"""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)

tag('br')
tag('p', 'hello')
tag('p', 'hello', 'world') # 'hello', 'world' -> *content
tag('p', 'hello', id=33) # id=33 -> **attrs
tag('p', 'hello', 'world', cls='sidebar')
tag(content='testing', name="img")
my_tag = {
'name': 'img',
'title': 'Sunset Boulevard',
'src': 'sunset.jpg',
'cls': 'framed'
}
tag(**my_tag)

cls 参数只能通过关键字指定,而不能通过位置参数指定。

定义函数时若只想定仅限关键字参数,要把它放在带有 * 参数后面,如果不想支持数量不定的位置参数,但支持 keyowrd-only, 在函数签名中放一个 *

1
2
3
4
5
def f(a, *, b):
return a, b

# f(1, 2)
f(1, b=2)

获取关于参数的信息

上面提到,函数内省可以查看函数内部信息,通过 HTTP 微框架 Bobo 作为例子来看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# eg: 5-12
# hello.py
import bobo

@bobo.query('/')
def hello(person):
return 'Hello %s!' % person

# 在环境中执行 bobo -f hello.py, 若运行端口为 http://localhost:8080/
# 没有传入参数
# curl -i http://localhost:8080/
# HTTP/1.0 403 Forbidden
# Date: Wed, 22 Apr 2020 06:23:33 GMT
# Server: WSGIServer/0.2 CPython/3.7.4
# Content-Type: text/html; charset=UTF-8
# Content-Length: 103

# <html>
# <head><title>Missing parameter</title></head>
# <body>Missing form variable person</body>
# </html>

# 传入参数
# curl -i http://localhost:8080/?person=Jim
# HTTP/1.0 200 OK
# Date: Wed, 22 Apr 2020 06:24:47 GMT
# Server: WSGIServer/0.2 CPython/3.7.4
# Content-Type: text/html; charset=UTF-8
# Content-Length: 10

# Hello Jim!%

Bobo 如何知道函数需要哪个参数呢?

函数对象有 __defaults__ 属性,其值为一个元祖,保存着位置参数和关键字参数的默认值。

keyword-only 参数默认值保存在 __kwdefaults__ 属性中。

参数的名称在 __code__ 属性中,其值为 code 对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def clip(text, max_len=80):
"""在 max_len 前后的第一个空格处截断文本"""
end = None
if (len(text)) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)

return text[:end].rstrip()


clip.__defaults__
# clip.__code__
# clip.__code__.co_varnames
# clip.__code__.co_argcount

函数签名信息,参数和默认值是分开的。可以使用 inspect 模块提取这些信息

1
2
3
4
5
6
7
from inspect import signature

sig = signature(clip)
sig
str(sig)
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)

kind 属性值在 _Parameterkind 类中,列举如下:

  • POSTIONAL_OR_KEYWORD
  • VAR_POSITIONAL
  • VAR_KEYWORD
  • KEYWORD-ONLY
  • POSITIONAL_ONLY

inspect.Signaturebind 方法,可以把任意个参数绑定在签名中的形参上。

框架可以使用此方法在调用函数前验证参数

1
2
3
4
5
6
7
8
9
10
11
12
13
import inspect
sig = inspect.signature(tag)
my_tag = {
'name': 'img',
'title': 'Sunset Boulevard',
'src': 'sunset.jpg',
'cls': 'framed'}
bound_args = sig.bind(**my_tag)
bound_args

del my_tag['name']
# missing argument error
bound_args = sig.bind(**my_tag)

框架和 IDE 工具可以使用这些信息验证代码

函数注解(annotation)

各个参数可以在 : 后添加注解表达式。

参数有默认值,注解放在参数名和 = 号之间,注解返回值在函数声明末尾添加 -> 和表达式

注解不会做任何处理,只存储在函数 __annotations__ 属性中。

注解只是元数据,可以供 IDE,框架和装饰器等工具使用

inspect.signature() 函数知道怎么提取注解

1
2
3
4
def clip(text: str, max_len: 'int > 0' = 80) -> str:
pass

clip.__annotations__
1
2
3
4
5
6
7
8
from inspect import signature

sig = signature(clip)
sig.return_annotation

for param in sig.parameters.values():
note = repr(param.annotation).ljust(13)
print(note, ':', param.name, '=', param.default)

支持函数式编程的包

operator 模块

operator 里有很多函数,对应着 Python 中的内置运算符,使用它们可以避免编写很多无趣的 lambda 函数,如:

  • add: lambda a, b: a + b
  • or_: lambda a, b: a or b
  • itemgetter: lambda a, b: a[b]
  • attrgetter: lambda a, b: getattr(a, b)
1
2
3
4
5
6
7
8
from functools import reduce
from operator import mul

def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))

def fact(n):
return reduce(mul, range(1, n+1))

还有一类函数,能替代从序列中取出或读取元素属性的 lambda 表达式。如 itemgetterattrgetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
metro_data = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

from operator import itemgetter

for city in sorted(metro_data, key=lambda fields: fields[1]):
print(city)

for city in sorted(metro_data, key=itemgetter(1)):
print(city)

# itemgetter 返回提取的值构成的元祖,可以用来提取指定字段或调整元祖顺序
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))

itemgetter 使用 [] 运算符,因为它不仅支持序列,还支持映射和任何实现 __getitem__ 的类

attrgetter 作用相似,它创建的函数根据名称提取对象的属性。包含 . 的,会进入到嵌套对象提取属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import namedtuple

LatLong = namedtuple('Latlong', 'lat long')
Metorpolis = namedtuple('Metorpolis', 'name cc pop coord')

metro_areas = [Metorpolis(name, cc, pop, LatLong(lat, long))
for name, cc, pop, (lat, long) in metro_data]

metro_areas[0]
metro_areas[0].coord.lat

from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')

for city in sorted(metro_areas, key=attrgetter('coord.lat')):
print(name_lat(city))
1
2
import operator
[name for name in dir(operator) if not name.startswith('_')]

operator 模块的函数可以通过 dir(operator) 查看。

介绍 methodcaller, 它的作用与前两个函数相似,它创建的函数会在对象调用参数指定的方法

1
2
3
4
5
6
7
from operator import methodcaller

s = 'The time has come'
upcase = methodcaller('upper')
upcase(s)
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)

使用 functools.partial 冻结参数

functools 最常用的函数有 reduce,之前已经介绍过。余下函数中最有用的是 partial 及其变体 partialmethod

它的作用是:把原函数某些参数固定。

partial 第一个函数是可调用对象,后面跟任意个位置参数和关键字参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from operator import mul
from functools import partial

triple = partial(mul, 3)
triple(7)

list(map(triple, range(1, 10)))

picture = partial(tag, 'img', cls='pic-frame')
picture(src='wumpus.jepg')
picture
picture.func
picture.args
picture.keywords

functoos.partialmethod 作用与 partial 一样,不过适用于处理方法的

小结

探讨 Python 函数的一等特性。意味着可以把函数赋值给变量,传入其他函数,存储于数据结构中,以及访问函数属性。

高阶函数是函数式编程的重要组成。

Python 的可调用对象: 7种

函数及其注解有丰富的特性。可通过 inspect 模块读取

最后介绍了 operator 模块中的一些函数,可以替换掉功能有限的 lambda 表达式。

Zen of Python(Python之禅)

作者 Yuechuan Xiao
2020年4月15日 16:54

Beautiful is better than ugly. (优美比丑陋好)

Explicit is better than implicit.(清晰比晦涩好)

Simple is better than complex.(简单比复杂好)

Complex is better than complicated.(复杂比错综复杂好)

Flat is better than nested.(扁平比嵌套好)

Sparse is better than dense.(稀疏比密集好)

Readability counts.(可读性很重要)

Special cases aren’t special enough to break the rules.(特殊情况也不应该违反这些规则)

Although practicality beats purity.(但现实往往并不那么完美)

Errors should never pass silently.(异常不应该被静默处理)

Unless explicitly silenced.(除非你希望如此)

In the face of ambiguity, refuse the temptation to guess.(遇到模棱两可的地方,不要胡乱猜测)

There should be one-- and preferably only one --obvious way to do it.(肯定有一种通常也是唯一一种最佳的解决方案)

Although that way may not be obvious at first unless you’re Dutch.(虽然这种方案并不是显而易见的,因为你不是那个荷兰人这里指的是Python之父Guido

Now is better than never.(现在开始做比不做好)

Although never is often better than *right* now.(不做比盲目去做好极限编程中的YAGNI原则

If the implementation is hard to explain, it’s a bad idea.(如果一个实现方案难于理解,它就不是一个好的方案)

If the implementation is easy to explain, it may be a good idea.(如果一个实现方案易于理解,它很有可能是一个好的方案)

Namespaces are one honking great idea – let’s do more of those!(命名空间非常有用,我们应当多加利用)

Django 导出和导入数据

作者 Yuechuan Xiao
2020年4月13日 18:57

dumpdata 命令:

它可以用来备份(导出)模型实例或整个数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
./manage.py dumpdata --help
usage: manage.py dumpdata [-h] [--format FORMAT] [--indent INDENT] [--database DATABASE] [-e EXCLUDE] [--natural-foreign] [--natural-primary] [-a] [--pks PRIMARY_KEYS]
[-o OUTPUT] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[app_label[.ModelName] [app_label[.ModelName] ...]]

Output the contents of the database as a fixture of the given format (using each model's default manager unless --all is specified).

positional arguments:
app_label[.ModelName]
Restricts dumped data to the specified app_label or app_label.ModelName.

optional arguments:
-h, --help show this help message and exit
--format FORMAT Specifies the output serialization format for fixtures.
--indent INDENT Specifies the indent level to use when pretty-printing output.
--database DATABASE Nominates a specific database to dump fixtures from. Defaults to the "default" database.
-e EXCLUDE, --exclude EXCLUDE
An app_label or app_label.ModelName to exclude (use multiple --exclude to exclude multiple apps/models).
--natural-foreign Use natural foreign keys if they are available.
--natural-primary Use natural primary keys if they are available.
-a, --all Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager.
--pks PRIMARY_KEYS Only dump objects with given primary keys. Accepts a comma-separated list of keys. This option only works when you specify one model.
-o OUTPUT, --output OUTPUT
Specifies file to which the output is written.
--version show program's version number and exit
-v {0,1,2,3}, --verbosity {0,1,2,3}
Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
--settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the DJANGO_SETTINGS_MODULE environment variable will be
used.
--pythonpath PYTHONPATH
A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
--traceback Raise on CommandError exceptions
--no-color Don't colorize the command output.
--force-color Force colorization of the command output.
  • 基础数据库导出

    1
    ./manage.py dumpdata > db.json

    这会导出整个数据库到 db.json

  • 备份指定的 app

    1
    ./manage.py dumpdata admin > admin.json

    这会导出 admin 应用的内容到 admin.json

  • 备份指定的数据表

    1
    ./manage.py dumpdata admin.logentry > logentry.json

    这会导出 admin.logentry 数据表的所有数据

    1
    ./manage.py dumpdata auth.user > user.json

    这会导出 auth.user 数据表的所有数据

  • dumpdata —exclude

    —exclude 选项用来指定无需被导出的 apps/tables

    1
    ./manage.py dumpdata --exclude auth.permission > db.json

    这会导出整个数据库,但不包括 auth.permisson

  • dumpdata —intent

    默认情况,dumpdata 的输出会挤在同一行,可读性很差。使用 —indent 可以设定缩进美化输出

    1
    ./manage.py dumpdata auth.user --indent 2 > user.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    [
    {
    "model": "auth.user",
    "pk": 1,
    "fields": {
    "password": "pbkdf2_sha256$150000$i8oET981EnSJ$d2RCpfY76gFHbwUs1HekSK+pOLYMJFcJ1wFcuyf6R28=",
    "last_login": "2020-04-13T09:21:34.639Z",
    "is_superuser": true,
    "username": "xiao",
    "first_name": "",
    "last_name": "",
    "email": "yuechuan.xiao@artosyn.cn",
    "is_staff": true,
    "is_active": true,
    "date_joined": "2020-04-13T08:59:01.310Z",
    "groups": [],
    "user_permissions": []
    }
    },
    {
    "model": "auth.user",
    "pk": 2,
    "fields": {
    "password": "pbkdf2_sha256$150000$PgBKh5sMAE1y$xdFkYi+gprF1v2rlOyw2OOsRn87zSeTVLJ9dGfoXzIw=",
    "last_login": null,
    "is_superuser": true,
    "username": "qa",
    "first_name": "",
    "last_name": "",
    "email": "qa@artosyn.cn",
    "is_staff": true,
    "is_active": true,
    "date_joined": "2020-04-13T08:59:16.279Z",
    "groups": [],
    "user_permissions": []
    }
    }
    ]
  • dumpdata —format

    默认输出格式为 JSON。使用 —format 可以指定输出格式

    • json
    • xml
    • yaml
    1
    ./manage.py dumpdata auth.user --indent 2 --format xml > user.xml

    这会输出 xml 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <?xml version="1.0" encoding="utf-8"?>
    <django-objects version="1.0">
    <object model="auth.user" pk="1">
    <field name="password" type="CharField">pbkdf2_sha256$150000$i8oET981EnSJ$d2RCpfY76gFHbwUs1HekSK+pOLYMJFcJ1wFcuyf6R28=</field>
    <field name="last_login" type="DateTimeField">2020-04-13T09:21:34.639297+00:00</field>
    <field name="is_superuser" type="BooleanField">True</field>
    <field name="username" type="CharField">xiao</field>
    <field name="first_name" type="CharField"></field>
    <field name="last_name" type="CharField"></field>
    <field name="email" type="CharField">yuechuan.xiao@artosyn.cn</field>
    <field name="is_staff" type="BooleanField">True</field>
    <field name="is_active" type="BooleanField">True</field>
    <field name="date_joined" type="DateTimeField">2020-04-13T08:59:01.310568+00:00</field>
    <field name="groups" rel="ManyToManyRel" to="auth.group"></field>
    <field name="user_permissions" rel="ManyToManyRel" to="auth.permission"></field>
    </object>
    <object model="auth.user" pk="2">
    <field name="password" type="CharField">pbkdf2_sha256$150000$PgBKh5sMAE1y$xdFkYi+gprF1v2rlOyw2OOsRn87zSeTVLJ9dGfoXzIw=</field>
    <field name="last_login" type="DateTimeField"><None></None></field>
    <field name="is_superuser" type="BooleanField">True</field>
    <field name="username" type="CharField">qa</field>
    <field name="first_name" type="CharField"></field>
    <field name="last_name" type="CharField"></field>
    <field name="email" type="CharField">qa@artosyn.cn</field>
    <field name="is_staff" type="BooleanField">True</field>
    <field name="is_active" type="BooleanField">True</field>
    <field name="date_joined" type="DateTimeField">2020-04-13T08:59:16.279788+00:00</field>
    <field name="groups" rel="ManyToManyRel" to="auth.group"></field>
    <field name="user_permissions" rel="ManyToManyRel" to="auth.permission"></field>
    </object>
    </django-objects>
  • loaddata 命令

    用来导入 fixtures(dumpdata 导出的数据)到数据库

    1
    ./manage.py loaddata user.json

    这会导入 user.json 里的内容到数据库

  • 恢复 fresh database

    当你通过 dumpdata 命令备份整个数据库时,它将备份所有数据表。若使用 dump 文件导入到另外的 Django 项目,会导致 IntegrityError

    可以通过备份时加入选项 —exclude contenttypesauth.permissions 数据表修复此问题

    1
    ./manage.py dumpdata --exclude auth.permission --exclude contenttypes > db.json

    现在再用 loaddata 命令导入 fresh dababase

    1
    ./manage.py loaddata db.json

Python代码规范 Pep8

作者 Yuechuan Xiao
2020年4月1日 14:12

以下所有内容包含在官方 PEP(Python Enhancement Proposals) 链接为 [pep8][https://www.python.org/dev/peps/pep-0008/]

简要版本

  • 代码编排

    • 缩进。4个空格的缩进(编辑器都可以完成此功能),不使用Tap,更不能混合使用Tap和空格。

      针对不同编辑器兼容性,对 tab 可能有不同的标准,导致样式不统一。

    • 每行最大长度79,换行可以使用反斜杠,最好使用圆括号。换行点要在操作符的后边敲回车。

      早期 unix 主机终端只能显示 80 个字符。

      通过限制所需的编辑器窗口宽度,可以并排打开多个文件,并且在使用在相邻列中显示两个版本的代码查看工具时,效果很好。

    • 类和top-level函数定义之间空两行;

      类中的方法定义之间空一行;

      函数内逻辑无关段落之间空一行;

      其他地方尽量不要再空行。

  • 文档编排

    • 模块内容的顺序:

      模块说明和docstring

      import

      globals&constants

      其他定义。

      其中import部分,又按标准、三方和自己编写顺序依次排放,之间空一行。

    • 不要在一句import中多个库,比如import os, sys不推荐。
      如果采用from XX import XX引用库,可以省略‘module.’,都是可能出现命名冲突,这时就要采用import XX。

      如果有命名冲突。可以使用 from X import Y as Z

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # -*- coding: utf-8 -*-
    #!/bin/python3
    # -------------------------------------------------------------------------
    # Author: Yuechuan Xiao
    # @Date: 2020-01-09 14:56:57
    # @LastEditors: Yuechuan Xiao
    # @LastEditTime: 2020-03-30 16:33:48
    # @Description:
    # report.py: gen build's jira issues html report.
    # -------------------------------------------------------------------------

    """
    Docstring

    reporter.py is used to generate a html report for specific build.
    """

    # Standard library
    import os
    import re
    from collections import namedtuple


    # Third party lib
    # Import multi-subcass from A package.
    from jinja2 import (
    Environment,
    FileSystemLoader,
    Template,
    select_autoescape)
    from jira import JIRA

    # If you have lcoal import
    # from .utils import X
    # from . import utils
  • 空格的使用
    总体原则,避免不必要的空格。

    • 各种右括号前不要加空格。
    • 逗号、冒号、分号前不要加空格。
    • 函数的左括号前不要加空格。如func(1)
    • 序列的左括号前不要加空格。如list[2]
    • 操作符左右各加一个空格,不要为了对齐增加空格。
    • 函数默认参数使用的赋值符左右省略空格。
    • 不要将多句语句写在同一行,尽管使用‘;’允许。
    • if/for/while语句中,即使执行语句只有一句,也必须另起一行。
  • 命名规范
    总体原则,新编代码必须按下面命名风格进行,现有库的编码尽量保持风格。

    • 尽量单独使用小写字母l,大写字母O等容易混淆的字母。
    • 模块命名尽量短小,使用全部小写的方式,可以使用下划线。
    • 包命名尽量短小,使用全部小写的方式,不可以使用下划线。
    • 类的命名使用CapWords的方式,模块内部使用的类采用_CapWords的方式。_
    • 异常命名使用CapWords+Error后缀的方式。
    • 全局变量尽量只在模块内有效,类似C语言中的static。实现方法有两种,一是__all__机制;二是前缀一个下划线
    • 函数命名使用全部小写的方式,可以使用下划线。
    • 常量命名使用全部大写的方式,可以使用下划线。
    • 类的属性(方法和变量)命名使用全部小写的方式,可以使用下划线。
    • 类的属性有3种作用域public、non-public和subclass API,可以理解成C++中的public、private、protected,non-public属性前,前缀一条下划线。
    • 类的属性若与关键字名字冲突,后缀一下划线,尽量不要使用缩略等其他方式。
    • 为避免与子类属性命名冲突,在类的一些属性前,前缀两条下划线。比如:类Foo中声明__a,访问时,只能通过Foo._Foo__a,避免歧义。如果子类也叫Foo,那就无能为力了。
    • 类的方法第一个参数必须是self,而静态方法第一个参数必须是cls。
  • 注释
    总体原则,错误的注释不如没有注释。所以当一段代码发生变化时,第一件事就是要修改注释!
    针对团队情况(是否国际化),注释倾向使用英文,最好是完整的句子,首字母大写,句后要有结束符,结束符后跟两个空格,开始下一句。如果是短语,可以省略结束符。

    • 块注释,在一段代码前增加的注释。在‘#’后加一空格。段落之间以只有‘#’的行间隔。比如:
    1
    2
    3
    4
    5
    6
    # Description : Module config.
    #

    # Input : None
    #
    # Output : None
    • 行注释,在一句代码后加注释。比如:x = x + 1 # Increment x
      但是这种方式尽量少使用。可以在 Magic Number 时使用。
    • 避免无谓的注释。
  • 文档描述
    1 为所有的共有模块、函数、类、方法写docstrings;非共有的没有必要,但是可以写注释(在def的下一行)。
    2 如果docstring要换行,参考如下例子,详见PEP 257

    1
    2
    3
    4
    5
    """Return a foobang

    Optional plotz says to frobnicate the bizbaz first.

    """
  • 编码建议

    • 编码中考虑到其他python实现的效率等问题,比如运算符‘+’在CPython(Python)中效率很高,都是Jython中却非常低,所以应该采用.join()的方式。
      2 尽可能使用i

    • s is not取代==,比如if x is not None 要优于if x
      3 使用基于类的异常,每个模块或包都有自己的异常类,此异常类继承自Exception。
      4 异常中不要使用裸露的except,except后跟具体的exceptions。
      5 异常中try的代码尽可能少。比如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      try:
      value = collection[key]
      except KeyError:
      return key_not_found(key)
      else:
      return handle_value(value)

      要优于
      try:
      # Too broad!
      return handle_value(collection[key])
      except KeyError:
      # Will also catch KeyError raised by handle_value()
      return key_not_found(key)
    • 使用startswith() and endswith()代替切片进行序列前缀或后缀的检查。比如

      1
      2
      3
      4
      5
      Yes: if foo.startswith(‘bar’):优于
      No: if foo[:3] == ‘bar’:
      - 使用isinstance()比较对象的类型。比如
      Yes: if isinstance(obj, int): 优于
      No: if type(obj) is type(1):
    • 判断序列空或不空,有如下规则

      1
      2
      3
      4
      5
      Yes: if not seq:
      if seq:
      优于
      No: if len(seq)
      if not len(seq)
    • 字符串不要以空格收尾。

    • 二进制数据判断使用 if boolvalue的方式。

Reference

Django 项目后端模板

作者 Yuechuan Xiao
2020年2月16日 14:55

Django 项目本身可以通过 django-admin 或者直接运行 python manage.py ARGS 来进行脚手架生成。但是生成的项目框架层次不算太好。

首先生成一个 Django 项目:

1
django-admin startproject backend

生成的项目框架如下:

1
2
3
4
5
6
7
backend
├── backend
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

其中的两个 backend 分别表示项目,以及 app 全局配置

建立文件夹 apps 用来放置应用,把内层 backend 改为 conf

1
2
3
4
5
6
7
8
backend
├── apps
├── conf
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py

注意这里需要配置以下几个文件:

1
2
3
4
5
# manage.py 
...
# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
...
1
2
3
4
5
6
7
8
# settings.py
...
# ROOT_URLCONF = 'backend.urls'
ROOT_URLCONF = 'conf.urls'
...
# WSGI_APPLICATION = 'backend.wsgi.application'
WSGI_APPLICATION = 'conf.wsgi.application'
...

现在可以测试 python manage.py runserver 是否可以起来。

接下来新建 Apps

1
2
mkdir apps/login
python manage.py startapp login apps/login

注册 app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# settings.py

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['apps'], # 添加 apps 文件夹
'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',
],
},
},
]


INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'apps.login',
]

导入 URL

1
2
3
4
5
6
7
...
from apps.login import urls as login_urls

urlpatterns = [
path('admin/', admin.site.urls),
path('login/', include(login_urls)),
]

现在一个基本的项目结构就建立好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
backend
├── apps
│ └── login
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── conf
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
└── manage.py

相比起来层次更清晰,而且也更适合用作前后端分离的命名

Docker 基础使用

作者 Yuechuan Xiao
2019年12月19日 15:43

安装

官方安装文档

Windows

安装

Debian/Ubuntu

1
2
3
4
5
6
7
8
# 官方
curl -sSL https://get.docker.com/ | sh

# 阿里云
curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

# Daocloud
curl -sSL https://get.daocloud.io/docker | sh

MacOS

1
brew cask install docker

简单指令

  • 查看 Docker 版本

    版本信息:docker --version

    配置信息: docker info

    help 信息: docker --help

  • 运行第一个 Docker 镜像

    docker run hello-world

Docker 命令行工具

Docker CLI 指令分为两类,一类是 Management Commands,一类是镜像与容器 Commands。

你可以通过 docker –help 进行查看。

Docker 的命令格式为:

docker [OPTIONS] COMMAND

这里我们选取常用的几个命令进行示例:

docker pull : 从镜像仓库中拉取镜像

1
2
3
4
5
6
7
# 从镜像仓库中拉取 Ubuntu 镜像
docker pull Ubuntu

# 运行 Ubuntu 镜像,分配 tty 进入交互模式
# -i : interactive mode
# -t: 分配 tty
docker run –it ubuntu:latest

Docker-CLI 与 Linux 语法具有相似性,例如:

1
2
3
4
5
6
7
8
# 列出所有镜像
docker image ls

# 列出所有容器
docker container ls

# 查看容器状态
docker container ps

如果你有 Linux 基础,那么相信对于 Docker-CLI 上手还是比较容易的。

TRY IT OUT #1

docker run -d -P daocloud.io/daocloud/dao-2048

-d 表示容器启动后在后台运行

用 docker ps 查看容器运行状态如图:

image-20191219151718661

看到端口映射关系 0.0.0.0:32768->80。指宿主机的 32768 端口映射到容器的 80 端口

用浏览器打开 localhost:32768

image-20191219151800494

Dockerfile 简介

Docker 可以从 Dockerfile 文件中构建镜像.

Dockerfile 语法请参考:https://docs.docker.com/engine/reference/builder/

下面列出一些最常用的语法:

  • FROM : 这会从 Docker Hub 中拉取镜像,目的镜像基于所拉取的镜像进行搭建

  • WORKDIR: RUN, CMD, ENTRYPOINT, COPY, ADD以此为工作路径

  • COPY: 拷贝文件或文件夹到指定路径

  • RUN:镜像的最上层执行命令,执行后的结果会被提交,作为后续操作基于的镜像。

  • EXPOSE:暴露端口号

  • ENV: 设置环境变量

  • CMD [“executable”,“param1”,“param2”]:一个 Dockerfile 应该只有一处 CMD 命令,如果有多处,则最后一处有效。

#TRy it out #2

首先准备一个 Dockerfile 文件 与一个 app.py 文件

image-20191219151919172

分别执行

image-20191219151933441

中间打印输出

image-20191219152000816image-20191219152012608image-20191219152022398

Docker 生成 container时会生成一个唯一的 container-id,在上图中 stop 命令用到了 container-id。当然,你可以使用 docker tag 命令对 container 进行重命名。

-p 4000:80 : 指的是从宿主机端口 4000 映射到容器端口 80

现在打开浏览器访问 localhost:4000:

image-20191219152037355

Reference

docker 官方文档

<流畅的Python> 序列构成的数组

作者 Yuechuan Xiao
2019年12月16日 20:27

你可能注意到了,之前提到的几个操作可以无差别地应用于文本、列表和表格上。
我们把文本、列表和表格叫作数据火车……FOR 命令通常能作用于数据火车上。
——Geurts、Meertens 和 Pemberton
ABC Programmer’s Handbook

内置序列类型概览

  • 容器序列
    listtuplecollections.deque 这些序列能存放不同类型的数据。
  • 扁平序列
    strbytesbytearraymemoryviewarray.array,这类序列只能容纳一种类型。

容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。

序列类型还能按照能否被修改来分类。

  • 可变序列
    listbytearrayarray.arraycollections.dequememoryview
  • 不可变序列
    tuplestrbytes

列表推导和生成器表达式

列表推导和可读性

列表推导是构建列表(list)的快捷方式,生成器表达式用来穿件其他任何类型的序列。

1
2
3
4
5
6
7
8
9
10
# 比较两段代码
symbols = 'abcde'
# 1
codes = []
for symbol in symbols:
codes.append(ord(symbol))
print(codes)
# 2
codes = [ord(symbol) for symbol in symbols]
print(codes)

列表推导能够提升可读性。
只用列表推导来创建新的列表,并尽量保持简短(不要超过一行)

列表推导同 filter 和 map 的比较

1
2
3
4
5
6
7
symbols = 'abcde'

beyond_ascii = [ord(s) for s in symbols if ord(s) > 100]
print(beyond_ascii)

beyond_ascii = list(filter(lambda c: c > 100, map(ord, symbols)))
print(beyond_ascii)
[101][101]

笛卡尔积

1
2
3
4
5
6
7
8
9
10
11
colors = ['black', 'white'] 
sizes = ['S', 'M', 'L']

tshirts = [(color, size) for color in colors
for size in sizes]
print(tshirts)

tshirts = [(color, size) for size in sizes
for color in colors]
print(tshirts)
# 注意顺序是依照 for-loop
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')][('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'), ('black', 'L'), ('white', 'L')]

生成器表达式

列表推导与生成器表达式的区别:

  • 生成器表达式遵守实现了迭代器接口,可以逐个地产出元素。列表推导是先建立一个完整的列表,再将这个列表传递到构造函数里。
  • 语法上近似,方括号换成圆括号
1
2
3
4
5
# symbols = 'abcde'
print(tuple(ord(symbol) for symbol in symbols))

import array
print(array.array('I', (ord(symbol) for symbol in symbols)))
  • 如果生成器表达式是一个函数调用过程中的唯一参数,则不需要额外括号
  • 生成器会在 for-loop 运行时才生成一个组合。逐个产出元素
1
2
3
4
5
colors = ['black', 'white'] 
sizes = ['S', 'M', 'L']

for tshirt in ('%s %s' %(c, s) for c in colors for s in sizes):
print(tshirt)
black Sblack Mblack Lwhite Swhite Mwhite L

元祖不仅仅是不可变的列表

元祖与记录

  • 元祖是对数据的记录
  • 元祖的位置信息为数据赋予了意义。对元祖内元素排序,位置信息将丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
# LA 国际机场经纬度
lax_coordinates = (33.9425, -118.408056)
# 城市,年份,人口(单位:百万),人口变化(单位:百分比),面积
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
# country_code, passport number
traveler_ids = [('USA', '31195855'), ('BBA', 'CE342567'), ('ESP', 'XDA205856')]

for passport in sorted(traveler_ids):
print('%s%s' % passport)

# 拆包(unpacking)
for country, _ in traveler_ids:
print(country)
BBACE342567ESPXDA205856USA31195855USABBAESP

元祖拆包

  • 平行赋值
1
2
3
4
5
lax_coordinates = (33.9425, -118.408056)
# 元祖拆包
latitude, longtitude = lax_coordinates
print(latitude)
print(longtitude)
33.9425-118.408056
  • 交换变量值,不使用中间变量
1
2
3
4
5
a = 3
b = 4
b, a = a, b
print(a)
print(b)
43
  • * 运算符,把一个可迭代对象拆开作为函数参数
1
2
3
4
5
6
7
8
divmod(20, 8)

t = (20, 8)
divmod(*t)

quotient, remainder = divmod(*t)
print(quotient)
print(remainder)
24
  • 函数用元祖形式返回多个值

_ 用作占位符,可以用来处理不需要的数据

1
2
3
4
import os

_, filename = os.path.split('/home/xiao/.ssh/id_rsa.pub')
print(filename)
id_rsa.pub
  • * 处理省下的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a, b, *rest = range(5)
print(a, b, rest)

a, b, *rest = range(3)
print(a, b, rest)

a, b, *rest = range(2)
print(a, b, rest)

# * 前缀只能用在一个变量前,该变量可出现在赋值表达式中任意位置
a, *body, c, d = range(5)
print(a, body, c, d)

*head, b, c, d = range(5)
print(head, b, c, d)
0 1 [2, 3, 4]0 1 [2]0 1 []0 [1, 2] 3 4[0, 1] 2 3 4

嵌套元祖拆包

1
2
3
4
5
6
7
8
9
10
11
12
13
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas: # <2>
if longitude <= 0: # <3>
print(fmt.format(name, latitude, longitude))
                |   lat.    |   long.  Mexico City     |   19.4333 |  -99.1333New York-Newark |   40.8086 |  -74.0204Sao Paulo       |  -23.5478 |  -46.6358

将元祖作为记录仍缺少一个功能:字段命名

具名元祖(numedtuple)

collections.namedtuple 是一个工厂函数,用来构建带字段名的元祖和一个有名字的

namedtuple 构建的类的实例所消耗的内存和元祖是一样的,因为字段名都存在对应的类里。
实例和普通的对象实例小一点,因为 Python 不会用 __dict__ 存放实例的属性

1
2
3
4
5
6
7
8
from collections import namedtuple

# 需要两个参数,类名和类各个字段的名字
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 129.691667))
print(tokyo)
print(tokyo.population)
print(tokyo.coordinates)
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 129.691667))36.933(35.689722, 129.691667)

namedtuple 除了从普通元祖继承的属性外,还有一些专有属性。
常用的有:

  • _fields 类属性
  • _make(iterable) 类方法
  • _asdict() 实例方法
1
2
3
4
5
6
7
8
print(City._fields)
LatLong = namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data)
print(delhi._asdict())

for key, value in delhi._asdict().items():
print(key + ':', value)
('name', 'country', 'population', 'coordinates')OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])name: Delhi NCRcountry: INpopulation: 21.935coordinates: LatLong(lat=28.613889, long=77.208889)

作为不可变列表的元祖

对比列表和元祖的方法
// 插入表格

结论:

  • 除了增减元素相关的方法和__reversed__ 外,元祖支持列表的其他所有方法。

切片

在 Python 里, 列表(list),元祖(tuple)和字符串(str)这类序列类型都支持切片操作

为什么切片的区间会忽略最后一个元素

  • Python 以0 作为起始下标
  • 当只有后一个位置信息时,可以快速导出切片和区间的元素数量
  • 当起止位置信息课件是,可以快速计算出切片和区间的长度 (stop - start)
  • 可利用任意一个下标把序列分割成不重叠的两部分。my_list[:x] my_list[x:]
1
2
3
### 对对象进行切片

- 可以通过 s[a:b:c] 的形式对 s 在 a 和 b 区间以 c 为间隔取值
1
2
3
4
s = 'bicycle'
print(s[::3])
print(s[::-1])
print(s[::-2])
byeelcycibeccb

多维切片和省略

[] 运算符可以使用以逗号分开的多个索引或切片。

a[i, j]a[m:n, k:1]得到二维切片

要正确处理[] 运算符,对象的特殊方法 __getitem____setitem__ 需要以元祖的形式来接受 a[i, j]的索引。

给切片赋值

切片放在赋值语句左边,或作为 del 操作对象,可以对序列进行嫁接、切除或就地修改

1
2
3
4
5
6
7
8
9
10
11
12
13
l = list(range(10))
print(l)

l[2:5] = [20, 30]
print(l)

del l[5:7]
print(l)

l[3::2] = [11, 22]
print(l)

# l[2:5] = 100 WRONG
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9][0, 1, 20, 30, 5, 6, 7, 8, 9][0, 1, 20, 30, 5, 8, 9][0, 1, 20, 11, 5, 22, 9]

对序列使用 + 和 *

  • +* 不修改原有的操作对象,而是构建一个新的序列
1
2
3
4
l = [1, 2, 3]
print(l * 5)

print(5 * 'abcd')
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]abcdabcdabcdabcdabcd

建立由列表组成的列表

a * n,如果在序列 a 中存在对其他可变变量的引用的话,得到的序列中包含的是 n 个对指向同一地址的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
board = [['_'] * 3 for i in range(3)]
# 换一种形式
# board = []
# for i in range(3):
# row = ['_'] * 3
# board.append(row)
print(board)
board[1][2] = 'X'
print(board)


# weird_board = [['_'] * 3] * 3
# 换一种形式
weird_board = []
row = ['_'] * 3
for i in range(3):
weird_board.append(row)
weird_board[1][2] = 'O'
# 会发现 3 个指向同一列表的引用
print(weird_board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']][['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']][['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

序列的增量赋值 +=、*=

  • += 背后的特殊方法是 __iadd__ 方法,没有则退一步调用 __add__
  • 同理 *= 的特殊方法是 __imul__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
l = [1, 2, 3]
print(id(l))

l *= 2
print(l)
# 列表ID 无改变
print(id(l))

t = (1, 2, 3)
print(id(t))
t *= 2
print(t)
# 新元祖被创建
print(id(t))
4534358344[1, 2, 3, 1, 2, 3]45343583444536971408(1, 2, 3, 1, 2, 3)4546754024

list.sort方法和内置函数sorted

  • list.sort 会就地排序列表,方法返回值为 None
  • sorted 会新建一个列表作为返回值
  • 两个方法都有 reverse 和 key 作为可选的关键字参数
    reserve 为 True 时,降序输出。默认为 false
    key 只有一个参数的函数,将被用在序列的每一个元素上,其结果作为排序算法依赖的对比关键字

用bisect管理已排序的序列

bisect 模块有两个主要函数:

  • bisect
  • insort
    都利用二分查找法来在有序序列中查找或插入人元素

用 bisect 来搜索

bisect(haystack, needle) 默认为升序,haystack 需要保持有序。
使用方法:
bisect(index, needle) 查找位置 index,再使用 haystack.insert(index, needle) 插入新值

也可以用 insort 来一步到位,且后者速度更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# BEGIN BISECT_DEMO
import bisect
import sys

HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]

ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'

def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle) # <1>
offset = position * ' |' # <2>
print(ROW_FMT.format(needle, position, offset)) # <3>

if __name__ == '__main__':

if sys.argv[-1] == 'left': # <4>
bisect_fn = bisect.bisect_left
else:
bisect_fn = bisect.bisect

print('DEMO:', bisect_fn.__name__) # <5>
print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)

# END BISECT_DEMO
DEMO: bisect_righthaystack ->  1  4  5  6  8 12 15 20 21 23 23 26 29 3031 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  |3130 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  |3029 @ 13      |  |  |  |  |  |  |  |  |  |  |  |  |2923 @ 11      |  |  |  |  |  |  |  |  |  |  |2322 @  9      |  |  |  |  |  |  |  |  |2210 @  5      |  |  |  |  |10 8 @  5      |  |  |  |  |8  5 @  3      |  |  |5  2 @  1      |2  1 @  1      |1  0 @  0    0 

Array

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

array.tofilefromfile 可以将数组以二进制格式写入文件,速度要比写入文本文件快很多,文件的体积也小。

另外一个快速序列化数字类型的方法是使用 pickle(https://docs.python.org/3/library/pickle.html)模块。pickle.dump 处理浮点数组的速度几乎跟array.tofile 一样快。不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。

array 具有 type code 来表示数组类型:具体可见 array 文档.

memoryview

memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import array

arr = array.array('h', [1, 2, 3])
memv_arr = memoryview(arr)
# 把 signed short 的内存使用 char 来呈现
memv_char = memv_arr.cast('B')
print('Short', memv_arr.tolist())
print('Char', memv_char.tolist())
memv_char[1] = 2 # 更改 array 第一个数的高位字节
# 0x1000000001
print(memv_arr.tolist(), arr)
print('-' * 10)
bytestr = b'123'
# bytes 是不允许更改的
try:
bytestr[1] = '3'
except TypeError as e:
print(repr(e))
memv_byte = memoryview(bytestr)
print('Memv_byte', memv_byte.tolist())
# 同样这块内存也是只读的
try:
memv_byte[1] = 1
except TypeError as e:
print(repr(e))

Deque

collections.deque 是比 list 效率更高,且线程安全的双向队列实现。

除了 collections 以外,以下 Python 标准库也有对队列的实现:

  • queue.Queue (可用于线程间通信)
  • multiprocessing.Queue (可用于进程间通信)
  • asyncio.Queue
  • heapq

<流畅的Python> Python 数据类型

作者 Yuechuan Xiao
2019年12月11日 14:21

Guido 对语言设计美学的深入理解让人震惊。我认识不少很不错的编程语言设计者,他们设计出来的东西确实很精彩,但是从来都不会有用户。Guido 知道如何在理论上做出一定妥协,设计出来的语言让使用者觉得如沐春风,这真是不可多得。
——Jim Hugunin
Jython 的作者,AspectJ 的作者之一,.NET DLR 架构师

Python 最好的品质之一是一致性:你可以轻松理解 Python 语言,并通过 Python 的语言特性在类上定义规范的接口,来支持 Python 的核心语言特性,从而写出具有“Python 风格”的对象。

Python 解释器在碰到特殊的句法时,会使用特殊方法(我们称之为魔术方法)去激活一些基本的对象操作。

__getitem__ 以双下划线开头的特殊方法,称为 dunder-getitem。特殊方法也称为双下方法(dunder-method)

my_c[key] 语句执行时,就会调用 my_c.__getitem__ 函数。这些特殊方法名能让你自己的对象实现和支持一下的语言构架,并与之交互:

  • 迭代
  • 集合类
  • 属性访问
  • 运算符重载
  • 函数和方法的调用
  • 对象的创建和销毁
  • 字符串表示形式和格式化
  • 管理上下文(即 with 块)

实现一个 Pythonic 的牌组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 通过实现魔术方法,来让内置函数支持你的自定义对象
# https://github.com/fluentpython/example-code/blob/master/01-data-model/frenchdeck.py
import collections
import random

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
return self._cards[position]

deck = FrenchDeck()

可以容易地获得一个纸牌对象

1
2
beer_card = Card('7', 'diamonds')
print(beer_card)

和标准 Python 集合类型一样,使用 len() 查看一叠纸牌有多少张

1
2
3
deck = FrenchDeck()
# 实现 __len__ 以支持下标操作
print(len(deck))

可选取特定一张纸牌,这是由 __getitem__ 方法提供的

1
2
3
# 实现 __getitem__ 以支持下标操作
print(deck[1])
print(deck[5::13])

随机抽取一张纸牌,使用 python 内置函数 random.choice

1
2
3
from random import choice
# 可以多运行几次观察
choice(deck)

实现特殊方法的两个好处:

  • 对于标准操作有固定命名
  • 更方便利用 Python 标准库

__getitem__ 方法把 [] 操作交给了 self._cards 列表,deck 类自动支持切片操作

1
2
deck[12::13]
deck[:3]

同时 deck 类支持迭代

1
2
3
4
5
6
for card in deck:
print(card)

# 反向迭代
for card in reversed(deck):
print(card)

迭代通常是隐式的,如果一个集合没有实现 __contains__ 方法,那么 in 运算符会顺序做一次迭代搜索。

1
2
Card('Q', 'hearts') in deck 
Card('7', 'beasts') in deck
False

进行排序,排序规则:
2 最小,A最大。花色 黑桃 > 红桃 > 方块 > 梅花

1
card.rank
'A'
1
2
3
4
5
6
7
8
9
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)

return rank_value * len(suit_values) + suit_values[card.suit]

for card in sorted(deck, key=spades_high):
print(card)

FrenchDeck 继承了 object 类。通过 __len__, __getitem__ 方法,FrenchDeck和 Python 自有序列数据类型一样,可体现 Python 核心语言特性(如迭代和切片),

Python 支持的所有魔术方法,可以参见 Python 文档 Data Model 部分。

比较重要的一点:不要把 lenstr 等看成一个 Python 普通方法:由于这些操作的频繁程度非常高,所以 Python 对这些方法做了特殊的实现:它可以让 Python 的内置数据结构走后门以提高效率;但对于自定义的数据结构,又可以在对象上使用通用的接口来完成相应工作。但在代码编写者看来,len(deck)len([1,2,3]) 两个实现可能差之千里的操作,在 Python 语法层面上是高度一致的。

如何使用特殊方法

特殊方法的存在是为了被 Python 解释器调用
除非大量元编程,通常代码无需直接使用特殊方法
通过内置函数来使用特殊方法是最好的选择

模拟数值类型

实现一个二维向量(Vector)类

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from math import hypot

class Vector:

def __init__(self, x=0, y=0):
self.x = x
self.y = y

def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)

def __abs__(self):
return hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

# 使用 + 运算符
v1 = Vector(2, 4)
v2 = Vector(2, 1)
v1 + v2

# 调用 abs 内置函数
v = Vector(3, 4)
abs(v)

# 使用 * 运算符
v * 3

Vector 类中 6 个方法(除 __init__ 外)并不会在类自身的代码中调用。一般只有解释器会频繁调用这些方法

字符串的表示形式

内置函数 repr, 通过 __repr__ 特殊方法来得到一个对象的字符串表示形式。

算数运算符

通过 __add__, __mul__, 向量类能够操作 + 和 * 两个算数运算符。

运算符操作对象不发生改变,返回一个产生的新值

自定义的布尔值

  • 任何对象可用于需要布尔值的上下文中(if, while 语句, and, or, not 运算符)
  • Python 调用 bool(x) 判定一个值 x,bool(x) 只能返回 True 或者 False
  • 如果类没有实现 __bool__,则调用 __len__, 若返回 0,则 bool 返回 False

特殊方法一览

Reference

为何 len 不是普通方法

“实用胜于纯粹“

The Zen of Python

为了让 Python 自带的数据结构走后门, CPython 会直接从结构体读取对象的长度,而不会调用方法.
这种处理方式在保持内置类型的效率和语言一致性保持了一个平衡.

小结

  • 通过实现特殊方法,自定义数据类型可以像内置类型一样处理
  • 合理的字符串表示形式是Python对象的基本要求。__repr__, __str__
  • 序列类型的模拟是特殊方法最常用的地方

如何编写最佳的Dockerfile

作者 Yuechuan Xiao
2019年12月1日 16:29

译者按: Dockerfile 的语法非常简单,然而如何加快镜像构建速度,如何减少 Docker 镜像的大小却不是那么直观,需要积累实践经验。这篇博客可以帮助你快速掌握编写 Dockerfile 的技巧。

本文采用意译,版权归原作者所有


我已经使用 Docker 有一段时间了,其中编写 Dockerfile 是非常重要的一部分工作。在这篇博客中,我打算分享一些建议,帮助大家编写更好的 Dockerfile。

目标

  • 更快的构建速度
  • 更小的 Docker 镜像大小
  • 更少的 Docker 镜像层
  • 充分利用镜像缓存
  • 增加 Dockerfile 可读性
  • 让 Docker 容器使用起来更简单

总结

  • 编写.dockerignore 文件
  • 容器只运行单个应用
  • 将多个 RUN 指令合并为一个
  • 基础镜像的标签不要用 latest
  • 每个 RUN 指令后删除多余文件
  • 选择合适的基础镜像(alpine 版本最好)
  • 设置 WORKDIR 和 CMD
  • 使用 ENTRYPOINT (可选)
  • 在 entrypoint 脚本中使用 exec
  • COPY 与 ADD 优先使用前者
  • 合理调整 COPY 与 RUN 的顺序
  • 设置默认的环境变量,映射端口和数据卷
  • 使用 LABEL 设置镜像元数据
  • 添加 HEALTHCHECK

示例

示例 Dockerfile 犯了几乎所有的错(当然我是故意的)。接下来,我会一步步优化它。假设我们需要使用 Docker 运行一个 Node.js 应用,下面就是它的 Dockerfile(CMD 指令太复杂了,所以我简化了,它是错误的,仅供参考)。

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM ubuntu

ADD . /app

RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y nodejs ssh mysql
RUN cd /app && npm install

# this should start three processes, mysql and ssh
# in the background and node app in foreground
# isn't it beautifully terrible? <3
CMD mysql & sshd & npm start

构建镜像:

docker build -t wtf

编写.dockerignore 文件

构建镜像时,Docker 需要先准备context ,将所有需要的文件收集到进程中。默认的context包含 Dockerfile 目录中的所有文件,但是实际上,我们并不需要.git 目录,node_modules 目录等内容。 .dockerignore 的作用和语法类似于 .gitignore,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少 Docker 镜像的大小。示例如下:

1
2
3
.git/
node_modules/
dist/

容器只运行单个应用

从技术角度讲,你可以在 Docker 容器中运行多个进程。你可以将数据库,前端,后端,ssh,supervisor 都运行在同一个 Docker 容器中。但是,这会让你非常痛苦:

  • 非常长的构建时间(修改前端之后,整个后端也需要重新构建)
  • 非常大的镜像大小
  • 多个应用的日志难以处理(不能直接使用 stdout,否则多个应用的日志会混合到一起)
  • 横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同)
  • 僵尸进程问题 - 你需要选择合适的 init 进程

因此,我建议大家为每个应用构建单独的 Docker 镜像,然后使用 Docker Compose 运行多个 Docker 容器。

现在,我从 Dockerfile 中删除一些不需要的安装包,另外,SSH 可以用docker exec替代。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM ubuntu

ADD . /app

RUN apt-get update
RUN apt-get upgrade -y

# we should remove ssh and mysql, and use
# separate container for database
RUN apt-get install -y nodejs # ssh mysql
RUN cd /app && npm install

CMD npm start

将多个 RUN 指令合并为一个

Docker 镜像是分层的,下面这些知识点非常重要:

  • Dockerfile 中的每个指令都会创建一个新的镜像层。
  • 镜像层将被缓存和复用
  • 当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
  • 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
  • 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在 Docker 容器中不可见了)。

Docker 镜像类似于洋葱。它们都有很多层。为了修改内层,则需要将外面的层都删掉。记住这一点的话,其他内容就很好理解了。

现在,我们将所有的RUN指令合并为一个。同时把apt-get upgrade删除,因为它会使得镜像构建非常不确定(我们只需要依赖基础镜像的更新就好了)

1
2
3
4
5
6
7
8
9
10
FROM ubuntu

ADD . /app

RUN apt-get update \
&& apt-get install -y nodejs \
&& cd /app \
&& npm install

CMD npm start

记住一点,我们只能将变化频率一样的指令合并在一起。将 node.js 安装与 npm 模块安装放在一起的话,则每次修改源代码,都需要重新安装 node.js,这显然不合适。因此,正确的写法是这样的:

1
2
3
4
5
6
7
FROM ubuntu

RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install

CMD npm start

基础镜像的标签不要用 latest

当镜像没有指定标签时,将默认使用latest 标签。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。当时,当镜像更新时,latest 标签会指向不同的镜像,这时构建镜像有可能失败。如果你的确需要使用最新版的基础镜像,可以使用 latest 标签,否则的话,最好指定确定的镜像标签。

示例 Dockerfile 应该使用16.04作为标签。

1
2
3
4
5
6
7
FROM ubuntu:16.04  # it's that easy!

RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install

CMD npm start

每个 RUN 指令后删除多余文件

假设我们更新了 apt-get 源,下载,解压并安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。但是,运行应用时 Docker 镜像中并不需要这些文件。我们最好将它们删除,因为它会使 Docker 镜像变大。

示例 Dockerfile 中,我们可以删除/var/lib/apt/lists/目录中的文件(它们是由 apt-get update 生成的)。

1
2
3
4
5
6
7
8
9
10
11
FROM ubuntu:16.04

RUN apt-get update \
&& apt-get install -y nodejs \
# added lines
&& rm -rf /var/lib/apt/lists/*

ADD . /app
RUN cd /app && npm install

CMD npm start

选择合适的基础镜像(alpine 版本最好)

在示例中,我们选择了ubuntu作为基础镜像。但是我们只需要运行 node 程序,有必要使用一个通用的基础镜像吗?node镜像应该是更好的选择。

1
2
3
4
5
6
7
8
FROM node

ADD . /app
# we don't need to install node
# anymore and use apt-get
RUN cd /app && npm install

CMD npm start

更好的选择是 alpine 版本的node镜像。alpine 是一个极小化的 Linux 发行版,只有 4MB,这让它非常适合作为基础镜像。

1
2
3
4
5
6
FROM node:7-alpine

ADD . /app
RUN cd /app && npm install

CMD npm start

apk是 Alpine 的包管理工具。它与apt-get有些不同,但是非常容易上手。另外,它还有一些非常有用的特性,比如no-cache和 --virtual选项,它们都可以帮助我们减少镜像的大小。

设置 WORKDIR 和 CMD

WORKDIR指令可以设置默认目录,也就是运行RUN / CMD / ENTRYPOINT指令的地方。

CMD指令可以设置容器创建是执行的默认命令。另外,你应该讲命令写在一个数组中,数组中每个元素为命令的每个单词(参考官方文档)。

1
2
3
4
5
6
7
FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install

CMD ["npm", "start"]

使用 ENTRYPOINT (可选)

ENTRYPOINT指令并不是必须的,因为它会增加复杂度。ENTRYPOINT是一个脚本,它会默认执行,并且将指定的命令错误其参数。它通常用于构建可执行的 Docker 镜像。entrypoint.sh 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env sh
# $0 is a script name,
# $1, $2, $3 etc are passed arguments
# $1 is our command
CMD=$1

case "$CMD" in
"dev" )
npm install
export NODE_ENV=development
exec npm run dev
;;

"start" )
# we can modify files here, using ENV variables passed in
# "docker create" command. It can't be done during build process.
echo "db: $DATABASE_ADDRESS" >> /app/config.yml
export NODE_ENV=production
exec npm start
;;

* )
# Run custom command. Thanks to this line we can still use
# "docker run our_image /bin/bash" and it will work
exec $CMD ${@:2}
;;
esac

示例 Dockerfile:

1
2
3
4
5
6
7
8
FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

可以使用如下命令运行该镜像:

docker run our-app dev

docker run out-app start

docker run -ti out-app /bin/bash

在 entrypoint 脚本中使用 exec

在前文的 entrypoint 脚本中,我使用了exec命令运行 node 应用。不使用exec的话,我们则不能顺利地关闭容器,因为 SIGTERM 信号会被 bash 脚本进程吞没。exec命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。

COPY 与 ADD 优先使用前者

COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包(参考官方文档)。

1
2
3
4
5
6
7
8
9
FROM node:7-alpine

WORKDIR /app

COPY . /app
RUN npm install

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

合理调整 COPY 与 RUN 的顺序

我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。

示例中,源代码会经常变化,则每次构建镜像时都需要重新安装 NPM 模块,这显然不是我们希望看到的。因此我们可以先拷贝package.json,然后安装 NPM 模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装 NPM 模块。

1
2
3
4
5
6
7
8
9
10
FROM node:7-alpine

WORKDIR /app

COPY package.json /app
RUN npm install
COPY . /app

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

设置默认的环境变量,映射端口和数据卷

运行 Docker 容器时很可能需要一些环境变量。在 Dockerfile 设置默认的环境变量是一种很好的方式。另外,我们应该在 Dockerfile 中设置映射端口和数据卷。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM node:7-alpine

# env variables required during build
ENV PROJECT_DIR=/app

WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

# env variables that can change
# volume and port settings
# and defaults for our application
ENV MEDIA_DIR=/media \
NODE_ENV=production \
APP_PORT=3000

VOLUME $MEDIA_DIR
EXPOSE $APP_PORT

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

ENV指令指定的环境变量在容器中可以使用。如果你只是需要指定构建镜像时的变量,你可以使用ARG指令。

使用 LABEL 设置镜像元数据

使用LABEL指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的 Dockerfile 语法使用MAINTAINER指令指定镜像创建者,但是它已经被弃用了。有时,一些外部程序需要用到镜像的元数据,例如nvidia-docker需要用到com.nvidia.volumes.needed

示例如下:

1
2
3
FROM node:7-alpine
LABEL maintainer "jakub.skalecki@example.com"
...

添加 HEALTHCHECK

运行容器时,可以指定–restart always选项。这样的话,容器崩溃时,Docker 守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让 Docker 周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回 0,否则返回 1。对 HEALTHCHECK 感兴趣的话,可以参考这篇博客。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM node:7-alpine
LABEL maintainer "jakub.skalecki@example.com"

ENV PROJECT_DIR=/app
WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

ENV MEDIA_DIR=/media \
NODE_ENV=production \
APP_PORT=3000

VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

当请求失败时,curl —fail 命令返回非 0 状态。

对进一步了解的使用者

如果你想要了解更多,请参阅 STOPSIGNAL, ONBUILD, 和 SHELL 指令。还要提到在构建镜像中一个非常有用的指令 --no-cache (特别是在 CI 服务器上),以及--squash here).

以上,Have fun 😃

❌
❌