2018年2月

转自 知乎 本文作者達聞西

作为一名久经片场的老司机,早就想写一些探讨驾驶技术的文章。这篇就介绍利用生成式对抗网络(GAN)的两个基本驾驶技能:

1) 去除(爱情)动作片中的马赛克 2) 给(爱情)动作片中的女孩穿(tuo)衣服

生成式模型

上一篇《用GAN生成二维样本的小例子》中已经简单介绍了GAN,这篇再简要回顾一下生成式模型,算是补全一个来龙去脉。

生成模型就是能够产生指定分布数据的模型,常见的生成式模型一般都会有一个用于产生样本的简单分布。例如一个均匀分布,根据要生成分布的概率密度函数,进行建模,让均匀分布中的样本经过变换得到指定分布的样本,这就可以算是最简单的生成式模型。比如下面例子:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

图中左边是一个自定义的概率密度函数,右边是相应的1w个样本的直方图,自定义分布和生成这些样本的代码如下:
`from functools import partial

import numpy

from matplotlib import pyplot

Define a PDF

x_samples = numpy.arange(-3, 3.01, 0.01)

PDF = numpy.empty(x_samples.shape)

PDF[x_samples < 0] = numpy.round(x_samples[x_samples < 0] + 3.5) / 3

PDF[x_samples >= 0] = 0.5 numpy.cos(numpy.pi x_samples[x_samples >= 0]) + 0.5

PDF /= numpy.sum(PDF)

Calculate approximated CDF

CDF = numpy.empty(PDF.shape)

cumulated = 0

for i in range(CDF.shape[0]):

cumulated += PDF[i]

CDF[i] = cumulated

Generate samples

generate = partial(numpy.interp, xp=CDF, fp=x_samples)

u_rv = numpy.random.random(10000)

x = generate(u_rv)

Visualization

fig, (ax0, ax1) = pyplot.subplots(ncols=2, figsize=(9, 4))

ax0.plot(x_samples, PDF)

ax0.axis([-3.5, 3.5, 0, numpy.max(PDF)*1.1])

ax1.hist(x, 100)

pyplot.show()`
对于一些简单的情况,我们会假设已知有模型可以很好的对分布进行建模,缺少的只是合适的参数。这时候很自然只要根据观测到的样本,学习参数让当前观测到的样本下的似然函数最大,这就是最大似然估计(Maximum Likelihood Estimation):

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

MLE是一个最基本的思路,实践中用得很多的还有KL散度(Kullback–Leibler divergence),假设真实分布是P,采样分布是Q,则KL散度为:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

从公式也能看出来,KL散度描述的是两个分布的差异程度。换个角度来看,让产生的样本和原始分布接近,也就是要让这俩的差异减小,所以最小化KL散度就等同于MLE。从公式上来看的话,我们考虑把公式具体展开一下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

公式的第二项就是熵,先不管这项,用H(P)表示。接下来考虑一个小trick:从Q中抽样n个样本{x1, x2, ..., xn},来估算P(x)的经验值(empirical density function):

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

其中 δ() 是狄拉克 δ 函数,把这项替换到上面公式的P(x):

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

因为是离散的采样值,所以

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

中只有 x=xi 的时候狄拉克 δ 函数才为1,所以考虑 x=x时这项直接化为 1:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

第一项正是似然的负对数形式。

说了些公式似乎跑得有点远了,其实要表达还是那个简单的意思:通过减小两个分布的差异可以让一个分布逼近另一个分布。仔细想想,这正是GAN里面adversarial loss的做法。

很多情况下我们面临的是更为复杂的分布,比如上篇文章中的例子,又或是实际场景中更复杂的情况,比如生成不同人脸的图像。这时候,作为具有universal approximation性质的神经网络是一个看上去不错的选择[1]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

所以虽然GAN里面同时包含了生成网络和判别网络,但本质来说GAN的目的还是生成模型。从生成式模型的角度,Ian Goodfellow总结过一个和神经网络相关生成式方法的“家谱”[1]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

在这其中,当下最流行的就是 GAN 和 Variational AutoEncoder(VAE),两种方法的一个简明示意如下[3]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

本篇不打算展开讲什么是VAE,不过通过这个图,和名字中的autoencoder也大概能知道,VAE中生成的loss是基于重建误差的。而只基于重建误差的图像生成,都或多或少会有图像模糊的缺点,因为误差通常都是针对全局。比如基于MSE(Mean Squared Error)的方法用来生成超分辨率图像,容易出现下面的情况[4]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

在这个二维示意中,真实数据分布在一个U形的流形上,而MSE系的方法因为loss的形式往往会得到一个接近平均值所在的位置(蓝色框)。

GAN在这方面则完爆其他方法,因为目标分布在流形上。所以只要大概收敛了,就算生成的图像都看不出是个啥,清晰度常常是有保证的,而这正是去除女优身上马赛克的理想特性!
<h2>马赛克->清晰画面:超分辨率(Super Resolution)问题</h2>
说了好些铺垫,终于要进入正题了。首先明确,去马赛克其实是个图像超分辨率问题,也就是如何在低分辨率图像基础上得到更高分辨率的图像:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

视频中超分辨率实现的一个套路是通过不同帧的低分辨率画面猜测超分辨率的画面,有兴趣了解这个思想的朋友可以参考我之前的一个答案:如何通过多帧影像进行超分辨率重构

不过基于多帧影像的方法对于女优身上的马赛克并不是很适用,所以这篇要讲的是基于单帧图像的超分辨率方法。
<h2>SRGAN</h2>
说到基于GAN的超分辨率的方法,就不能不提到SRGAN[4]:《Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network》。这个工作的思路是:基于像素的MSE loss往往会得到大体正确,但是高频成分模糊的结果。所以只要重建低频成分的图像内容,然后靠GAN来补全高频的细节内容,就可以了:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

这个思路其实和最早基于深度网络的风格迁移的思路很像(有兴趣的读者可以参考我之前文章 瞎谈CNN:通过优化求解输入图像 的最后一部分),其中重建内容的content loss是原始图像和低分辨率图像在VGG网络中的各个ReLU层的激活值的差异:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

生成细节adversarial loss就是GAN用来判别是原始图还是生成图的loss:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

把这两种loss放一起,取个名叫perceptual loss。训练的网络结构如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

正是上篇文章中讲过的C-GAN,条件C就是低分辨率的图片。SRGAN生成的超分辨率图像虽然PSNR等和原图直接比较的传统量化指标并不是最好,但就视觉效果,尤其是细节上,胜过其他方法很多。比如下面是作者对比bicubic插值和基于ResNet特征重建的超分辨率的结果:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

可以看到虽然很多细节都和原始图片不一样,不过看上去很和谐,并且细节的丰富程度远胜于SRResNet。这些栩栩如生的细节,可以看作是GAN根据学习到的分布信息“联想”出来的。

对于更看重“看上去好看”的超分辨率应用,SRGAN显然是很合适的。当然对于一些更看重重建指标的应用,比如超分辨率恢复嫌疑犯面部细节,SRGAN就不可以了。
<h2>pix2pix</h2>
虽然专门用了一节讲SRGAN,但本文用的方法其实是pix2pix[5]。这项工作刚在arxiv上发布就引起了不小的关注,它巧妙的利用GAN的框架解决了通用的Image-to-Image translation的问题。举例来说,在不改变分辨率的情况下:把照片变成油画风格;把白天的照片变成晚上;用色块对图片进行分割或者倒过来;为黑白照片上色;…每个任务都有专门针对性的方法和相关研究,但其实总体来看,都是像素到像素的一种映射啊,其实可以看作是一个问题。这篇文章的巧妙,就在于提出了pix2pix的方法,一个框架,解决所有这些问题。方法的示意图如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

就是一个Conditional GAN,条件C是输入的图片。除了直接用C-GAN,这项工作还有两个改进:

1)利用U-Net结构生成细节更好的图片[6]

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

U-Net是德国Freiburg大学模式识别和图像处理组提出的一种全卷积结构。和常见的先降采样到低维度,再升采样到原始分辨率的编解码(Encoder-Decoder)结构的网络相比,U-Net的区别是加入skip-connection,对应的feature maps和decode之后的同样大小的feature maps按通道拼(concatenate)一起,用来保留不同分辨率下像素级的细节信息。U-Net对提升细节的效果非常明显,下面是pix2pix文中给出的一个效果对比:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

可以看到,各种不同尺度的信息都得到了很大程度的保留。

2)利用马尔科夫性的判别器(PatchGAN)

pix2pix和SRGAN的一个异曲同工的地方是都有用重建解决低频成分,用GAN解决高频成分的想法。在pix2pix中,这个思想主要体现在两个地方。一个是loss函数,加入了L1 loss用来让生成的图片和训练的目标图片尽量相似,而图像中高频的细节部分则交由GAN来处理:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

还有一个就是 PatchGAN,也就是具体的GAN中用来判别是否生成图的方法。PatchGAN的思想是,既然GAN只负责处理低频成分,那么判别器就没必要以一整张图作为输入,只需要对NxN的一个图像patch去进行判别就可以了。这也是为什么叫Markovian discriminator,因为在patch以外的部分认为和本patch互相独立。

具体实现的时候,作者使用的是一个NxN输入的全卷积小网络,最后一层每个像素过sigmoid输出为真的概率,然后用BCEloss计算得到最终loss。这样做的好处是因为输入的维度大大降低,所以参数量少,运算速度也比直接输入一张快,并且可以计算任意大小的图。作者对比了不同大小patch的结果,对于256x256的输入,patch大小在70x70的时候,从视觉上看结果就和直接把整张图片作为判别器输入没什么区别了:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服
<h2>生成带局部马赛克的训练数据</h2>
利用pix2pix,只要准备好无码和相应的有码图片就可以训练去马赛克的模型了,就是这么简单。那么问题是,如何生成有马赛克的图片?

有毅力的话,可以手动加马赛克,这样最为精准。这节介绍一个不那么准,但是比随机强的方法:利用分类模型的激活区域进行自动马赛克标注。

基本思想是利用一个可以识别需要打码图像的分类模型,提取出这个模型中对应类的CAM(Class Activation Map)[7],然后用马赛克遮住响应最高的区域即可。这里简单说一下什么是CAM,对于最后一层是全局池化(平均或最大都可以)的CNN结构,池化后的feature map相当于是做了个加权相加来计算最终的每个类别进入softmax之前的激活值。CAM的思路是,把这个权重在池化前的feature map上按像素加权相加,最后得到的单张的激活图就可以携带激活当前类别的一些位置信息,这相当于一种弱监督(classification-->localization):

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

上图是一个CAM的示意,用澳洲梗类别的CAM,放大到原图大小,可以看到小狗所在的区域大致是激活响应最高的区域。

那么就缺一个可以识别XXX图片的模型了,网上还恰好就有个现成的,yahoo于2016年发布的开源色情图片识别模型 Open NSFW(Not Safe For Work),链接如下:

http://t.cn/RceUCu0

CAM的实现并不难,结合Open NSFW自动打码的代码和使用放在了这里:

http://t.cn/Rop9Ak6

(成功打码的)效果差不多是下面这样子:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服
<h2>去除(爱情)动作片中的马赛克</h2>
这没什么好说的了,一行代码都不用改,只需要按照前面的步骤把数据准备好,然后按照pix2pix官方的使用方法训练就可以了:

Torch版pix2pix:

http://t.cn/RfoJxZF

pyTorch版pix2pix(Cycle-GAN二合一版):

http://t.cn/RXJHrUV

从D盘里随随便便找了几千张图片,用来执行了一下自动打码和pix2pix训练(默认参数),效果是下面这样:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

什么?你问说好给女优去马赛克呢?女优照片呢?

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

还是要说一下,在真人照片上的效果比蘑菇和花强。
<h2>对偶学习(Dual Learning)</h2>
去马赛克已经讲完了,接下来就是给女孩穿(tuo)衣服了,动手之前,还是先讲一下铺垫:对偶学习 和 Cycle-GAN

对偶学习是MSRA于2016年提出的一种用于机器翻译的增强学习方法[8],目的是解决海量数据配对标注的难题,个人觉得算是一种弱监督方法(不过看到大多数文献算作无监督)。以机器翻译为例,对偶学习基本思想如下图[9]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

左边的灰衣男只懂英语,右边的黑衣女只懂中文,现在的任务就是,要学习如何翻译英语到中文。对偶学习解决这个问题的思路是:给定一个模型 f :x-y 刚开始无法知道f翻译得是否正确,但是如果考虑上 f 的对偶问题 g:y-x,那么我可以尝试翻译一个英文句子到中文,再翻译回来。这种转了一圈的结果 x'=g(f(x)),灰衣男是可以用一个标准(BLEU)判断 x' 和 x 是否一个意思,并且把结果的一致性反馈给这两个模型进行改进。同样的,从中文取个句子,这样循环翻译一遍,两个模型又能从黑衣女那里获取反馈并改进模型。其实这就是强化学习的过程,每次翻译就是一个action,每个action会从环境(灰衣男或黑衣女)中获取reward,对模型进行改进,直至收敛。

也许有的人看到这里会觉得和上世纪提出的Co-training很像,这个在知乎上也有讨论:

如何理解刘铁岩老师团队在NIPS 2016上提出的对偶学习(Dual Learning)?

个人觉得还是不一样的,Co-Training是一种multi-view方法,比如一个输入x,如果看作是两个拼一起的特征x=(x1, x2),并且假设 x和 x互相独立,那么这时候训练两个分类器 f1() 和 f2() 对于任意样本 x 应该有f1(x1)=f2(x2)。这对没有标注的样本是很有用的,相当于利用了同一个样本分类结果就应该一样的隐含约束。所以Co-Training的典型场景是少量标注+大量未标注的半监督场景。并且f1和f2其实是两个不同,但是domain指向相同的任务。而Dual Learning中 f 和 g 是对偶任务,利用的隐含约束是 x-y-x 的cycle consistency。对输入的特征也没有像Co-Training有那么明确的假设,学习方法上也不一样,Dual Learning算是强化学习。
<h2>CycleGAN和未配对图像翻译(Unpaired Image-to-Image Translation)</h2>
CycleGAN,翻译过来就是:轮着干,是结合了对偶学习和GAN一个很直接而巧妙的想法[10],示意图如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

X和Y分别是两种不同类型图的集合,比如穿衣服的女优和没穿衣服的女优。所以给定一张穿了衣服的女优,要变成没穿衣服的样子,就是个图片翻译问题。CycleGAN示意图中(b)和(c)就是Dual Learning:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

在Dual Learning基础上,又加入了两个判别器D_X和D_Y用来进行对抗训练,让翻译过来的图片尽量逼近当前集合中的图片:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

全考虑一起,最终的loss是:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

也许有人会问,那不加cycle-consistency,直接用GAN学习一个Xrightarrow Y的映射,让生成的Y的样本尽量毕竟Y里本身的样本可不可以呢?这个作者在文中也讨论了,会产生GAN训练中容易发生的mode collapse问题。mode collapse问题的一个简单示意如下[1]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

上边的是真实分布,下边的是学习到的分布,可以看到学习到的分布只是完整分布的一部分,这个叫做partial mode collapse,是训练不收敛情况中常见的一种。如果是完全的mode collapse,就是说生成模型得到的都是几乎一样的输出。而加入Cycle-consistency会让一个domain里不同的样本都尽量映射到另一个domain里不同的地方,理想情况就是双射(bijection)。直观来理解,如果通过Xrightarrow Y都映射在Y中同一个点,那么这个点y通过Yrightarrow X映射回来显然不可能是多个不同的x,所以加入cycle-consistency就帮助避免了mode collapse。这个问题在另一篇和CycleGAN其实本质上没什么不同的方法DiscoGAN中有更详细的讨论[11],有兴趣的话可以参考。

有一点值得注意的是,虽然名字叫CycleGAN,并且套路也和C-GAN很像,但是其实只有adversarial,并没有generative。因为严格来说只是学习了Xrightarrow Y和Yrightarrow X的mapping,所谓的generative network里并没有随机性。有一个和CycleGAN以及DiscoGAN其实本质上也没什么不同的方法叫DualGAN[12],倒是通过dropout把随机性加上了。不过所有加了随机性产生的样本和原始样本间的cycle-consistency用的还是l1 loss,总觉得这样不是很对劲。当然现在GAN这么热门,其实只要是用了adversarial loss的基本都会取个名字叫XXGAN,也许是可以增加投稿命中率。

另外上节中提到了Co-Training,感觉这里也应该提一下CoGAN[13],因为名字有些相似,并且也可以用于未配对的图像翻译。CoGAN的大体思想是:如果两个Domain之间可以互相映射,那么一定有一些特征是共有的。比如男人和女人,虽然普遍可以从长相区分,但不变的是都有两个眼睛一个鼻子一张嘴等等。所以可以在生成的时候,把生成共有特征和各自特征的部分分开,示意图如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

其实就是两个GAN结构,其中生成网络和判别网络中比较高层的部分都采用了权值共享(虚线相连的部分),没有全职共享的部分分别处理不同的domain。这样每次就可以根据训练的domain生成一个样本在两个domain中不同的对应,比如戴眼镜和没戴眼镜:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

分别有了共有特征和各自domain特征,那么做mapping的思路也就很直接了[14]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

在GAN前边加了个domain encoder,然后对每个domain能得到三种样本给判别器区分:直接采样,重建采样,从另一个domain中transfer后的重建采样。训练好之后,用一个domain的encoder+另一个domain的generator就很自然的实现了不同domain的转换。用在图像翻译上的效果如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

还有个巧妙的思路,是把CoGAN拆开,不同domain作为C-GAN条件的更加显式的做法[15]:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

第一步用噪声Z作为和domain无关的共享表征对应的latent noise,domain信息作为条件C训练一个C-GAN。第二步,训练一个encoder,利用和常见的encode-decode结构相反的decode(generate)-encode结构。学习好的encoder可以结合domain信息,把输入图像中和domain无关的共享特征提取出来。第三步,把前两步训练好的encoder和decoder(generator)连一起,就可以根据domain进行图像翻译了。

CoGAN一系的方法虽然结构看起来更复杂,但个人感觉理解起来要比dual系的方法更直接,并且有latent space,可解释性和属性对应也好一些。

又扯远了,还是回到正题:
<h2>给女优穿上衣服</h2>
其实同样没什么好说的,Cycle-GAN和pix2pix的作者是一拨人,文档都写得非常棒,准备好数据,分成穿衣服的和没穿衣服的两组,按照文档的步骤训练就可以:

Torch版Cycle-GAN:

http://t.cn/R6pEHV5

pyTorch版Cycle-GAN(pix2pix二合一版):

http://t.cn/RXJHrUV

Cycle-GAN收敛不易,我用了128x128分辨率训练了各穿衣服和没穿衣服的女优各一千多张,同样是默认参数训练了120个epoch,最后小部分成功“穿衣服”的结果如下:

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

虽然都有些突兀,但好歹是穿上衣服了。注意马赛克不是图片里就有的,是我后来加上去的。

那么,脱衣服的例子在哪里?

提高驾驶技术:用GAN去除(爱情)动作片中的马赛克和衣服

参考文献

[1] I. Goodfellow. Nips 2016 tutorial: Generative adversarial networks. arXiv preprint arXiv:1701.00160, 2016.

[2] A. B. L. Larsen, S. K. S?nderby, Generating Faces with Torch. Torch | Generating Faces with Torch

[3] A. B. L. Larsen, S. K. S?nderby, H. Larochelle, and O. Winther. Autoencoding beyond pixels using a learned similarity metric. In ICML, pages 1558–1566, 2016.

[4] C. Ledig, L. Theis, F. Huszar, J. Caballero, A. Aitken, A. Tejani, J. Totz, Z. Wang, and W. Shi. Photo-realistic single image super-resolution using a generative adversarial network. arXiv:1609.04802, 2016.

[5] P. Isola, J.-Y. Zhu, T. Zhou, and A. A. Efros. Image-to-image translation with conditional adversarial networks. arxiv, 2016.

[6] O. Ronneberger, P. Fischer, and T. Brox. U-net: Convolutional networks for biomedical image segmentation. In MICCAI, pages 234–241. Springer, 2015.

[7] B. Zhou, A. Khosla, A. Lapedriza, A. Oliva, and A. Torralba. Learning deep features for discriminative localization. arXiv preprint arXiv:1512.04150, 2015.

[8] He, D., Xia, Y., Qin, T., Wang, L., Yu, N., Liu, T.-Y., and Ma, W.-Y. (2016a). Dual learning for machine translation. In the Annual Conference on Neural Information Processing Systems (NIPS), 2016.

[9] Tie-Yan Liu, Dual Learning: Pushing the New Frontier of Artificial Intelligence, MIFS 2016

[10] J.-Y. Zhu, T. Park, P. Isola, and A. A. Efros. Unpaired image-to-image translation using cycle-consistent adversarial networkss. arXiv preprint arXiv:1703.10593, 2017.

[11] T. Kim, M. Cha, H. Kim, J. Lee, and J. Kim. Learning to Discover Cross-Domain Relations with Generative Adversarial Networks. ArXiv e-prints, Mar. 2017.

[12] Z. Yi, H. Zhang, P. T. Gong, et al. DualGAN: Unsupervised dual learning for image-to-image translation. arXiv preprint arXiv:1704.02510, 2017.

[13] M.-Y. Liu and O. Tuzel. Coupled generative adversarial networks. In Advances in Neural Information Processing Systems (NIPS), 2016.

[14] M.-Y. Liu, T. Breuel, and J. Kautz. Unsupervised image-to-image translation networks. arXiv preprint arXiv:1703.00848, 2017.

[15] Dong, H., Neekhara, P., Wu, C., Guo, Y.: Unsupervised image-to-image translation with generative adversarial networks. arXiv preprint arXiv:1701.02676, 2017.

安装和启用
遵循标准的Flask扩展安装和启用方式,先通过pip来安装扩展:

$ pip install Flask-Login
接下来创建扩展对象实例:

from flask import Flask
from flask.ext.login import LoginManager

app = Flask(__name__)
login_manager = LoginManager(app)
同时,你可以对LoginManager对象赋上配置参数:

# 设置登录视图的名称,如果一个未登录用户请求一个只有登录用户才能访问的视图,
# 则闪现一条错误消息,并重定向到这里设置的登录视图。
# 如果未设置登录视图,则直接返回401错误。
login_manager.login_view = 'login'
# 设置当未登录用户请求一个只有登录用户才能访问的视图时,闪现的错误消息的内容,
# 默认的错误消息是:Please log in to access this page.。
login_manager.login_message = 'Unauthorized User'
# 设置闪现的错误消息的类别
login_manager.login_message_category = "info"
编写用户类
使用Flask-Login之前,你需要先定义用户类,该类必须实现以下三个属性和一个方法:

属性 is_authenticated
当用户登录成功后,该属性为True。

属性 is_active
如果该用户账号已被激活,且该用户已登录成功,则此属性为True。

属性 is_anonymous
是否为匿名用户(未登录用户)。

方法 get_id()
每个用户都必须有一个唯一的标识符作为ID,该方法可以返回当前用户的ID,这里ID必须是Unicode。

因为每次写个用户类很麻烦,Flask-Login提供了”UserMixin”类,你可以直接继承它即可:

from flask.ext.login import UserMixin

class User(UserMixin):
pass
从会话或请求中加载用户
在编写登录登出视图前,我们要先写一个加载用户对象的方法。它的功能是根据传入的用户ID,构造一个新的用户类的对象。为了简化范例,我们不引入数据库,而是在列表里定义用户记录。

# 用户记录表
users = [
{'username': 'Tom', 'password': '111111'},
{'username': 'Michael', 'password': '123456'}
]

# 通过用户名,获取用户记录,如果不存在,则返回None
def query_user(username):
for user in users:
if user['username'] == username:
return user

# 如果用户名存在则构建一个新的用户类对象,并使用用户名作为ID
# 如果不存在,必须返回None
@login_manager.user_loader
def load_user(username):
if query_user(username) is not None:
curr_user = User()
curr_user.id = username
return curr_user
上述代码中,通过”@login_manager.user_loader”装饰器修饰的方法,既是我们要实现的加载用户对象方法。它是一个回调函数,在每次请求过来后,Flask-Login都会从Session中寻找”user_id”的值,如果找到的话,就会用这个”user_id”值来调用此回调函数,并构建一个用户类对象。因此,没有这个回调的话,Flask-Login将无法工作。

有一个问题,启用Session的话一定需要客户端允许Cookie,因为Session ID是保存在Cookie中的,如果Cookie被禁用了怎么办?那我们的应用只好通过请求参数将用户信息带过来,一般情况下会使用一个动态的Token来表示登录用户的信息。此时,我们就不能依靠”@login_manager.user_loader”回调,而是使用”@login_manager.request_loader”回调。

from flask import request

# 从请求参数中获取Token,如果Token所对应的用户存在则构建一个新的用户类对象
# 并使用用户名作为ID,如果不存在,必须返回None
@login_manager.request_loader
def load_user_from_request(request):
username = request.args.get('token')
if query_user(username) is not None:
curr_user = User()
curr_user.id = username
return curr_user
为了简化代码,上面的例子就直接使用用户名作为Token了,实际项目中,大家还是要用一个复杂的算法来验证Token。

登录及登出
一切准备就绪,我们开始实现登录视图:

from flask import render_template, redirect, url_for, flash
from flask.ext.login import login_user

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
user = query_user(username)
# 验证表单中提交的用户名和密码
if user is not None and request.form['password'] == user['password']:
curr_user = User()
curr_user.id = username

# 通过Flask-Login的login_user方法登录用户
login_user(curr_user)

# 如果请求中有next参数,则重定向到其指定的地址,
# 没有next参数,则重定向到"index"视图
next = request.args.get('next')
return redirect(next or url_for('index'))

flash('Wrong username or password!')
# GET 请求
return render_template('login.html')
上述代码同之前Login视图最大的不同就是你在用户验证通过后,需要调用Flask-Login扩展提供的”login_user()”方法来让用户登录,该方法需传入用户类对象。这个”login_user()”方法会帮助你操作用户Session,并且会在请求上下文中记录用户信息。另外,在具体实现时,建议大家对”next”参数值作验证,避免被URL注入攻击。

“login.html”模板很简单,就是显示一个用户名密码的表单:

&lt;!doctype html&gt;
Login Sample
<h1>Login</h1>
{% with messages = get_flashed_messages() %}
<div>{{ messages[0] }}</div>
{% endwith %}

<form action="{{ url_for('login') }}" method="POST"><input id="username" name="username" type="text" placeholder="Username" />
<input id="password" name="password" type="password" placeholder="Password" />
<input name="submit" type="submit" /></form>接下来,让我们写个index视图:

from flask.ext.login import current_user, login_required

@app.route('/')
@login_required
def index():
return 'Logged in as: %s' % current_user.get_id()
装饰器”@login_required”就如同我们在进阶系列第四篇中写的一样,确保只有登录用户才能访问这个index视图,Flask-Login帮我们实现了这个装饰器。如果用户未登录,它就会将页面重定向到登录视图,也就是我们在第一节中配置的”login_manager.login_view”的视图。

同时,重定向的地址会自动加上”next”参数,参数的值是当前用户请求的地址,这样,登录成功后就会跳转回当前视图。可以看到我们对于用户登录所需要的操作,这个装饰器基本都实现了,很方便吧!

Flask-Login还提供了”current_user”代理,可以访问到登录用户的用户类对象。我们在模板中也可以使用这个代理。让我们再写一个home视图:

@app.route('/home')
@login_required
def home():
return render_template('hello.html')
模板代码如下:

&lt;!doctype html&gt;
Login Sample
{% if current_user.is_authenticated %}
<h1>Hello {{ current_user.get_id() }}!</h1>
{% endif %}
在上面的模板代码中,我们直接访问了”current_user”对象的属性和方法。

登出视图也很简单,Flask-Login提供了”logout_user()”方法来帮助你清理用户Session。

from flask.ext.login import logout_user

@app.route('/logout')
@login_required
def logout():
logout_user()
return 'Logged out successfully!'
自定义未授权访问的处理方法
“@login_required”装饰器对于未登录用户访问的默认处理是重定向到登录视图,如果我们不想它这么做的话,可以自定义处理方法:

@login_manager.unauthorized_handler
def unauthorized_handler():
return 'Unauthorized'
这个”@login_manager.unauthorized_handler”装饰器所修饰的方法就会代替”@login_required”装饰器的默认处理方法。有了上面的代码,当未登录用户访问index视图时,页面就会直接返回”Unauthorized”信息。

Remember Me
在登录视图中,调用”login_user()”方法时,传入”remember=True”参数,即可实现“记住我”功能:

...
login_user(curr_user, remember=True)
...
Flask-Login是通过在Cookie实现的,它会在Cookie中添加一个”remember_token”字段来记住之前登录的用户信息,所以禁用Cookie的话,该功能将无法工作。

Fresh登录
当用户通过账号和密码登录后,Flask-Login会将其标识为Fresh登录,即在Session中设置”_fresh”字段为True。而用户通过Remember Me自动登录的话,则不标识为Fresh登录。对于”@login_required”装饰器修饰的视图,是否Fresh登录都可以访问,但是有些情况下,我们会强制要求用户登录一次,比如修改登录密码,这时候,我们可以用”@fresh_login_required”装饰器来修饰该视图。这样,通过Remember Me自动登录的用户,将无法访问该视图:

from flask.ext.login import fresh_login_required

@app.route('/home')
@fresh_login_required
def home():
return 'Logged in as: %s' % current_user.get_id()
会话保护
Flask-Login自动启用会话保护功能。对于每个请求,它会验证用户标识,这个标识是由客户端IP地址和User Agent的值经SHA512编码而来。在用户登录成功时,Flask-Login就会将这个值保存起来以便后续检查。默认的会话保护模式是”basic”,为了加强安全性,你可以启用强会话保护模式,方法是配置LoginManager实例对象中的”session_protection”属性:

login_manager.session_protection = "strong"
在”strong”模式下,一旦用户标识检查失败,便会清空所用Session内容,并且Remember Me也失效。而”basic”模式下,只是将登录标为非Fresh登录。你还可以将”login_manager.session_protection”置为None来取消会话保护。

用户认证的原理
在了解使用Flask来实现用户认证之前,我们首先要明白用户认证的原理。假设现在我们要自己去实现用户认证,需要做哪些事情呢?

首先,用户要能够输入用户名和密码,所以需要网页和表单,用以实现用户输入和提交的过程。
用户提交了用户名和密码,我们就需要比对用户名,密码是否正确,而要想比对,首先我们的系统中就要有存储用户名,密码的地方,大多数后台系统会通过数据库来存储,但是实际上我们也可以简单的存储到文件当中。(为简明起见,本文将用户信息存储到json文件当中)
登录之后,我们需要维持用户登录状态,以便用户在访问特定网页的时候来判断用户是否已经登录,以及是否有权限访问改网页。这就需要有维护一个会话来保存用户的登录状态和用户信息。
从第三步我们也可以看出,如果我们的网页需要权限保护,那么当请求到来的时候,我们就首先要检查用户的信息,比如是否已经登录,是否有权限等,如果检查通过,那么在response的时候就会将相应网页回复给请求的用户,但是如果检查不通过,那么就需要返回错误信息。
在第二步,我们知道要将用户名和密码存储起来,但是如果只是简单的用明文存储用户名和密码,很容易被“有心人”盗取,从而造成用户信息泄露,那么我们实际上应当将用户信息尤其是密码做加密处理之后再存储比较安全。
用户登出
通过Flask以及相应的插件来实现登录过程
接下来讲述如何通过Flask框架以及相应的插件来实现整个登录过程,需要用到的插件如下:

flask-wtf
wtf
werkzeug
flask_login
使用flask-wtf和wtf来实现表单功能
flask-wtf对wtf做了一些封装,不过有些东西还是要直接用wtf,比如StringField等。flask-wtf和wtf主要是用于建立html中的元素和Python中的类的对应关系,通过在Python代码中操作对应的类,对象等从而控制html中的元素。我们需要在python代码中使用flask-wtf和wtf来定义前端页面的表单(实际是定义一个表单类),再将对应的表单对象作为render_template函数的参数,传递给相应的template,之后Jinja模板引擎会将相应的template渲染成html文本,再作为http response返回给用户。

定义表单类示例代码:

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, PasswordField
from wtforms.validators import DataRequired

# 定义的表单都需要继承自FlaskForm
class LoginForm(FlaskForm):
# 域初始化时,第一个参数是设置label属性的
username = StringField('User Name', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('remember me', default=False)
在wtf当中,每个域代表就是html中的元素,比如StringField代表的是元素,当然wtf的域还定义了一些特定功能,比如validators,可以通过validators来对这个域的数据做检查,详细请参考wtf教程。 对应的html模板可能如下login.html:

{% extends "layout.html" %}

Login Page

User Name:

Password:

Remember Me

{{ form.csrf_token }}

这里{{ form.csrf_token }}也可以使用{{ form.hidden_tag() }}来替换

同时我们也可以使用form去定义模板,跟直接用html标签去定义效果是相同的,Jinja模板引擎会将对象、属性转化为对应的html标签, 相对应的template,如下login.html:



{% extends "base.html" %}

{% block content %}

Sign In

{{ form.csrf_token }}

{{ form.username.label }}
{{ form.username(size=80) }}

{{ form.password.label }}

{{ form.password(size=80) }}

{{ form.remember_me }} Remember Me

{% endblock %}
现在我们需要在view中定义相应的路由,并将相应的登录界面展示给用户。 简单起见,将view的相关路由定义放在主程序当中

# app.py
@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title="Sign In", form=form)
这里简单起见,当用户请求'/login'路由时,直接返回login.html网页,注意这里的html网页是经过Jinja模板引擎将相应的模板转换后的html网页。 至此,如果我们把以上代码整合到flask当中,就应该能够看到相应的登录界面了,那么当用户提交之后,我们应当怎样存储呢?这里我们暂时先不用数据库这样复杂的工具存储,先简单地存为文件。接下来就看下如何去存储。

加密和存储
我们可以首先定义一个User类,用于处理与用户相关的操作,包括存储和验证等。

# models.py

from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import json
import uuid

# define profile.json constant, the file is used to
# save user name and password_hash
PROFILE_FILE = "profiles.json"

class User(UserMixin):
def __init__(self, username):
self.username = username
self.password_hash = self.get_password_hash()
self.id = self.get_id()

@property
def password(self):
raise AttributeError('password is not a readable attribute')

@password.setter
def password(self, password):
"""save user name, id and password hash to json file"""
self.password_hash = generate_password_hash(password)
with open(PROFILE_FILE, 'w+') as f:
try:
profiles = json.load(f)
except ValueError:
profiles = {}
profiles[self.username] = [self.password_hash,
self.id]
f.write(json.dumps(profiles))

def verify_password(self, password):
if self.password_hash is None:
return False
return check_password_hash(self.password_hash, password)

def get_password_hash(self):
"""try to get password hash from file.

:return password_hash: if the there is corresponding user in
the file, return password hash.
None: if there is no corresponding user, return None.
"""
try:
with open(PROFILE_FILE) as f:
user_profiles = json.load(f)
user_info = user_profiles.get(self.username, None)
if user_info is not None:
return user_info[0]
except IOError:
return None
except ValueError:
return None
return None

def get_id(self):
"""get user id from profile file, if not exist, it will
generate a uuid for the user.
"""
if self.username is not None:
try:
with open(PROFILE_FILE) as f:
user_profiles = json.load(f)
if self.username in user_profiles:
return user_profiles[self.username][1]
except IOError:
pass
except ValueError:
pass
return unicode(uuid.uuid4())

@staticmethod
def get(user_id):
"""try to return user_id corresponding User object.
This method is used by load_user callback function
"""
if not user_id:
return None
try:
with open(PROFILE_FILE) as f:
user_profiles = json.load(f)
for user_name, profile in user_profiles.iteritems():
if profile[1] == user_id:
return User(user_name)
except:
return None
return None
User类需要继承flask-login中的UserMixin类,用于实现相应的用户会话管理。
这里我们是直接存储用户信息到一个json文件"profiles.json"
我们并不直接存储密码,而是存储加密后的hash值,在这里我们使用了werkzeug.security包中的generate_password_hash函数来进行加密,由于此函数默认使用了sha1算法,并添加了长度为8的盐值,所以还是相当安全的。一般用途的话也就够用了。
验证password的时候,我们需要使用werkzeug.security包中的check_password_hash函数来验证密码
get_id是UserMixin类中就有的method,在这我们需要overwrite这个method。在json文件中没有对应的user id时,可以使用uuid.uuid4()生成一个用户唯一id
至此,我们就实现了第二步和第五步,接下来要看第三步,如何去维护一个session

维护用户session
先看下代码,这里把相应代码也放入到app.py当中

from forms import LoginForm
from flask_wtf.csrf import CsrfProtect
from model import User
from flask_login import login_user, login_required
from flask_login import LoginManager, current_user
from flask_login import logout_user

app = Flask(__name__)

app.secret_key = os.urandom(24)

# use login manager to manage session
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'login'
login_manager.init_app(app=app)

# 这个callback函数用于reload User object,根据session中存储的user id
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)

# csrf protection
csrf = CsrfProtect()
csrf.init_app(app)

@app.route('/login')
def login():
form = LoginForm()
if form.validate_on_submit():
user_name = request.form.get('username', None)
password = request.form.get('password', None)
remember_me = request.form.get('remember_me', False)
user = User(user_name, password)
if user.verify_password(password):
login_user(user)
return redirect(request.args.get('next') or url_for('main'))
return render_template('login.html', title="Sign In", form=form)
维护用户的会话,关键就在这个LoginManager对象。
必须实现这个load_user callback函数,用以reload user object
当密码验证通过后,使用login_user()函数来登录用户,这时用户在会话中的状态就是登录状态了
受保护网页
保护特定网页,只需要对特定路由加一个装饰器就可以,如下

# app.py

# ...
@app.route('/')
@app.route('/main')
@login_required
def main():
return render_template(
'main.html', username=current_user.username)
# ...
current_user保存的就是当前用户的信息,实质上是一个User对象,所以我们直接调用其属性, 例如这里我们要给模板传一个username的参数,就可以直接用current_user.username
使用@login_required来标识改路由需要登录用户,非登录用户会被重定向到'/login'路由(这个就是由login_manager.login_view = 'login' 语句来指定的)
用户登出
# app.py

# ...
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
# ...
至此,我们就实现了一个完整的登陆和登出的过程。

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

没有__init__()的无状态对象
下面这个示例,是一个简化去掉了__init__()的类。这是一个常见的Strategy设计模式对象。策略对象插入到主对象来实现一种算法或者决策。它可能依赖主对象的数据,策略对象自身可能没有任何数据。我们经常设计策略类来遵循Flyweight设计模式:我们避免在Strategy对象内部进行存储。所有提供给Strategy的值都是作为方法的参数值。Strategy对象本身可以是无状态的。这更多是为了方法函数的集合而非其他。

在本例中,我们为Player实例提供了游戏策略。下面是一个抓牌和减少其他赌注的策略示例(比较笨的策略):

class GameStrategy:
def insurance(self, hand):
return False
def split(self, hand):
return False
def double(self, hand):
return False
def hit(self, hand):
return sum(c.hard for c in hand.cards) <= 17
每个方法都需要当前的Hand作为参数值。决策是基于可用信息的,也就是指庄家的牌和闲家的牌。

我们可以使用不同的Player实例来构建单个策略实例,如下面代码片段所示:

dumb = GameStrategy()
我们可以想象创造一组相关的策略类,在21点中玩家可以针对各种决策使用不同的规则。

一些额外的类定义
如前所述,一个玩家有两个策略:一个用于下注,一个用于出牌。每个Player实例都与模拟计算执行器有一序列的交互。我们称计算执行器为Table类。

Table类需要Player实例提供以下事件:

玩家必须基于下注策略来设置初始赌注。

玩家将得到一手牌。

如果手牌是可分离的,玩家必须决定是分离或不基于出牌策略。这可以创建额外的Hand实例。在一些赌场,额外的一手牌也是可分离的。

对于每个Hand实例,玩家必须基于出牌策略来决定是要牌、加倍或停牌。

玩家会获得奖金,然后基于输赢情况调整下注策略。

从这,我们可以看到Table类有许多API方法来获得赌注,创建Hand对象提供分裂、分解每一手牌、付清赌注。这个对象跟踪了一组Players的出牌状态。

以下是处理赌注和牌的Table类:

class Table:
def __init__(self):
self.deck = Deck()
def place_bet(self, amount):
print("Bet", amount)
def get_hand(self):
try:
self.hand = Hand2(d.pop(), d.pop(), d.pop())
self.hole_card = d.pop()
except IndexError:
# Out of cards: need to shuffle.
self.deck = Deck()
return self.get_hand()
print("Deal", self.hand)
return self.hand
def can_insure(self, hand):
return hand.dealer_card.insure
Player使用Table类来接收赌注,创建一个Hand对象,出牌时根据这手牌来决定是否买保险。使用额外方法去获取牌并决定偿还。

在get_hand()中展示的异常处理不是一个精确的赌场玩牌模型。这可能会导致微小的统计误差。更精确的模拟需要编写一副牌,当空的时候可以重新洗牌,而不是抛出异常。

为了正确地交互和模拟现实出牌,Player类需要一个下注策略。下注策略是一个有状态的对象,决定了初始赌注。各种下注策略调整赌注通常都是基于游戏的输赢。

理想情况下,我们渴望有一组下注策略对象。Python的装饰器模块允许我们创建一个抽象超类。一个非正式的方法创建策略对象引发的异常必须由子类实现。

我们定义了一个抽象超类,此外还有一个具体子类定义了固定下注策略,如下所示:

class BettingStrategy:
def bet(self):
raise NotImplementedError("No bet method")
def record_win(self):
pass
def record_loss(self):
pass

class Flat(BettingStrategy):
def bet(self):
return 1
超类定义了带有默认值的方法。抽象超类中的基本bet()方法抛出异常。子类必须覆盖bet()方法。其他方法可以提供默认值。这里给上一节的游戏策略添加了下注策略,我们可以看看Player类周围更复杂的__init__()方法。

我们可以利用abc模块正式化抽象超类的定义。就像下面的代码片段那样:

import abc
class BettingStrategy2(metaclass=abc.ABCMeta):
@abstractmethod
def bet(self):
return 1
def record_win(self):
pass
def record_loss(self):
 pass
这样做的优势在于创建了BettingStrategy2的实例,不会造成任何子类bet()的失败。如果我们试图通过未实现的抽象方法来创建这个类的实例,它将引发一个异常来替代创建对象。

是的,抽象方法有一个实现。它可以通过super().bet()来访问。

多策略的__init__()
我们可从各种来源创建对象。例如,我们可能需要复制一个对象作为创建备份或冻结一个对象的一部分,以便它可以作为字典的键或被置入集合中;这是内置类set和frozenset背后的想法。

有几个总体设计模式,它们有多种方法来构建一个对象。一个设计模式就是一个复杂的__init__(),称为多策略初始化。同时,有多个类级别的(静态)构造函数的方法。

这些都是不兼容的方法。他们有完全不同的接口。

避免克隆方法

在Python中,一个克隆方法没必要复制一个不需要的对象。使用克隆技术表明可能是未能理解Python中的面向对象设计原则。

克隆方法封装了在错误的地方创建对象的常识。被克隆的源对象不能了解通过克隆建立的目标对象的结构。然而,如果源对象提供了一个合理的、得到了良好封装的接口,反向(目标对象有源对象相关的内容)是可以接受的。

我们这里展示的例子是有效的克隆,因为它们很简单。我们将在下一章展开它们。然而,展示这些基本技术是用来做更多的事情,而不是琐碎的克隆,我们看看将可变对象Hand冻结为不可变对象。

下面可以通过两种方式创建Hand对象的示例:

class Hand3:
def __init__(self, *args, **kw):
if len(args) == 1 and isinstance(args[0], Hand3):
# Clone an existing hand; often a bad idea
other = args[0]
self.dealer_card = other.dealer_card
self.cards = other.cards
else:
# Build a fresh, new hand.
dealer_card, *cards = args
self.dealer_card = dealer_card
self.cards = list(cards)
第一种情况,从现有的Hand3对象创建Hand3实例。第二种情况,从单独的Card实例创建Hand3对象。

与frozenset对象的相似之处在于可由单独的项目或现有set对象创建。我们将在下一章学习创建不可变对象。使用像下面代码片段这样的构造,从现有的Hand创建一个新的Hand使得我们可以创建一个Hand对象的备份:

h = Hand(deck.pop(), deck.pop(), deck.pop())
memento = Hand(h)
我们保存Hand对象到memento变量中。这可以用来比较最后处理的牌与原来手牌,或者我们可以在集合或映射中使用时冻结它。

1. 更复杂的初始化选择
为了编写一个多策略初始化,我们经常被迫放弃特定的命名参数。这种设计的优点是灵活,但缺点是不透明的、毫无意义的参数命名。它需要大量的用例文档来解释变形。

我们还可以扩大我们的初始化来分裂Hand对象。分裂Hand对象的结果是只是另一个构造函数。下面的代码片段说明了如何分裂Hand对象:

class Hand4:
def __init__(self, *args, **kw):
if len(args) == 1 and isinstance(args[0], Hand4):
# Clone an existing handl often a bad idea
other = args[0]
self.dealer_card = other.dealer_card
self.cards= other.cards
elif len(args) == 2 and isinstance(args[0], Hand4) and 'split' in kw:
# Split an existing hand
other, card = args
self.dealer_card = other.dealer_card
self.cards = [other.cards[kw['split']], card]
elif len(args) == 3:
# Build a fresh, new hand.
dealer_card, *cards = args
self.dealer_card = dealer_card
self.cards = list(cards)
else:
raise TypeError("Invalid constructor args={0!r} kw={1!r}".format(args, kw))
def __str__(self):
return ", ".join(map(str, self.cards))
这个设计包括获得额外的牌来建立合适的、分裂的手牌。当我们从一个Hand4对象创建一个Hand4对象,我们提供一个分裂的关键字参数,它从原Hand4对象使用Card类索引。

下面的代码片段展示了我们如何使用被分裂的手牌:

d = Deck()
h = Hand4(d.pop(), d.pop(), d.pop())
s1 = Hand4(h, d.pop(), split=0)
s2 = Hand4(h, d.pop(), split=1)
我们创建了一个Hand4初始化的h实例并分裂到两个其他Hand4实例,s1和s2,并处理额外的Card类。21点的规则只允许最初的手牌有两个牌值相等。

虽然这个__init__()方法相当复杂,它的优点是可以并行的方式从现有集创建fronzenset。缺点是它需要一个大文档字符串来解释这些变化。

2. 初始化静态方法
当我们有多种方法来创建一个对象时,有时会更清晰的使用静态方法来创建并返回实例,而不是复杂的__init__()方法。

也可以使用类方法作为替代初始化,但是有一个实实在在的优势在于接收类作为参数的方法。在冻结或分裂Hand对象的情况下,我们可能需要创建两个新的静态方法冻结或分离对象。使用静态方法作为代理构造函数是一个小小的语法变化,但当组织代码的时候它拥有巨大的优势。

下面是一个有静态方法的Hand,可用于从现有的Hand实例构建新的Hand实例:

class Hand5:
def __init__(self, dealer_card, *cards):
self.dealer_card = dealer_card
self.cards = list(cards)
@staticmethod
def freeze(other):
hand = Hand5(other.dealer_card, *other.cards)
return hand
@staticmethod
def split(other, card0, card1 ):
hand0 = Hand5(other.dealer_card, other.cards[0], card0)
hand1 = Hand5(other.dealer_card, other.cards[1], card1)
return hand0, hand1
def __str__(self):
return ", ".join(map(str, self.cards))
一个方法冻结或创建一个备份。另一个方法分裂Hand5实例来创建两个Hand5实例。

这更具可读性并保存参数名的使用来解释接口。

下面的代码片段展示了我们如何通过这个版本分裂Hand5实例:

d = Deck()
h = Hand5(d.pop(), d.pop(), d.pop())
s1, s2 = Hand5.split(h, d.pop(), d.pop())
我们创建了一个初始的Hand5的h实例,分裂成两个手牌,s1和s2,处理每一个额外的Card类。split()静态方法比__init__()简单得多。然而,它不遵循从现有的set对象创建fronzenset对象的模式。

更多的__init__()技巧
我们会看看一些其他更高级的__init__()技巧。在前面的部分这些不是那么普遍有用的技术。

下面是Player类的定义,使用了两个策略对象和table对象。这展示了一个看起来并不舒服的__init__()方法:

class Player:
def __init__(self, table, bet_strategy, game_strategy):
self.bet_strategy = bet_strategy
self.game_strategy = game_strategy
self.table = table
def game(self):
self.table.place_bet(self.bet_strategy.bet())
self.hand = self.table.get_hand()
if self.table.can_insure(self.hand):
if self.game_strategy.insurance(self.hand):
self.table.insure(self.bet_strategy.bet())
# Yet more... Elided for now
Player的__init__()方法似乎只是统计。只是简单传递命名好的参数到相同命名的实例变量。如果我们有大量的参数,简单地传递参数到内部变量会产生过多看似冗余的代码。

我们可以如下使用Player类(和相关对象):

table = Table()
flat_bet = Flat()
dumb = GameStrategy()
p = Player(table, flat_bet, dumb)
p.game()
我们可以通过简单的传递关键字参数值到内部实例变量来提供一个非常短的和非常灵活的初始化。

下面是使用关键字参数值构建Player类的示例:

class Player2:
def __init__(self, **kw):
"""Must provide table, bet_strategy, game_strategy."""
self.__dict__.update(kw)
def game(self):
self.table.place_bet(self.bet_strategy.bet())
self.hand= self.table.get_hand()
if self.table.can_insure(self.hand):
if self.game_strategy.insurance(self.hand):
self.table.insure(self.bet_strategy.bet())
# etc.
为了简洁而牺牲了大量可读性。它跨越到一个潜在的默默无闻的领域。

因为__init__()方法减少到一行,它消除了某种程度上“累赘”的方法。这个累赘,无论如何,是被传递到每个单独的对象构造函数表达式中。我们必须将关键字添加到对象初始化表达式中,因为我们不再使用位置参数,如下面代码片段所示:

p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb)
为什么这样做呢?

它有一个潜在的优势。这样的类定义是相当易于扩展的。我们可能只有几个特定的担忧,提供额外关键字参数给构造函数。

下面是预期的用例:

>>> p1 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb)
>>> p1.game()
下面是一个额外的用例:

>>> p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb, log_name="Flat/Dumb")
>>> p2.game()
我们添加了一个与类定义无关的log_name属性。也许,这可以被用作统计分析的一部分。Player2.log_name属性可以用来注释日志或其他数据的收集。

我们能添加的东西是有限的;我们只能添加没有与内部使用的命名相冲突的参数。类实现的常识是需要的,用于创建没有滥用已在使用的关键字的子类。由于**kw参数提供了很少的信息,我们需要仔细阅读。在大多数情况下,比起检查实现细节我们宁愿相信类是正常工作的。

在超类的定义中是可以做到基于关键字的初始化的,对于使用超类来实现子类会变得稍微的简单些。我们可以避免编写一个额外的__init__()方法到每个子类,当子类的唯一特性包括了简单新实例变量。

这样做的缺点是,我们已经模糊了没有正式通过子类定义记录的实例变量。如果只是一个小变量,整个子类可能有太多的编程开销用于给一个类添加单个变量。然而,一个小变量常常会导致第二个、第三个。不久,我们将会认识到一个子类会比一个极其灵活的超类还要更智能。

我们可以(也应该)通过混合的位置和关键字实现生成这些,如下面的代码片段所示:

class Player3(Player):
def __init__(self, table, bet_strategy, game_strategy, **extras):
self.bet_strategy = bet_strategy
self.game_strategy = game_strategy
self.table= table
self.__dict__.update(extras)
这比完全开放定义更明智。我们已经取得了所需的位置参数。我们留下任何非必需参数作为关键字。这个阐明了__init__()给出的任何额外的关键字参数的使用。

这种灵活的关键字初始化取决于我们是否有相对透明的类定义。这种开放的态度面对改变需要注意避免调试名称冲突,因为关键字参数名是开放式的。

1. 初始化类型验证
类型验证很少是一个合理的要求。在某种程度上,是没有对Python完全理解。名义目标是验证所有参数是否是一个合适的类型。试图这样做的原因主要是因为适当的定义往往是过于狭隘以至于没有什么真正的用途。

这不同于确认对象满足其他条件。数字范围检查,例如,防止无限循环的必要。

我们可以制造问题去试图做些什么,就像下面__init__()方法中那样:

class ValidPlayer:
def __init__(self, table, bet_strategy, game_strategy):
assert isinstance(table, Table)
assert isinstance(bet_strategy, BettingStrategy)
assert isinstance(game_strategy, GameStrategy)
self.bet_strategy = bet_strategy
self.game_strategy = game_strategy
self.table = table
isinstance()方法检查、规避Python的标准鸭子类型。

我们写一个赌场游戏模拟是为了尝试不断变化的GameStrategy。这些很简单(仅仅四个方法),几乎没有从超类的继承中得到任何帮助。我们可以独立的定义缺乏整体的超类。

这个示例中所示的初始化错误检查,将迫使我们通过错误检查的创建子类。没有可用的代码是继承自抽象超类。

最大的一个鸭子类型问题就围绕数值类型。不同的数值类型将工作在不同的上下文中。试图验证类型的争论可能会阻止一个完美合理的数值类型正常工作。当尝试验证时,我们有以下两个选择在Python中:

我们编写验证,这样一个相对狭窄的集合类型是允许的,总有一天代码会因为聪明的新类型被禁止而中断。

我们避开验证,这样一个相对广泛的集合类型是允许的,总有一天代码会因为不聪明地类型被使用而中断。

注意,两个本质上是相同的。代码可能有一天被中断。要么因为禁止使用即使它是聪明,要么因为不聪明的使用。

让它

一般来说,更好的Python风格就是简单地允许使用任何类型的数据。

我们将在第4章《一致设计的基本知识》回到这个问题。

这个问题是:为什么限制未来潜在的用例?

通常回答是,没有理由限制未来潜在的用例。

比起阻止一个聪明的,但可能是意料之外的用例,我们可以提供文档、测试和调试日志帮助其他程序员理解任何可以处理的限制类型。我们必须提供文档、日志和测试用例,这样额外的工作开销最小。

下面是一个示例文档字符串,它提供了对类的预期:

class Player:
def __init__(self, table, bet_strategy, game_strategy):
"""Creates a new player associated with a table,
and configured with proper betting and play strategies
:param table: an instance of :class:`Table`
:param bet_strategy: an instance of :class:`BettingStrategy`
:param game_strategy: an instance of :class:`GameStrategy`
"""
self.bet_strategy = bet_strategy
self.game_strategy = game_strategy
self.table = table
程序员使用这个类已经被警告了限制类型是什么。其他类型的使用是被允许的。如果类型不符合预期,执行会中断。理想情况下,我们将使用unittest和doctest来发现bug。

2. 初始化、封装和私有
一般Python关于私有的政策可以总结如下:我们都是成年人了。

面向对象的设计有显式接口和实现之间的区别。这是封装的结果。类封装了数据结构、算法、一个外部接口或者一些有意义的事情。这个想法是从实现细节封装分离基于类的接口。

但是,没有编程语言反映了每一个设计细节。Python中,通常情况下,并没有考虑都用显式代码实现所有设计。

类的设计,一方面是没有完全在代码中有私有(实现)和公有(接口)方法或属性对象的区别。私有的概念主要来自(c++或Java)语言,这已经很复杂了。这些语言设置包括如私有、保护、和公有以及“未指定”,这是一种半专用的。私有关键字的使用不当,通常使得子类定义产生不必要的困难。

Python私有的概念很简单,如下

本质上都是公有的。源代码是可用的。我们都是成年人。没有什么可以真正隐藏的。

一般来说,我们会把一些名字的方式公开。他们普遍实现细节,如有变更,恕不另行通知,但是没有正式的私有的概念。

在部分Python中,命名以_开头的一般是非公有的。help()函数通常忽略了这些方法。Sphinx等工具可以从文档隐藏这些名字。

Python的内部命名是以__开始(结束)的。这就是Python保持内部不与应用程序的命名起冲突。这些内部的集合名称完全是由语言内部参考定义的。此外,在我们的代码中尝试使用__试图创建“超级私人”属性或方法是没有任何好处的。一旦Python的发行版本开始使用我们选择内部使用的命名,会造成潜在的问题。同样,我们使用这些命名很可能与内部命名发生冲突。

Python的命名规则如下:

大多数命名是公有的。

以_开头的都是非公有的。使用它们来实现细节是真正可能发生变化的。

以__开头或结尾的命名是Python内部的。我们不能这样命名;我们使用语言参考定义的名称。

一般情况下,Python方法使用文档和好的命名来表达一个方法(或属性)的意图。通常,接口方法会有复杂的文档,可能包括doctest的示例,而实现方法将有更多的简写文档,很可能没有doctest示例。

新手Python程序员,有时奇怪私有没有得到更广泛的使用。而经验丰富的Python程序员,却惊讶于为了整理并不实用的私有和公有声明去消耗大脑的卡路里,因为从方法的命名和文档中就能知道变量名的意图。