当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深——这通常意味着自己正在手动完成事情,本应该通过写代码来让宏的扩展自动实现。
——Paul Graham, Lisp 黑客和风险投资人
Python 内置了迭代器模式,用于进行惰性运算,按需求一次获取一个数据项,避免不必要的提前计算。
迭代器在 Python 中并不是一个具体类型的对象,更多地使指一个具体协议。
Python 解释器在迭代一个对象时,会自动调用 iter(x)
。
内置的 iter
函数会做以下操作:
__iter__
方法(abc.Iterable
),若实现,且返回的结果是个迭代器(abc.Iterator
),则调用它,获取迭代器并返回;__getitem__
方法(abc.Sequence
),若实现则尝试从 0 开始按顺序获取元素并返回;TypeError
,表明对象不可迭代。判断一个对象是否可迭代,最好的方法不是用 isinstance
来判断,而应该直接尝试调用 iter
函数。
注:可迭代对象和迭代器不一样。从鸭子类型的角度看,可迭代对象 Iterable
要实现 __iter__
,而迭代器 Iterator
要实现 __next__
. 不过,迭代器上也实现了 __iter__
,用于返回自身。
《设计模式:可复用面向对象软件的基础》一书讲解迭代器设计模式时,在“适用性”一 节中说:
迭代器模式可用来:
访问一个聚合对象的内容而无需暴露它的内部表示
支持对聚合对象的多种遍历
为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)
为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用 iter(my_iterable)
都新建一个独立的迭代器。
iter
函数解释器需要迭代对象 x 时,会自动调用 iter(x)
:
检查对象是否实现了 __iter__
方法并调用,获取到迭代器
如果没有实现__iter__
, 检查是否有 __getitem__
函数,尝试按顺序下标获取元素
如果上述状况都不符合, 抛出 “C object is not iterable” 异常
这就是为什么这个示例需要定义 SentenceIterator
类。所以,不应该把 Sentence 本身作为一个迭代器,否则每次调用 iter(sentence)
时返回的都是自身,就无法进行多次迭代了。
1 | # 通过实现迭代器协议,让一个对象变得可迭代 |
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 | # 使用生成器函数来帮我们创建迭代器 |
1 | # 使用 re.finditer 来惰性生成值 |
itertools
模块生成等差数列1 | # 实用模块 |
1 | # 使用 yield from 语句可以在生成器函数中直接迭代一个迭代器 |
[1, 2, 3, 4, 5] [1, 2, 3, 4, 5]
iter
函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。
1 | # iter 的神奇用法 |
文章转载自:
https://hanjianwei.com/2013/07/25/python-mro/
对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。
而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。对于只支持单继承的语言来说,MRO 一般比较简单;而对于 Python 这种支持多继承的语言来说,MRO 就复杂很多。
先看一个「菱形继承」的例子:
如果 x
是 D
的一个实例,那么 x.show()
到底会调用哪个 show
方法呢?
如果按照 [D, B, A, C]
的搜索顺序,那么 x.show()
会调用 A.show()
;
如果按照 [D, B, C, A]
的搜索顺序,那么 x.show()
会调用 C.show()
。
由此可见,MRO 是把类的继承关系线性化的一个过程,而线性化方式决定了程序运行过程中具体会调用哪个方法。既然如此,那什么样的 MRO 才是最合理的?Python 中又是如何实现的呢?
Python 至少有三种不同的 MRO:
Python 有两种类:经典类(classic class)和新式类(new-style class)。两者的不同之处在于新式类继承自 object
。
在 Python 2.1 以前,经典类是唯一可用的形式;Python 2.2 引入了新式类,使得类和内置类型更加统一;在 Python 3 中,新式类是唯一支持的类。
经典类采用了一种很简单的 MRO 方法:从左至右的深度优先遍历。以上述「菱形继承」为例,其查找顺序为 [D, B, A, C, A]
,如果只保留重复类的第一个则结果为 [D, B, A, C]
。我们可以用 inspect.getmro
来获取类的 MRO:
1 | import inspect |
这种深度优先遍历对于简单的情况还能处理的不错,但是对于上述「菱形继承」其结果却不尽如人意:虽然 C.show()
是 A.show()
的更具体化版本(显示了更多的信息),但我们的 x.show()
没有调用它,而是调用了 A.show()
。这显然不是我们希望的结果。
对于新式类而言,所有的类都继承自 object
,所以「菱形继承」是非常普遍的现象,因此不可能采用这种 MRO 方式。
为解决经典类 MRO 所存在的问题,Python 2.2 针对新式类提出了一种新的 MRO 计算方式:在定义类时就计算出该类的 MRO 并将其作为类的属性。因此新式类可以直接通过 __mro__
属性获取类的 MRO。
Python 2.2 的新式类 MRO 计算方式和经典类 MRO 的计算方式非常相似:它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。重新考虑上面「菱形继承」的例子,由于新式类继承自 object
因此类图稍有改变:
按照深度遍历,其顺序为 [D, B, A, object, C, A, object]
,重复类只保留最后一个,因此变为 [D, B, C, A, object]
。代码为:
1 | class A(object): |
这种 MRO 方式已经能够解决「菱形继承」问题,再让我们看个稍微复杂点的例子:
1 | class X(object): pass |
首先进行深度遍历,结果为 [C, A, X, object, Y, object, B, Y, object, X, object]
;然后,只保留重复元素的最后一个,结果为 [C, A, B, Y, X, object]
。Python 2.2 在实现该方法的时候进行了调整,使其更尊重基类中类出现的顺序,其实际结果为 [C, A, B, X, Y, object]
。
这样的结果是否合理呢?首先我们看下各个类中的方法解析顺序:对于 A
来说,其搜索顺序为 [A, X, Y, object]
;对于 B
,其搜索顺序为 [B, Y, X, object]
;对于 C
,其搜索顺序为 [C, A, B, X, Y, object]
。我们会发现,B
和 C
中 X
、Y
的搜索顺序是相反的!也就是说,当 B
被继承时,它本身的行为竟然也发生了改变,这很容易导致不易察觉的错误。此外,即使把 C
搜索顺序中 X
和 Y
互换仍然不能解决问题,这时候它又会和 A
中的搜索顺序相矛盾。
事实上,不但上述特殊情况会出现问题,在其它情况下也可能出问题。其原因在于,上述继承关系违反了线性化的「 单调性原则 」。Michele Simionato对单调性的定义为:
A MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C. Otherwise, the innocuous operation of deriving a new class could change the resolution order of methods, potentially introducing very subtle bugs.
也就是说,子类不能改变基类的方法搜索顺序。在 Python 2.2 的 MRO 算法中并不能保证这种单调性,它不会阻止程序员写出上述具有二义性的继承关系,因此很可能成为错误的根源。
除了单调性之外,Python 2.2 及 经典类的 MRO 也可能违反继承的「 局部优先级 」,具体例子可以参见官方文档。采用一种更好的 MRO 方式势在必行。
为解决 Python 2.2 中 MRO 所存在的问题,Python 2.3以后采用了 C3 方法来确定方法解析顺序。你如果在 Python 2.3 以后版本里输入上述代码,就会产生一个异常,禁止创建具有二义性的继承关系:
1 | class C(A, B): pass |
我们把类 C
的线性化(MRO)记为 L[C] = [C1, C2,…,CN]
。其中 C1
称为 L[C]
的头,其余元素 [C2,…,CN]
称为尾。如果一个类 C
继承自基类 B1
、B2
、……、BN
,那么我们可以根据以下两步计算出 L[C]
:
L[object] = [object]
L[C(B1…BN)] = [C] + merge(L[B1]…L[BN], [B1]…[BN])
这里的关键在于 merge
,其输入是一组列表,按照如下方式输出一个列表:
L[B1]
的头),记作 H
。H
未出现在其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤1;否则,取出下一个列表的头部记作 H
,继续该步骤。该方法有点类似于图的拓扑排序,但它同时还考虑了基类的出现顺序。我们用 C3 分析一下刚才的例子。
object
,X
,Y
的线性化结果比较简单:
1 | L[object] = [object] |
A
的线性化计算如下:
1 | L[A] = [A] + merge(L[X], L[Y], [X], [Y]) |
注意第3步,merge([object], [Y, object], [Y])
中首先输出的是 Y
而不是 object
。这是因为 object
虽然是第一个列表的头,但是它出现在了第二个列表的尾部。所以我们会跳过第一个列表,去检查第二个列表的头部,也就是 Y
。Y
没有出现在其它列表的尾部,所以将其输出。
同理,B
的线性化结果为:
1 | L[B] = [B, Y, X, object] |
最后,我们看看 C
的线性化结果:
1 | L[C] = [C] + merge(L[A], L[B], [A], [B]) |
到了最后一步我们没有办法继续计算下去 了:X
虽然是第一个列表的头,但是它出现在了第二个列表的尾部;Y
虽然是第二个列表的头,但是它出现在了第一个列表的尾部。因此,我们无法构建一个没有二义性的继承关系,只能手工去解决(比如改变 B
基类中 X
、Y
的顺序)。
我们再看一个没有冲突的例子:
计算过程如下:
1 | L[object] = [object] |
当然,可以用代码验证类的 MRO,上面的例子可以写作:
1 | class D(object): pass |
抽象类表示接口。
——Bjarne Stroustrup, C++ 之父
本章讨论的话题是接口:
从鸭子类型的代表特征动态协议,到使接口更明确、能验证实现是否符合规定的抽象基类(Abstract Base Class, ABC)。
接口的定义:对象公开方法的子集,让对象在系统中扮演特定的角色。
协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。
允许一个类上只实现部分接口。
什么是接口
对象公开方法的子集,让对象在系统中扮演特定的角色。
鸭子类型与动态协议
受保护的类型与私有类型不能在接口中
可以把公开的数据属性放在接口中
1 | class Foo: |
Foo 实现了序列协议的 __getitem__
方法。因此可支持下标操作。
Foo 实例是可迭代的对象,因此可以使用 in 操作符
FrenchDeck 类见前面章节。
FrenchDeck 实例的行为像序列,那么其实可以用 random 的 shuffle
方法来代替在类中实现的方法。
1 | from random import shuffle |
FrenchDeck 对象不支持元素赋值。这是因为它只实现了不可变的序列协议,可变的序列还必须提供 __setitem__
方法。
1 | def set_card(deck, pos, card): |
这种技术叫做猴子补丁:在运行是修改类或程序,而不改动源码。缺陷是补丁代码与要打补丁的程序耦合紧密。
抽象基类是一个非常实用的功能,可以使用抽象基类来检测某个类是否实现了某种协议,而这个类并不需要继承自这个抽象类。collections.abc
和 numbers
模块中提供了许多常用的抽象基类以用于这种检测。
有了这个功能,我们在自己实现函数时,就不需要非常关心外面传进来的参数的具体类型(isinstance(param, list)
),只需要关注这个参数是否支持我们需要的协议(isinstance(param, abc.Sequence
)以保障正常使用就可以了。
但是注意:从 Python 简洁性考虑,最好不要自己创建新的抽象基类,而应尽量考虑使用现有的抽象基类。
1 | # 抽象基类 |
1 | # 在抽象基类上进行自己的实现 |
有一点需要注意:抽象基类上的方法并不都是抽象方法。
换句话说,想继承自抽象基类,只需要实现它上面所有的抽象方法即可,有些方法的实现是可选的。
比如 Sequence.__contains__
,Python 对此有自己的实现(使用 __iter__
遍历自身,查找是否有相等的元素)。但如果你在 Sequence
之上实现的序列是有序的,则可以使用二分查找来覆盖 __contains__
方法,从而提高查找效率。
我们可以使用 __abstractmethods__
属性来查看某个抽象基类上的抽象方法。这个抽象基类的子类必须实现这些方法,才可以被正常实例化。
1 | # 自己定义一个抽象基类 |
使用 register
接口可以将某个类注册为某个 ABC 的“虚拟子类”。支持 register
直接调用注册,以及使用 @register
装饰器方式注册(其实这俩是一回事)。
注册后,使用 isinstance
以及实例化时,解释器将不会对虚拟子类做任何方法检查。
注意:虚拟子类不是子类,所以虚拟子类不会继承抽象基类的任何方法。
1 | # 虚拟子类 |
本文档是对TeachYourselfCS内容的中文翻译,原作者为Ozan Onay和Myles Byrne。如需了解翻译相关信息或帮助改进翻译,请参见本文档结尾。
This document is a Chinese translation of TeachYourselfCS, which is written by Ozan Onay and Myles Byrne. For more information about this translation, please refer to the end of this document.
如果你是一个自学成才的工程师,或者从编程培训班毕业,那么你很有必要学习计算机科学。幸运的是,不必为此花上数年光阴和不菲费用去攻读一个学位:仅仅依靠自己,你就可以获得世界一流水平的教育💸。
互联网上,到处都有许多的学习资源,然而精华与糟粕并存。你所需要的,不是一个诸如“200+免费在线课程”的清单,而是以下问题的答案:
在这份指引中,我们尝试对这些问题做出确定的回答。
大致按照列出的顺序,借助我们所建议的教材或者视频课程(但是最好二者兼用),学习如下的九门科目。目标是先花100到200个小时学习完每一个科目,然后在你职业生涯中,不时温习其中的精髓🚀。
科目 | 为何要学? | 最佳书籍 | 最佳视频 |
---|---|---|---|
编程 | 不要做一个“永远没彻底搞懂”诸如递归等概念的程序员。 | 《计算机程序的构造和解释》 | Brian Harvey’s Berkeley CS 61A |
计算机架构 | 如果你对于计算机如何工作没有具体的概念,那么你所做出的所有高级抽象都是空中楼阁。 | 《深入理解计算机系统》 | Berkeley CS 61C |
算法与数据结构 | 如果你不懂得如何使用栈、队列、树、图等常见数据结构,遇到有难度的问题时,你将束手无策。 | 《算法设计手册》 | Steven Skiena’s lectures |
数学知识 | 计算机科学基本上是应用数学的一个“跑偏的”分支,因此学习数学将会给你带来竞争优势。 | 《计算机科学中的数学》 | Tom Leighton’s MIT 6.042J |
操作系统 | 你所写的代码,基本上都由操作系统来运行,因此你应当了解其运作的原理。 | 《操作系统导论》 | Berkeley CS 162 |
计算机网络 | 互联网已然势不可挡:理解工作原理才能解锁全部潜力。 | 《计算机网络:自顶向下方法》 | Stanford CS 144 |
数据库 | 对于多数重要程序,数据是其核心,然而很少人理解数据库系统的工作原理。 | 《Readings in Database Systems》 (暂无中译本) | Joe Hellerstein’s Berkeley CS 186 |
编程语言与编译器 | 若你懂得编程语言和编译器如何工作,你就能写出更好的代码,更轻松地学习新的编程语言。 | 《Crafting Interpreters》 | Alex Aiken’s course on Lagunita |
分布式系统 | 如今,多数 系统都是分布式的。 | 《数据密集型应用系统设计》 | MIT 6.824 |
如果花几年时间自学 9 门科目让人望而却步,我们建议你只专注于两本书:《深入理解计算机系统》 和 《数据密集型应用系统设计》。根据我们的经验,投入到这两本书的时间可以获得极高的回报率,特别适合从事网络应用开发的自学工程师。这两本书也可以作为上面表格中其他科目的纲领。
软件工程师分为两种:一种充分理解了计算机科学,从而有能力应对充满挑战的创造性工作;另一种仅仅凭着对一些高级工具的熟悉而勉强应付。
这两种人都自称软件工程师,都能在职业生涯早期挣到差不多的工资。然而,随着时间流逝,第一种工程师不断成长,所做的事情将会越来越有意义且更为高薪,不论是有价值的商业工作、突破性的开源项目、技术上的领导力或者高质量的个人贡献。
全球短信系统每日收发约200亿条信息,而仅仅靠57名工程师,现在的 WhatsApp 每日收发420亿条。
— Benedict Evans (@BenedictEvans) 2016年2月2日
第一种工程师总是寻求深入学习计算机科学的方法,或是通过传统的方法学习,或是在职业生涯中永无止息地学习;第二种工程师
通常浮于表面,只学习某些特定的工具和技术,而不研究其底层的基本原理,仅仅在技术潮流的风向改变时学习新的技能。
如今,涌入计算机行业的人数激增,然而计算机专业的毕业生数量基本上未曾改变。第二种工程师的供过于求正在开始减少他们的工作机会,使他们无法涉足行业内更加有意义的工作。对你而言,不论正在努力成为第一种工程师,还是只想让自己的职业生涯更加安全,学习计算机科学是唯一可靠的途径。
23333 然而他们… pic.twitter.com/XVNYlXAHar
— Jenna Bilotta (@jenna) 2017年3月4日
大多数计算机专业本科教学以程序设计“导论”作为开始。这类课程的最佳版本不仅能满足初学者的需要,还适用于那些在初学编程阶段遗漏了某些有益的概念和程序设计模式的人。
对于这部分内容,我们的标准推荐是这部经典著作:《计算机程序的构造和解释》。在网络上,这本书既可供免费阅读(英文版),也作为MIT的免费视频课程。不过尽管这些视频课程很不错,我们对于视频课程的推荐实际上是Brian Harvey 开设的 SICP 课程(即 Berkeley 的 61A 课程)。比起MIT的课程,它更加完善,更适用于初学者。
我们建议至少学完SICP的前三章,并完成配套的习题。如果需要额外的练习,可以去解决一些小的程序设计问题,比如exercism。
中文翻译新增:
- 关于SICP国内视频观看地址
- Scheme 学习的相关资源参见:https://github.com/DeathKing/Learning-SICP
- 更详细的补充说明:#3
自从 2016 年首次发布这份指南以来,最常被问到的一个问题是,我们是否推荐由 John DeNero 讲授的更新的 CS 61A 课程,以及配套的书籍 《Composing Programs》,这本书“继承自 SICP” 但使用 Python 讲解。我们认为 DeNero 的课程也很不错,有的学生可能更喜欢,但我们还是建议把 SICP, Scheme 和 Brian Harvey 的视频课程作为首选。
为什么这么说呢?因为 SICP 是独一无二的,它可以——至少很有可能——改变你对计算机和编程的基本认识。不是每个人都有这样的体验。有的人讨厌这本书,有的人看了前几页就放弃了。但潜在的回报让它值得一读。
如果你觉得SICP过于难,试试 《Composing Programs》。如果还是不合适,那我们推荐 《程序设计方法》(中文版,英文版) ;如果你觉得SICP过于简单,那我们推荐 《Concepts, Techniques, and Models of Computer Programming》 。如果读这些书让你觉得没有收获,也行你应该先学习其他科目,一两年后再重新审视编程的理念。
新版原文删除了对 《Concepts, Techniques, and Models of Computer Programming》 一书的推荐,但这本书对各种编程模型有深入的见解,值得一读。所以译文中依然保留。
— 译者注
最后,有一点要说明的是:本指南不适用于完全不懂编程的新手。我们假定你是一个没有计算机专业背景的程序员,希望填补一些知识空白。事实上,我们把“编程”章节包括进来只是提醒你还有更多知识需要学习。对于那些从来没有学过编程,但又想学的人来说,这份指南更合适。
计算机架构——有时候又被称为“计算机系统”或者“计算机组成”——是了解软件底层的的重要视角。根据我们的经验,这是自学的软件工程师最容易忽视的领域。
我们最喜欢的入门书是 《深入理解计算机系统》。典型的计算机体系结构导论课程会涵盖本书的 1 - 6 章。
我们喜爱《深入理解计算机系统》,因为它的实用性,并且站在程序员的视角。虽然计算机体系结构的内容比本书所涉及的内容多得多,但对于那些想了解计算机系统以求编写更快、更高效、更可靠的软件的人来说,这本书是很好的起点。
对于那些既想了解这个主题又想兼顾硬件和软件的知识的人来说,我们推荐 《计算机系统要素》,又名“从与非门到俄罗斯方块”(“Nand2Tetris”),这本书规模宏大,让读者对计算机内的所有部分如何协同工作有完全的认识。这本书的每一章节对应如何构建计算机整体系统中的一小部分,从用HDL(硬件描述语言)写基本的逻辑门电路出发,途经CPU和汇编,最终抵达诸如俄罗斯方块这般规模的应用程序。
我们推荐把此书的前六章读完,并完成对应的项目练习。这么做,你将更加深入地理解,计算机架构和运行其上的软件之间的关系。
这本书的前半部分(包括所有对应的项目)均可从Nand2Tetris的网站上免费获得。同时,在Coursera上,这是一门视频课程。
为了追求简洁和紧凑,这本书牺牲了内容上的深度。尤其值得注意的是,流水线和存储层次结构是现代计算机架构中极其重要的两个概念,然而这本书对此几乎毫无涉及。
当你掌握了Nand2Tetris的内容后,我们推荐要么回到《深入理解计算机系统》,或者考虑Patterson和Hennessy二人所著的 《计算机组成与设计》,一本优秀的经典著作。这本书中的不同章节重要程度不一,因此我们建议根据Berkeley的CS61C课程 “计算机架构中的伟大思想”来着重阅读一些章节。这门课的笔记和实验在网络上可以免费获得,并且在互联网档案中有这门课程的过往资料。
硬件是平台。
— Mike Acton, Engine Director at Insomniac Games
(观看他在CppCon上的演说)
正如几十年来的共识,我们认为,计算机科学教育所赋予人们的最大能量在于对常见算法和数据结构的熟悉。此外,这也可以训练一个人对于各种问题的解决能力,有助于其他领域的学习。
关于算法与数据结构,有成百上千的书可供使用,但是我们的最爱是Steven Skiena编写的 《算法设计手册》。显而易见,他对此充满热爱,迫不及待地想要帮助其他人理解。在我们看来,这本书给人一种焕然一新的体验,完全不同于那些更加经常被推荐的书(比如Cormen,Leiserson,Rivest 和 Stein,或者 Sedgewick的书,后两者充斥着过多的证明,不适合以 解决问题 为导向的学习)。
如果你更喜欢视频课程,Skiena慷慨地提供了他的课程。此外,Tim Roughgarden的课程也很不错,
在Stanford的MOOC平台Lagunita,或者Coursera上均可获得。Skiena和Roughgarden的这两门课程没有优劣之分,选择何者取决于个人品味。
至于练习,我们推荐学生在Leetcode上解决问题。Leetcode上的问题往往有趣且带有良好的解法和讨论。此外,在竞争日益激烈的软件行业,这些问题可以帮助你评估自己应对技术面试中常见问题的能力。我们建议解决大约100道随机挑选的Leetcode问题,作为学习的一部分。
最后,我们强烈推荐 《怎样解题》。这本书极为优秀且独特,指导人们解决广义上的问题,因而一如其适用于数学,它适用于计算机科学。
我可以广泛推荐的方法只有一个: 写之前先思考。
— Richard Hamming
从某个角度说,计算机科学是应用数学的一个“发育过度”的分支。尽管许多软件工程师试图——并且在不同程度上成功做到——忽视这一点,我们鼓励你用学习来拥抱数学。如若成功,比起那些没有掌握数学的人,你将获得巨大的竞争优势。
对于计算机科学,数学中最相关的领域是“离散数学”,其中的“离散”与“连续”相对立,大致上指的是应用数学中那些有趣的主题,而不是微积分之类的。由于定义比较含糊,试图掌握离散数学的全部内容是没有意义的。较为现实的学习目标是,了解逻辑、排列组合、概率论、集合论、图论以及密码学相关的一些数论知识。考虑到线性代数在计算机图形学和机器学习中的重要性,该领域同样值得学习。
学习离散数学,我们建议从László Lovász的课程笔记开始。Lovász教授成功地让这些内容浅显易懂且符合直觉,因此,比起正式的教材,这更适合初学者。
对于更加高阶的学习,我们推荐 《计算机科学中的数学》,MIT同名课程的课程笔记,篇幅与书籍相当(事实上,现已出版)。这门课程的视频同样可免费获得,是我们所推荐的学习视频。
对于线性代数,我们建议从Essence of linear algebra系列视频开始,然后再去学习Gilbert Strang的《线性代数导论》和视频课程。
如果人们不相信数学是简单的,那么只能是因为他们没有意识到生活有多么复杂。
— John von Neumann
《操作系统概念》 (“恐龙书”)和 《现代操作系统》 是操作系统领域的经典书籍。二者都因为写作风格和对学生不友好而招致了一些批评。
《操作系统导论》(Operating Systems: Three Easy Pieces) 是一个不错的替代品,并且可在网上免费获得(英文版)。我们格外喜欢这本书的结构,并且认为这本书的习题很值得一做。
在读完《操作系统导论》后,我们鼓励你探索特定操作系统的设计。可以借助“{OS name} Internals”风格的书籍,比如 Lion’s commentary on Unix, The Design and Implementation of the FreeBSD Operating System,以及 Mac OS X Internals。对于 Linux ,我们推荐 Robert Love 的 《Linux内核设计与实现》。
为了巩固对操作系统的理解,阅读小型系统内核的代码并且为其增加特性是一个很不错的方法。比如,xv6,由MIT的一门课程所维护的从Unix V6到ANSI C和x86的移植,就是一个很棒的选择。《操作系统导论》有一个附录,记载了一些可能的xv6实验项目,其中充满了关于潜在项目的很棒想法。
鉴于有那么多关于网络服务端和客户端的软件工程,计算机网络是计算机科学中价值最为“立竿见影”的领域之一。我们的学生,系统性地学习了计算机网络,最终能够理解那些曾困扰他们多年的术语、概念和协议。
在这一主题上,我们最爱的书籍是 《计算机网络:自顶向下方法》。书中的小项目和习题相当值得练习,尤其是其中的“Wireshark labs”(这部分在网上可以获得)。
如果更喜欢视频课程,我们推荐Stanford的Introduction to Computer Networking,可在他们的MOOC平台Lagunita上免费观看。
对于计算机网络的学习,做项目比完成小的习题更有益。一些可能的项目有:HTTP服务器,基于UDP的聊天APP,迷你TCP栈,代理,负载均衡器,或者分布式哈希表。
你无法盯着水晶球预见未来,未来的互联网何去何从取决于社会。
— Bob Kahn
比起其他主题,自学数据库系统需要更多的付出。这是一个相对年轻的研究领域,并且出于很强的商业动机,研究者把想法藏在紧闭的门后。此外,许多原本有潜力写出优秀教材的作者反而选择了加入或创立公司。
鉴于如上情况,我们鼓励自学者大体上抛弃教材,而是从2015年春季学期的CS 186课程(Joe Hellerstein在Berkeley的数据库课程)开始,然后前往阅读论文。
对于初学者,有一篇格外值得提及的论文:“Architecture of a Database System”。这篇论文提供了独特的对关系型数据库管理系统(RDBMS)如何工作的高层次观点,是后续学习的实用梗概。
《Readings in Database Systems》,或者以数据库“红书”更为人知,是由Peter Bailis,Joe Hellerstein和Michael Stonebraker编纂的论文合集。对于那些想要在CS 186课程的水平更进一步的学习者,“红书”应当是下一步。
如果你坚持一定要一本导论教材,那我们推荐Ramakrishnan和Gehrke所著的 《数据库管理系统:原理与设计》。如需更深一步,Jim Gray的经典著作 《Transaction Processing: Concepts and Techniques》 值得一读,不过我们不建议把这本书当作首要资源。
如果没有编写足够数量的代码,很难巩固数据库理论。CS 186课程的学生给Spark添加特性,倒是不错的项目,不过我们仅仅建议从零实现一个简单的关系型数据库管理系统。自然,它将不会有太多的特性,但是即便只实现典型的关系型数据库管理系统每个方面最基础的功能,也是相当有启发的。
最后,数据模型往往是数据库中一个被忽视的、教学不充分的方面。关于这个主题,我们推荐的书籍是 Data and Reality: A Timeless Perspective on Perceiving and Managing Information in Our Imprecise World。
多数程序员学习编程语言的知识,而多数计算机科学家学习编程语言 相关 的知识。这使得计算机科学家比起程序员拥有显著的优势,即便在编程领域!因为他们的知识可以推而广之:相较只学习过特定编程语言的人,他们可以更深入更快速地理解新的编程语言。
我们推荐的入门书是 Bob Nystrom 所著的优秀的 Crafting Interpreters,可在网上免费获取。这本书条理清晰,富有趣味性,非常适合那些想要更好地理解语言和语言工具的人。我们建议你花时间读完整本书,并尝试任何一个感兴趣的“挑战”。
另一本更为传统的推荐书籍是 《编译原理》,通常称为“龙书”。不幸的是,这本书不是为自学者而设计的,而是供教师从中挑选一些主题用于1-2学期的教学。
如果你选择使用龙书进行自学,你需要从中甄选主题,而且最好是在导师的帮助下。我们建议依据某个视频课程来设定学习的结构,然后按需从龙书中获取深入的内容。我们推荐的在线课程是Alex Aiken在MOOC平台 edX 所开设的。
不要做一个只写样板代码的程序员。相反,给用户和其他程序员创造工具。从纺织工业和钢铁工业中学习历史教训:你想制造机器和工具,还是操作这些机器?
— Ras Bodik 在他的编译器课程伊始
随着计算机在数量上的增加,计算机同样开始 分散。尽管商业公司过去愿意购买越来越大的大型机,现在的典型情况是,甚至很小的应用程序都同时在多台机器上运行。思考这样做的利弊权衡,即是分布式系统的研究所在,也是越来越重要的一项技能。
我们推荐的自学参考书是 Martin Kleppmann 的 《数据密集型应用系统设计》。与传统的教科书相比,它是一本为实践者设计的具有很高的可读性的书,并且保持了深度和严谨性。
对于那些偏爱传统教材,或者希望可以从网上免费获取的人,我们推荐的教材是Maarten van Steen和Andrew Tanenbaum所著的 《分布式系统原理与范型》(中文第二版,英文第三版)。
对于喜欢视频课程的人,MIT的6.824 是一门很好的在线视频课程,由 Robert Morris 教授的研究生课程,在这里可以看到课程安排。
不管选择怎样的教材或者其他辅助资料,学习分布式系统必然要求阅读论文。这里有一个不错的论文清单,而且我们强烈建议你出席你当地的Papers We Love(仅限美国)。
我们面向自学的软件工程师、培训班学生、“早熟的”高中生或者想要通过自学补充正式教育的大学生。关于何时开启这段自学旅程,完全取决于个人,不过多数人在有一定的职业经历后深入学习计算机科学理论会获益匪浅。比如,我们注意到,如果学生在工作中曾经使用过数据库,他们会 喜爱 学习数据库系统课程;如果学生从事过一两个Web项目,他们会 喜爱 学习计算机网络。
我们试图把计算机科学主题清单限制到那些我们认为 每一个软件工程师 都应该了解的内容,不限于专业或行业。拥有了这些基础,你将能更加轻松地挑选教材或论文,然而无需指引地学习核心概念。在这里,我们给出一些其他常见主题的自学起点:
事实上,所有主题之间都有一定程度的重叠,彼此循环引用。以离散数学和算法的关系为例:先学习数学可以帮助你更深入地分析和理解算法,然而先学习算法可以为学习离散数学提供更大的动力和应用背景。理想情况下,你将在你的职业生涯多次重温二者。
因此,我们所推荐的次序主要是为了帮助你 起步……如果你出于某种强烈的原因而倾向以不同的顺序学习,那也没有关系,勇敢开始吧!不过在我们看来,最重要的“先决条件”是:先学计算机架构再学操作系统或数据库,先学计算机网络和操作系统再学分布式系统。
OSS指引涵盖太多主题,在许多主题中推荐劣质资源,没有就特定课程哪些方面有价值提供原因或指引。我们努力对这份指引中的课程加以限制,仅仅包括那些你作为软件工程师 确实需要了解的,不论你的专业方向,并且对每门课程为何必要做出了解释以帮助你理解。
FreeCodeCamp主要关注编程,而不是计算机科学。至于你为什么要学习计算机科学,参见上文。如果你是个新手,我们建议先学 freeCodeCamp 的课程,一两年后再回归本指南。
学习一门特定的编程语言和学习计算机科学的一个领域完全不在一个维度——相比之下,学习语言 容易 且 缺乏价值。如果你已经了解了一些语言,我们强烈建议遵照我们的指引,然后在学习的空当中习得语言,或者暂且不管以后再说。如果你已经把编程学得不错了(比如学完了 《计算机程序的构造和解释》),尤其是如果你学习过编译器,那么面对一门新的语言,你只需要花一个周末稍多的时间即可基本掌握,之后你可以在工作中学习相关的类库/工具/生态。
没有任何一种技术的重要程度可以达到学习其使用足以成为计算机科学教学的核心部分。不过,你对学习那门技术充满热情,这很不错。诀窍是先从特定的技术回退到基本的领域或概念,判断这门流行技术在技术的宏观大局中位于何处,然后才深入学习这门技术。
先尝试读一下,有些人觉得 SICP 让人神魂颠倒,这在其他书很少见。如果你不喜欢,你可以尝试其他的东西,也许以后再回到 SICP。
龙书依旧是内容最为完整的编译器单本书籍。由于过分强调一些如今不够时新的主题的细节,比如解析,这本书招致了恶评。然而事实上,这本书从未打算供人一页一页的学习,而仅仅是为了给教师准备一门课程提供足够的材料。类似地,自学者可以从书中量身按需挑选主题,或者最好依照公开课授课教师在课程大纲中的建议。
我们所建议的许多教材在网上都可以免费获得,这多亏了作者们的慷慨。对于那些不免费的书籍,我们建议购买旧版本的二手书籍。广而言之,如果一本教材有多个版本,旧版本大概率是完全足够使用的。即便新版本的价格是旧版本的10倍,新版本也绝不可能比旧版本好10倍!
中文翻译新增: 事实上,比起美国,在国内购买技术书籍可以说是相当“廉价”了。如果仍旧寻求更加便宜的购买渠道,可以参考这篇V2EX上的讨论帖子,其中提到了一些不错的购买渠道。
这份指引由Bradfield School of Computer Science(旧金山)的两位教员:Ozan Onay和Myles Byrne编写,并由 Oz 于 2020 年更新。这份指引基于我们对数千名自学成才的工程师和培训班学生教授计算机科学基础的经验。感谢我们所有学生对自学资源的持续反馈。
只要有足够的时间和动力,我们非常有信心,你可以自学完以上所有课程。如果你喜欢一个集中式、结构化、由教师指导的课程,你可能对我们的计算机科学强化班感兴趣。我们不建议你去攻读硕士学位。
这份指引的中文翻译是社区共同贡献的成果,我们欢迎任何反馈和改进!
]]>转载自:https://ncona.com/2020/06/create-diagrams-with-code-using-graphviz/
您是否曾为绘制过架构图时重复的单击和拖动而感到乏味?
您是否需要对该图进行修改发现改动却很复杂?
Graphviz 是一个开源的图形可视化软件,它使我们能够使用代码描述图表,并为我们自动绘制。如果将来需要修改该图,我们只需要修改描述代码,节点和边将自动为我们重新定位。
在开始编写图形之前,我们需要学习如何将代码转换为图像,以便可以测试正在做的事情。
Webgraphviz.com 可用于从浏览器绘制图形。
我们可以使用 apt 在 Ubuntu 中安装命令行工具:
1 | 1 sudo apt install graphviz |
在 macOS 环境 使用 brew 安装
1 | brew install graphviz |
除其他外,这将安装 dot
CLI,该CLI可用于从文本文件生成图像:
1 | 1 dot -Tpng input.gv -o output.png |
在上面的示例中,我们将 png 指定为output(-Tpng
),但是有许多可用的选项。如我们所见,输入文件通常使用gv
扩展名。
DOT是用于描述要由Graphviz解析的图形的最常见格式。
一个简单的图形具有以下形式:
1 | graph MyGraph { |
如果要使用有向图(带箭头),则需要使用digraph
:
1 | digraph MyGraph { |
箭头可以单向或双向:
1 | digraph MyGraph { |
如果我们不喜欢椭圆形,可以使用其他形状:
1 | digraph MyGraph { |
我们还可以向节点添加一些颜色和样式:
1 | digraph MyGraph { |
箭头的尾巴和头部也可以修改:
1 | digraph MyGraph { |
可以在箭头形状文档中找到不同的箭头类型。
以及向箭头线添加样式:
1 | digraph MyGraph { |
如果我们注意上面的代码和图表,我们可以看到,当我们为箭头指定多种颜色时,如果不指定任何权重,每种颜色将只有一行。如果我们想要一个带有多种颜色的箭头,则至少一种颜色必须指定要覆盖的线条的重量百分比:
1 | 1 a -> e [dir=none,color="green:red;.3:blue"] |
我们可以向节点添加标签:
1 | digraph MyGraph { |
以及顶点:
1 | digraph MyGraph { |
我们可以设置标签样式:
1 | digraph MyGraph { |
聚类也称为子图。集群的名称必须以开头cluster_
,否则将不会包含在框中。
1 | digraph MyGraph { |
集群可以根据需要嵌套:
1 | digraph MyGraph { |
HTML使我们可以创建更复杂的节点,这些节点可以分为多个部分。可以在图中独立地引用每个部分:
1 | digraph MyGraph { |
只有HTML的一个子集可用于创建节点,并且规则非常严格。为了使节点正确显示,我们需要将设置shape
为plaintext
。
需要注意的另一件事是port
属性,它使我们可以使用冒号(a:a1
)来引用该特定单元格。
我们可以设置HTML节点的样式,但只能使用HTML的子集:
1 | digraph MyGraph { |
有时我们想为节点使用指定图标,这可以通过image
属性来完成:
1 | digraph MyGraph { |
等级是最难理解的事情之一,因为它们会改变渲染引擎的工作方式。在这里,我将介绍一些我认为有用的基本知识。
图表通常会从上到下呈现:
1 | digraph MyGraph { |
使用rankdir
属性,我们可以从左到右渲染它:
1 | digraph MyGraph { |
排名还可以用于强制一个节点与另一个节点处于同一级别:
1 | digraph MyGraph { |
在上面的示例中,我们用于rank=same
将node c
与node 对齐b
。
该rankdir
属性是全局属性,因此无法在集群内部更改,但是使用rank
我们可以模拟LR
集群内部的方向:
1 | digraph MyGraph { |
我们可以结合rank
使用constraint=false
以创建更紧凑的图形:
1 | digraph MyGraph { |
等级还可以用于指定每个节点之间的距离:
1 | digraph MyGraph { |
其缺省值ranksep
是.5
。
在这篇文章中,我们学习了如何使用 Graphviz 基于声明性语言生成图。这使我在将来更容易绘制架构图并对其进行修改。
我介绍了我认为对于日常使用最重要的功能,但是坦率地说,很多功能我仍还不了解。
]]>转载来源:https://linuxops.org/blog/python/prettytable.html
最近在用python写一个小工具,这个工具主要就是用来管理各种资源的信息,比如阿里云的ECS等信息,因为我工作的电脑使用的是LINUX,所以就想着用python写一个命令行的管理工具,基本的功能就是同步阿里云的资源的信息到数据库,然后可以使用命令行查询。
因为信息是展现在命令行中的,众所周知,命令行展现复杂的文本看起来着实累人,于是就想着能像表格那样展示,那看起来就舒服多了。
prettytable库就是这么一个工具,prettytable可以打印出美观的表格,并且对中文支持相当好(如果有试图自己实现打印表格,你就应该知道处理中文是多么的麻烦)
说明:本文使用Markdown语法编写,为了展示方便,以及复制方便,所以本文中没有使用截图,因为格式控制的问题,文章中的运行结果会出现一些分割线的偏移,在终端中呈现并此问题,请各位手动去操作验证。
prettytable并非python的内置库,通过 pip install prettytable
即可安装。
我们先来看一个示例:
1 | #!/usr/bin/python |
以上示例运行结果如下:
1 | linuxops@deepin:~$ python p.py |
在以上的示例中,我们通过form
导入了表格库。 table
实例化了一个表格库,并且添加了['编号','云编号','名称','IP地址']
为表头,如果没有添加表头,那么会以默认的Field+编号显示,例如:
1 | +---------+----------+----------+------------+ |
所以为更直观看出每一列的意义,还是要添加表头的。
prettytable提供了多种的添加数据的方式,最常用的应该就是按行按列添加数据了。
在上面简单的示例中,我们就是按行添加数据的。
添加的数据必须要是列表的形式,而且数据的列表长度要和表头的长度一样。在实际的使用中,我们应该要关注到添加的数据是否和表头对应,这一点很重要。
看下面的示例:
1 | #!/usr/bin/python |
运行结果如下:
1 | +-------+--------+------------+ |
以上示例中,我们通过add_column
来按列添加数据,按列添加数据不需要在实例化表格的时候制定表头,它的表头是在添加列的时候指定的。
table.add_column('项目', ['编号','云编号','名称','IP地址'])
这一行代码为例,项目
指定了这个列的表头名为"项目",['编号','云编号','名称','IP地址']
为列的值,同样为列表。
PrettyTable不仅提供了手动按行按列添加数据,也支持直接从csv文件中读取数据。
1 | #!/usr/bin/python |
如果要读取cvs文件数据,必须要先导入from_csv
,否则无法运行。上面的示例运行结果如下:
1 | +------+----------+----------+------------+ |
csv文件不能通过xls直接重命名得到,会报错。如果是xls文件,请用另存为csv获得csv文件
从数据库查询出来的数据可以直接导入到表格打印,下面的例子使用了sqlite3
,如果使用的是mysql也是一样的,只要能查询到数据就能导入到表格中
1 | #!/usr/bin/python |
运行结果如下:
1 | +------+----------+----------+------------+ |
支持从html的表格中导入,请看下面这个例子:
1 | #!/usr/bin/python |
运行结果如下:
1 | +------+----------+----------+------------+ |
如上示例中,我们可以导入html的表格,但是不一样的地方是print
语句,使用html表格导入数据的时候print的必须是列表中的第一个元素,否则有可能会报[<prettytable.PrettyTable object at 0x7fa87feba590>]
这样的错误。
这是因为table
并不是PrettyTable对象,而是包含单个PrettyTable对象的列表,它通过解析html而来,所以无法直接打印table
,而需要打印table[0]
正如支持多种输入一样,表格的输出也支持多种格式,我们在上面中的例子中已经使用了print的方式输出,这是一种常用的输出方式。
直接通过print
打印出表格。这种方式打印出的表格会带边框。
print(table.get_html_string())
可以打印出html标签的表格。
在上面的例子中,使用print(table.get_html_string())
会打印出如下结果:
1 | <table> |
prettytable在创建表格之后,你依然可以有选择的输出某些特定的行.
print table.get_string(fields=["编号", "IP地址"])
可以输出指定的列
通过print(table.get_string(start = 0, end = 2))
的可以打印出指定的列,当然start
和end
参数让我可以自由控制显示区间。当然区间中包含start
不包含end
,是不是很熟悉这样的用法?
根据输出指定行列的功能,我们可以同时指定行和列来输出,这里就不说明了。
从上面的输出区间,我们做一个大胆的假设,既然区间包含start
不包含end
这种规则和切片的一样,我们可以不可通过切片来生成一个新的表格然后将其打印。
事实上是可以的。
1 | new_table = table[0:2] |
如上代码段中,我们就可以打印出0到1行共2行的表格,python的切片功能异常强大,配合切片我们可以自由的输入任意的行。
有时候我们需要对输出的表格进行排序,使用print table.get_string(sortby="编号", reversesort=True)
就可以对表格进行排序,其中reversesort
指定了是否倒序排序,默认为False
,即默认正序列排序。
sortby
指定了排序的字段。
通过set_style()
可以设置表格样式,prettytable内置了多种的样式个人觉得MSWORD_FRIENDLY
,PLAIN_COLUMNS
,DEFAULT
这三种样式看起来比较清爽,在终端下显示表格本来看起就很累,再加上一下花里胡哨的东西看起来就更累。
除了以上推荐的三种样式以外,还有一种样式不得不说,那就是RANDOM
,这是一种随机的样式,每一次打印都会在内置的样式中随机选择一个,比较好玩。
具体内置了几种样式,请各位参考官网完整自己尝试输出看看。
1 | #!/usr/bin/python |
除了内置的样式以外,PrettyTable也提供了用户自定义,例如对齐方式,数字输出格式,边框连接符等等
align
提供了用户设置对齐的方式,值有l
,r
,c
方便代表左对齐,右对齐和居中 如果不设置,默认居中对齐。
在PrettyTable中,边框由三个部分组成,横边框,竖边框,和边框连接符(横竖交叉的链接符号)
如下示例:
1 | #!/usr/bin/python |
table.junction_char
控制边框连接符
table.horizontal_char
控制横边框符号
table.vertical_char
控制竖边框符号
上例运行如下:
1 | $++++++$++++++++++$++++++++++$++++++++++++$ |
以上简单介绍了表格常用的一些样式设置,具体的请参考官方网站。
https://github.com/jazzband/prettytable
https://code.google.com/archive/p/prettytable/wikis/Tutorial.wiki
]]>有很多人抱怨,把这个特性命名为“装饰器”不好。主要原因是,这个名称与 GoF 书使用的不一致。装饰器这个名称可能更适合在编译器领域使用,因为它会遍历并注解语法书。
—“PEP 318 — Decorators for Functions and Methods”
本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。
讨论内容:
nonlocal
能解决什么问题装饰器是可调用的对象,其参数是一个函数(被装饰的函数)。
装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
装饰器两大特性:
1 | # 装饰器通常会把函数替换成另一个函数 |
装饰器在导入时(模块加载时)立即执行
1 | # registration.py |
通过上面的例子,强调装饰器函数在导入模块式立即执行,而普通函数在被调用时运行。导入时和运行时的区别。
上面的装饰器会原封不动地返回被装饰的函数,而不一定会对函数做修改。
这种装饰器叫注册装饰器,通过使用它来中心化地注册函数,例如把 URL 模式映射到生成 HTTP 响应的函数上的注册处。
1 | promos = [] |
1 | # 比较两个例子 |
Python 假定在函数体内部的变量为局部变量。如果未在局部变量中找到,会逐级向上查找变量。
如果想在函数中赋值时让解释器把 b 当做全局变量,用 global 关键字
1 | def f3(a): |
闭包和匿名函数常被弄混。只有涉及到嵌套函数时才有闭包问题。
闭包指延伸了作用域的函数,其中包含函数定义体中的引用,但非定义体中定义的非全局变量。和函数是否匿名无关。关键是能访问定义体之外定义的非全局变量。
1 | class Averager(): |
在 averager 函数中,series 是自由变量,指未在本地作用域绑定的变量。
通过 __code__.co_freevars
__closure__
查看自由变量和闭包
闭包是一种函数,保留定义函数时存在的自由变量的绑定。调用函数时,虽然定义作用域不可用了,但仍能使用那些绑定
只有嵌套在其他函数中的函数才可能需要处理不在全局作用域的外部变量
下面一个例子有缺陷:
1 | def make_averager(): |
注意 count, total 的赋值语句使它们成为局部变量,在赋值是会隐式创建局部变量,这样它们就不是自由变量了,因此不会保存在闭包中。
为解决这个问题,Python3 引入了 nonlocal 声明,作用是吧变量标记为自由变量,即使在函数中为变量新值了,也会变成自由变量。在闭包中的绑定也会更新
对于没有 nonlocal 的 Python2 PEP3104
1 | def make_averager(): |
1 | import time |
装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装装饰函数本该返回的值,同时做一些额外操作
1 | factorial.__name__ |
上述实现的 clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的 __name__
, __doc__
属性
functools.wraps 装饰器把相关属性从 func 复制到 clocked 中,还能正确处理关键字函数
1 | import time |
Python 内置的三个装饰器分别为 property
, classmethod
和 staticmethod
.
但 Python 内置的库中,有两个装饰器很常用,分别为 functools.lru_cache
和 functools.singledispatch
.
1 |
|
Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的辩题,也无法使用不同的方式处理不同的数据类型。
一种常见的方法是把 htmlize 编程一个分派函数,使用 if-elif-else 分别调用专门的函数。但这样不便于模块的拓展,而且臃肿
functoos.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。
使用 functoos.singledispatch 装饰的普通函数会变成反函数。
1 | # 生成 HTML 显示不同类型的 python 对象 |
只要可能,注册的专门函数应该处理抽象基类(numbers.Integral, abc.MutableSequence), 不要处理具体实现(int,list)
这样代码支持的兼容类型更广泛。
使用 singledispatch 可以在系统的任何地方和任何模块注册专门函数。
1 |
|
为了方便理解,可以把参数化装饰器看成一个函数:这个函数接受任意参数,返回一个装饰器(参数为 func 的另一个函数)。
1 | # 参数化的注册装饰器 |
为 clock 装饰器添加一个功能,让用户传入一个格式化字符串,控制被装饰函数的输出。
1 | import time |
本节先编写了一个没有内部函数的 @register 装饰器。 然后实现了有两层嵌套函数的参数化装饰器 @clock()
参数化装饰器基本上设计至少两层嵌套函数。
标准库 functools 提供两个非常重要的装饰器 @lru_cache() 和 @singledispatch
理解装饰器,需要区分导入时、运行时、变量作用域,闭包等。
推荐阅读:decorator 第三方库
1 |
不管别人怎么说或怎么想,我从未觉得 Python 受到来自函数式语言的太多影响。我非常熟悉命令式语言,如 C 和 Algol 68,虽然我把函数定为一等对象,但是我并不把 Python 当作函数式编程语言。
—— Guido van Rossum: Python 仁慈的独裁者
在 Python 中,函数是一等对象。
编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
1 | def factorial(n): |
通过 type(factorial)
可以看到 function
是一种类型,或者说,函数也是对象,可以通过__doc__
去访问它的属性。
那么作为对象的函数,也能作为参数被传递。函数式风格编程也基于此
1 | fact = factorial |
输入或者输出是函数的即为高阶函数(higher order function)。例如:map
, sorted
。
1 | fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana'] |
函数式语言通常提供 map
、filter
和 reduce
三个高阶函数。
在 Python 中引入了列表推导和生成式表达式,可以替代它们且更容易阅读。
1 | list(map(fact, range(6))) |
map
和 filter
返回生成器,可用生成器表达式替代reduce
常用求和,目前最好使用 sum
替代
1 | from functools import reduce |
sum
和 reduce
把操作连续应用在序列元素上,得到返回值
all(iterable)
, any(iterable)
也是规约函数
all(iterable)
: 每个元素为真,返回真any(iterable)
: 存在一个元素为真,返回真Python 支持 lambda 表达式。 它是函数对象,在句法上被限制只能用存表达式。
参数列表中最适合使用匿名函数。
1 | # 根据单词末尾字符排序 |
Python 的可调用对象
def
或 lambda
创建len
或 time.strfttime
dict.get
__new__
创建实例,再对实例运行 __init__
方法__call__
方法,则实例可以作为函数调用判断对象是否能调用,使用内置的 callable()
函数
1 | abs, str, 13 |
任何 Python 对象都可以表现得像函数,只需实现实例方法 __call__
1 | import random |
实现 __call__
方法的类是创建函数类对象的简便方式。
函数类对象有自己的状态,即为实例变量。装饰器函数也可以有.
内省(introspection)可以查看函数内部的细节,函数有许多属性。使用 dir 函数可以查看,或使用 code 属性
1 | dir(factorial) |
1 | # eg:5-9 |
函数属性说明
// 插入表格
本节讨论 python 参数处理机制。py3 提供了仅限关键字参数(keyword-only argument)
调用函数使用 * 和 ** 展开可迭代对象。
1 | def tag(name, *content, cls=None, **attrs): |
cls
参数只能通过关键字指定,而不能通过位置参数指定。
定义函数时若只想定仅限关键字参数,要把它放在带有 * 参数后面,如果不想支持数量不定的位置参数,但支持 keyowrd-only, 在函数签名中放一个 *
1 | def f(a, *, b): |
上面提到,函数内省可以查看函数内部信息,通过 HTTP 微框架 Bobo 作为例子来看下
1 | # eg: 5-12 |
Bobo 如何知道函数需要哪个参数呢?
函数对象有 __defaults__
属性,其值为一个元祖,保存着位置参数和关键字参数的默认值。
keyword-only 参数默认值保存在 __kwdefaults__
属性中。
参数的名称在 __code__
属性中,其值为 code 对象的引用。
1 | def clip(text, max_len=80): |
函数签名信息,参数和默认值是分开的。可以使用 inspect 模块提取这些信息
1 | from inspect import signature |
kind 属性值在 _Parameterkind
类中,列举如下:
inspect.Signature 有 bind
方法,可以把任意个参数绑定在签名中的形参上。
框架可以使用此方法在调用函数前验证参数
1 | import inspect |
框架和 IDE 工具可以使用这些信息验证代码
各个参数可以在 : 后添加注解表达式。
参数有默认值,注解放在参数名和 = 号之间,注解返回值在函数声明末尾添加 -> 和表达式
注解不会做任何处理,只存储在函数 __annotations__
属性中。
注解只是元数据,可以供 IDE,框架和装饰器等工具使用
inspect.signature()
函数知道怎么提取注解
1 | def clip(text: str, max_len: 'int > 0' = 80) -> str: |
1 | from inspect import signature |
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 | from functools import reduce |
还有一类函数,能替代从序列中取出或读取元素属性的 lambda 表达式。如 itemgetter
,attrgetter
1 | metro_data = [ |
itemgetter 使用 []
运算符,因为它不仅支持序列,还支持映射和任何实现 __getitem__
的类
attrgetter 作用相似,它创建的函数根据名称提取对象的属性。包含 .
的,会进入到嵌套对象提取属性
1 | from collections import namedtuple |
1 | import operator |
operator 模块的函数可以通过 dir(operator)
查看。
介绍 methodcaller, 它的作用与前两个函数相似,它创建的函数会在对象调用参数指定的方法
1 | from operator import methodcaller |
functools
最常用的函数有 reduce
,之前已经介绍过。余下函数中最有用的是 partial
及其变体 partialmethod
它的作用是:把原函数某些参数固定。
partial
第一个函数是可调用对象,后面跟任意个位置参数和关键字参数
1 | from operator import mul |
functoos.partialmethod 作用与 partial 一样,不过适用于处理方法的
探讨 Python 函数的一等特性。意味着可以把函数赋值给变量,传入其他函数,存储于数据结构中,以及访问函数属性。
高阶函数是函数式编程的重要组成。
Python
的可调用对象: 7种
函数及其注解有丰富的特性。可通过 inspect
模块读取
最后介绍了 operator
模块中的一些函数,可以替换掉功能有限的 lambda 表达式。
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!(命名空间非常有用,我们应当多加利用)
]]>dumpdata
命令:它可以用来备份(导出)模型实例或整个数据库
1 | ./manage.py dumpdata --help |
基础数据库导出
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 | [ |
dumpdata —format
默认输出格式为 JSON。使用 —format
可以指定输出格式
1 | ./manage.py dumpdata auth.user --indent 2 --format xml > user.xml |
这会输出 xml 文件
1 |
|
loaddata
命令
用来导入 fixtures(dumpdata 导出的数据)到数据库
1 | ./manage.py loaddata user.json |
这会导入 user.json 里的内容到数据库
恢复 fresh database
当你通过 dumpdata 命令备份整个数据库时,它将备份所有数据表。若使用 dump 文件导入到另外的 Django 项目,会导致 IntegrityError
。
可以通过备份时加入选项 —exclude
contenttypes 和 auth.permissions 数据表修复此问题
1 | ./manage.py dumpdata --exclude auth.permission --exclude contenttypes > db.json |
现在再用 loaddata 命令导入 fresh dababase
1 | ./manage.py loaddata db.json |
代码编排
缩进。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 | # -*- coding: utf-8 -*- |
空格的使用
总体原则,避免不必要的空格。
func(1)
。list[2]
。命名规范
总体原则,新编代码必须按下面命名风格进行,现有库的编码尽量保持风格。
l
,大写字母O
等容易混淆的字母。CapWords
的方式,模块内部使用的类采用_CapWords的方式。_注释
总体原则,错误的注释不如没有注释。所以当一段代码发生变化时,第一件事就是要修改注释!
针对团队情况(是否国际化),注释倾向使用英文,最好是完整的句子,首字母大写,句后要有结束符,结束符后跟两个空格,开始下一句。如果是短语,可以省略结束符。
1 | # Description : Module config. |
文档描述
1 为所有的共有模块、函数、类、方法写docstrings;非共有的没有必要,但是可以写注释(在def的下一行)。
2 如果docstring要换行,参考如下例子,详见PEP 257
1 | """Return a foobang |
编码建议
编码中考虑到其他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 | try: |
使用startswith() and endswith()代替切片进行序列前缀或后缀的检查。比如
1 | Yes: if foo.startswith(‘bar’):优于 |
判断序列空或不空,有如下规则
1 | Yes: if not seq: |
字符串不要以空格收尾。
二进制数据判断使用 if boolvalue
的方式。
Django 项目本身可以通过 django-admin
或者直接运行 python manage.py ARGS
来进行脚手架生成。但是生成的项目框架层次不算太好。
首先生成一个 Django 项目:
1 | django-admin startproject backend |
生成的项目框架如下:
1 | backend |
其中的两个 backend
分别表示项目,以及 app 全局配置
建立文件夹 apps
用来放置应用,把内层 backend
改为 conf
1 | backend |
注意这里需要配置以下几个文件:
1 | # manage.py |
1 | # settings.py |
现在可以测试 python manage.py runserver
是否可以起来。
接下来新建 Apps
1 | mkdir apps/login |
注册 app
1 | # settings.py |
导入 URL
1 | ... |
现在一个基本的项目结构就建立好了。
1 | backend |
相比起来层次更清晰,而且也更适合用作前后端分离的命名
]]>]]>对递归的理解的要点主要在于放弃!
放弃你对于理解和跟踪递归全程的企图,只理解递归两层之间的交接,以及递归终结的条件。
想象你来到某个热带丛林,意外发现了十层之高的汉诺塔。正当你苦苦思索如何搬动它时,林中出来一个土著,毛遂自荐要帮你搬塔。他名叫二傻,戴着一个草帽,草帽上有一个2字,号称会把一到二号盘搬到任意柱。
你灵机一动,问道:“你该不会有个兄弟叫三傻吧?”
“对对,老爷你咋知道的?他会搬一到三号盘。“
”那你去把他叫来,我不需要你了。“
于是三傻来了,他也带着个草帽,上面有个3字。你说:”三傻,你帮我把头三个盘子移到c柱吧。“
三傻沉吟了一会,走进树林,你听见他大叫:”二傻,出来帮我把头两个盘子搬到C!“由于天气炎热你开始打瞌睡。朦胧中你没看见二傻是怎么工作的,二傻干完以后,走入林中大叫一声:“老三,我干完了!”
三傻出来,把三号盘从A搬到B,然后又去叫二傻:“老二,帮我把头两个盘子搬回A!”
余下的我就不多说了,总之三傻其实只搬三号盘,其他叫二傻出来干。最后一步是三傻把三号盘搬到C,然后呼叫二傻来把头两个盘子搬回C
事情完了之后你把三傻叫来,对他说:“其实你不知道怎么具体一步一步把三个盘子搬到C,是吧?”
三傻不解地说:“我不是把任务干完了?”
你说:“可你其实叫你兄弟二傻干了大部分工作呀?”
三傻说:“我外包给他和你屁相干?”
你问到:“二傻是不是也外包给了谁?“
三傻笑了:“这跟我有屁相干?”
你苦苦思索了一夜,第二天,你走入林中大叫:“十傻,你在哪?”
一个头上带着10号草帽的人,十傻,应声而出:“老爷,你有什么事?”
“我要你帮把1到10号盘子搬到C柱“
“好的,老爷。“十傻转身就向林内走。
“慢着,你该不是回去叫你兄弟九傻吧“
“老爷你怎么知道的?“
“所以你使唤他把头九个盘子搬过来搬过去,你只要搬几次十号盘就好了,对吗?“
“对呀!“
“你知不知道他是怎么干的?“
“这和我有屁相干?“
你叹了一口气,决定放弃。十傻开始干活。树林里充满了此起彼伏的叫声:“九傻,来一下!“ “老八,到你了!““五傻!。。。“”三傻!。。。“”大傻!“
你注意到大傻从不叫人,但是大傻的工作也最简单,他只是把一号盘搬来搬去。
若干年后,工作结束了。十傻来到你面前。你问十傻:“是谁教给你们这么干活的?“
十傻说:“我爸爸。他给我留了这张纸条。”
他从口袋里掏出一张小纸条,上面写着:“照你帽子的号码搬盘子到目标柱。如果有盘子压住你,叫你上面一位哥哥把他搬走。如果有盘子占住你要去的柱子,叫你哥哥把它搬到不碍事的地方。等你的盘子搬到了目标,叫你哥哥把该压在你上面的盘子搬回到你上头。“
你不解地问:“那大傻没有哥哥怎么办?“
十傻笑了:“他只管一号盘,所以永远不会碰到那两个‘如果’,也没有盘子该压在一号上啊。”
但这时他忽然变了颜色,好像泄漏了巨大的机密。他惊慌地看了你一眼,飞快地逃入树林。
第二天,你到树林里去搜寻这十兄弟。他们已经不知去向。你找到了一个小屋,只容一个人居住,但是屋里有十顶草帽,写着一到十号的号码。
作者:Fireman A
链接:https://www.zhihu.com/question/24385418/answer/257751077
字典这个数据结构活跃在所有 Python 程序的背后,即便你的源码里并没有直接用到它。
——A. M. Kuchling
dict
是 Python 语言的基石。
可散列对象需要实现 __hash__
和 __eq__
函数。
如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
collections.abc 模块中有 Mapping 和 MutableMapping 两个抽象基类,起作用是为 dict 和其他类似的类型定义形式接口。
//pic
但非抽象映射类型一般不会直接继承这些抽象基类,而是直接对 dict 或 collections.User.Dict 进行扩展。
这些抽象基类的主要作用是作为形式化的文档,以及跟 isinstance 一起被用来判定某个数据是否为广义上的映射类型。
1 | my_dict = {} |
True
用 instance 而不是用 type 是用来避免参数可能不是 dict 而是其他的映射类型
标准库的所有映射类型都是利用 dict 实现。
什么是可散列的数据类型?
字典的提供了多种构造方法
link
1 | # 字典提供了很多种构造方法 |
True
字典推导(dictcomp)可以从任何以键值对为元素的可迭代对象构建出字典
1 | DIAL_CODES = [ |
{'China': 86, 'India': 91, 'United States': 1}
dict、defaultdict、OrderedDict 的常见方法,后两个数据类型是 dict 的变种,位于 collections 模块内。
setdefault 处理找不到的键
d[k] 无法找到正确的键时,会抛出异常。
用 d.get(k, default) 来代替 d[k], 可以对找不到的键设置默认返回值。
1 | """ |
1 | $ python index0.py zen.txt |
使用 dict.setdefault
1 | """ |
某个键不存在时,希望读取时能得到一个默认值,有两个方式:
1 | """ |
defaultdict 里的 default_factory 只在 getitem 里调用。
实际上,上面的机制是通过特殊方法 missing 支持的。
如果 dict 继承类提供了 missing 方法,且 getitem 遇到找不到键的情况是会自动调用它,而不是抛出异常
1 | class StrKeyDict0(dict): # <1> |
TwoFourTwoFour'N/A'
此节总结了标准库 collections 模块中,除了 defaultdict 之外的不同映射类型
collections.OrderedDict
collections.ChainMap
容纳多个不同的映射对象,然后在进行键查找操作时会从前到后逐一查找,直到被找到为止
collections.Counter
collections.UserDict
dict 的纯 Python 实现,让用户集成写子类的
1 | # UserDict |
1 | # MyppingProxyType 用于构建 Mapping 的只读实例 |
1 | # set 的操作 |
除空集之外,集合的字面量——{1}
、{1, 2}
,等等——看起来跟它的数学形式一模一样。如果是空集,那么必须写成 set()
的形式,否则它会变成一个 dict
.
跟 list
一样,字面量句法会比 set
构造方法要更快且更易读。
集合和字典采用散列表来实现:
hash
, 根据 hash 的某几位(取决于散列表的大小)找到元素后,将该元素与 key 进行比较这样导致的后果:
hash
函数;__eq__
判断相等性;a == b
, 则必须有 hash(a) == hash(b)
。注:所有由用户自定义的对象都是可散列的,因为他们的散列值由 id() 来获取,而且它们都是不相等的。
由于字典使用散列表实现,所以字典的空间效率低下。使用 tuple
代替 dict
可以有效降低空间消费。
不过:内存太便宜了,不到万不得已也不要开始考虑这种优化方式,因为优化往往是可维护性的对立面。
往字典中添加键时,如果有散列表扩张的情况发生,则已有键的顺序也会发生改变。所以,不应该在迭代字典的过程各种对字典进行更改。
1 | # 字典中就键的顺序取决于添加顺序 |
Docker Compose 是Docker 官方编排(Orchestration)项目之一,负责快速的部署分布式应用。
Compose 定位是 「定义和运行多个Docker 容器的应用(Defining and running multi-container Docker applications)」
其前身是开源项目Fig。其代码目前在https://github.com/docker/compose 上开源。
1 | pip install -U docker-compose |
或
1 | sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose |
Dockerfile
1 | FROM python:3.7-slim |
app.py
1 | from flask import Flask |
docker-compose.yml
1 | version: "3" |
执行 docker-compose build
可生成镜像
执行 docker-compose up
启动容器运行
浏览器访问
Docker Machine 是Docker
官方编排(Orchestration)项目之一,负责在多种平台上快速安装Docker
环境。
使用 virtualbox
类型的驱动,创建一台Docker
主机,命名为 manager
。
1 | docker-machine create -d virtualbox manager |
可以在创建时加上如下参数,来配置主机或者主机上的Docker
。
1 | --engine-opt dns=114.114.114.114 配置Docker 的默认DNS |
更多参数请使用 docker-machine create —help
命令查看。
docker-machine ls
查看主机
docker-machine env manager
查看环境变量
切换 docker
主机 manager
为操作对象
1 | eval $(docker-machine env manager) |
或者可以 ssh
登录到 docker
主机
1 | docker-machine ssh manager |
Swarm
是使用SwarmKit
构建的Docker
引擎内置(原生)的集群管理和编排工具。
初始化集群
在上节介绍 docker-machine
的时候,我们创建了manager
节点,而初始化集群需要在管理节点内执行
docker swarm init --advertise-addr=IP_ADDR
现在来创建两个工作节点worker1
, worker2
并加入集群
1 | docker-machine create -d virtualbox worker1 |
同理worker2
节点
进入manager
节点执行
docker node ls
由此,我们就得到了一个最小化的集群。
docker stack deploy –c docker-compose.yml
后,在docker ps
中无法看到端口映射?关于docker swarm mode 部署后端口的问题,可以使用docker service ls
来查看端口是否正确暴露,因为此时是通过service来暴露的,并不是直接在container上暴露,所以此时用docker ps
是看不到的,但暴露的端口依旧可以访问,这样实现和k8s里的service实现是有些相似的。
docker-compose -f docker-compose.yml up -d
,返回1 | Pulling myapp (friendlyhello:v2)... |
compose文件中如果已经build过,就用image直接指定这个image,注释掉build的指令。如果没有build过,就放开build指令,执行docker-compose
的build它,当然也可以使用docker build
来构建它。因为这一块在上一章节已经提到过,所以对于部分这次直接切入的同学可能会有疑惑。而到了docker stack时,已经不支持docker stack
来build它了,需要统一使用docker build来构建镜像。
1 | # 官方 |
1 | brew cask install docker |
查看 Docker 版本
版本信息:docker --version
配置信息: docker info
help 信息: docker --help
运行第一个 Docker 镜像
docker run hello-world
Docker CLI 指令分为两类,一类是 Management Commands,一类是镜像与容器 Commands。
你可以通过 docker –help
进行查看。
Docker 的命令格式为:
docker [OPTIONS] COMMAND
这里我们选取常用的几个命令进行示例:
docker pull
: 从镜像仓库中拉取镜像
1 | # 从镜像仓库中拉取 Ubuntu 镜像 |
Docker-CLI 与 Linux 语法具有相似性,例如:
1 | # 列出所有镜像 |
如果你有 Linux 基础,那么相信对于 Docker-CLI 上手还是比较容易的。
docker run -d -P daocloud.io/daocloud/dao-2048
-d 表示容器启动后在后台运行
用 docker ps 查看容器运行状态如图:
看到端口映射关系 0.0.0.0:32768->80。指宿主机的 32768 端口映射到容器的 80 端口
用浏览器打开 localhost:32768
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 文件
分别执行
中间打印输出
Docker 生成 container时会生成一个唯一的 container-id,在上图中 stop 命令用到了 container-id。当然,你可以使用 docker tag 命令对 container 进行重命名。
-p 4000:80 : 指的是从宿主机端口 4000 映射到容器端口 80
现在打开浏览器访问 localhost:4000
:
你可能注意到了,之前提到的几个操作可以无差别地应用于文本、列表和表格上。
我们把文本、列表和表格叫作数据火车……FOR 命令通常能作用于数据火车上。
——Geurts、Meertens 和 Pemberton
ABC Programmer’s Handbook
list
、tuple
和 collections.deque
这些序列能存放不同类型的数据。str
、bytes
、bytearray
、memoryview
和 array.array
,这类序列只能容纳一种类型。容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
序列类型还能按照能否被修改来分类。
list
、bytearray
、array.array
、collections.deque
和 memoryview
。tuple
、str
和 bytes
列表推导是构建列表(list)的快捷方式,生成器表达式用来穿件其他任何类型的序列。
1 | # 比较两段代码 |
列表推导能够提升可读性。
只用列表推导来创建新的列表,并尽量保持简短(不要超过一行)
1 | symbols = 'abcde' |
[101][101]
1 | colors = ['black', 'white'] |
[('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 | # symbols = 'abcde' |
1 | colors = ['black', 'white'] |
black Sblack Mblack Lwhite Swhite Mwhite L
1 | # LA 国际机场经纬度 |
BBACE342567ESPXDA205856USA31195855USABBAESP
1 | lax_coordinates = (33.9425, -118.408056) |
33.9425-118.408056
1 | a = 3 |
43
*
运算符,把一个可迭代对象拆开作为函数参数1 | divmod(20, 8) |
24
_ 用作占位符,可以用来处理不需要的数据
1 | import os |
id_rsa.pub
*
处理省下的元素1 | a, b, *rest = range(5) |
0 1 [2, 3, 4]0 1 [2]0 1 []0 [1, 2] 3 4[0, 1] 2 3 4
1 | metro_areas = [ |
| lat. | long. Mexico City | 19.4333 | -99.1333New York-Newark | 40.8086 | -74.0204Sao Paulo | -23.5478 | -46.6358
将元祖作为记录仍缺少一个功能:字段命名
collections.namedtuple
是一个工厂函数,用来构建带字段名的元祖和一个有名字的
namedtuple 构建的类的实例所消耗的内存和元祖是一样的,因为字段名都存在对应的类里。
实例和普通的对象实例小一点,因为 Python 不会用__dict__
存放实例的属性
1 | from collections import namedtuple |
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 | print(City._fields) |
('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)这类序列类型都支持切片操作
my_list[:x]
my_list[x:]
1 | ### 对对象进行切片 |
1 | s = 'bicycle' |
byeelcycibeccb
[]
运算符可以使用以逗号分开的多个索引或切片。
如 a[i, j]
,a[m:n, k:1]
得到二维切片
要正确处理[]
运算符,对象的特殊方法 __getitem__
,__setitem__
需要以元祖的形式来接受 a[i, j]
的索引。
切片放在赋值语句左边,或作为 del 操作对象,可以对序列进行嫁接、切除或就地修改
1 | l = list(range(10)) |
[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 | l = [1, 2, 3] |
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]abcdabcdabcdabcdabcd
a * n,如果在序列 a 中存在对其他可变变量的引用的话,得到的序列中包含的是 n 个对指向同一地址的引用
1 | board = [['_'] * 3 for i in range(3)] |
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']][['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']][['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
+=
背后的特殊方法是 __iadd__
方法,没有则退一步调用 __add__
*=
的特殊方法是 __imul__
1 | l = [1, 2, 3] |
4534358344[1, 2, 3, 1, 2, 3]45343583444536971408(1, 2, 3, 1, 2, 3)4546754024
bisect 模块有两个主要函数:
bisect(haystack, needle) 默认为升序,haystack 需要保持有序。
使用方法:
bisect(index, needle) 查找位置 index,再使用 haystack.insert(index, needle) 插入新值
也可以用 insort 来一步到位,且后者速度更快
1 | # BEGIN 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
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。
array.tofile
和 fromfile
可以将数组以二进制格式写入文件,速度要比写入文本文件快很多,文件的体积也小。
另外一个快速序列化数字类型的方法是使用 pickle(https://docs.python.org/3/library/pickle.html)模块。pickle.dump 处理浮点数组的速度几乎跟array.tofile 一样快。不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。
array 具有 type code
来表示数组类型:具体可见 array 文档.
memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。
1 | import array |
collections.deque
是比 list
效率更高,且线程安全的双向队列实现。
除了 collections 以外,以下 Python 标准库也有对队列的实现:
1 | *斜体*或_斜体_ |
斜体或_斜体_
粗体
加粗斜体
删除线
1 | # 一级标题 |
一级标题字号最大,依级递减。
Markdown 支持两种形式的链接语法: 行内式和参考式两种形式,行内式一般使用较多。
语法说明:[文字](链接)
[]里写链接文字,()里写链接地址, ()中的”“中可以为链接指定title属性,title属性可加可不加。title属性的效果是鼠标悬停在链接上会出现指定的 title文字。链接地址与链接标题前有一个空格。
1 | 欢迎来到[Django](https://docs.djangoproject.com/zh-hans/3.0/) |
欢迎来到Django
欢迎来到Django
参考式超链接一般用在学术论文上面,或者另一种情况,如果某一个链接在文章中多处使用,那么使用引用 的方式创建链接将非常好,它可以让你对链接进行统一的管理。
语法说明: [文字][链接文字]
参考式链接分为两部分,文中的写法 [链接文字][链接标记],在文本的任意位置添加[链接标记]:链接地址 “链接标题”,链接地址与链接标题前有一个空格。
如果链接文字本身可以做为链接标记,你也可以写成[链接文字][]
[链接文字]:链接地址的形式,见代码的最后一行。
1 | 我经常去的几个网站[Google][1],[技术博客1][2],[技术博客2][]。 |
我经常去的几个网站Google,Demi的随笔和技术空间,[Demi的随笔和技术空间][]。
我经常去的几个网站Google,技术博客1,[技术博客2][]。
语法说明:
Markdown 支持以比较简短的自动链接形式来处理网址和电子邮件信箱,只要是用<>包起来, Markdown 就会自动把它转成链接。一般网址的链接文字就和链接地址一样,例如:
1 | <http://example.com/> |
http://example.com/
address@example.com
网页中,锚点其实就是页内超链接,也就是链接本文档内部的某些元素,实现当前页面中的跳转。比如我这里写下一个锚点,点击回到目录,就能跳转到目录。 在目录中点击这一节,就能跳过来。还有下一节的注脚。这些根本上都是用锚点来实现的。
注意:
1 | ## 跳转测试{#index} |
使用 *,+,- 表示无序列表。
1 | - 无序列表项 一 |
有序列表则使用数字接着一个英文句点。
1 | 1. 有序列表项 一 |
语法说明:
定义型列表由名词和解释组成。一行写上定义,紧跟一行写上解释。解释的写法:紧跟一个缩进(Tab)
1 | 代码块 1 Markdown |
view
轻量级文本标记语言,可以转换成html,pdf等格式(左侧有一个可见的冒号和四个不可见的空格)
这是代码块的定义(左侧有一个可见的冒号和四个不可见的空格)
代码块(左侧有八个不可见的空格)
语法说明:
列表项目标记通常是放在最左边,但是其实也可以缩进,最多 3 个空格,项目标记后面则一定要接着至少一个空格或制表符。
1 | * 轻轻的我走了, 正如我轻轻的来; 我轻轻的招手, 作别西天的云彩。 |
view
语法说明:
列表项目可以包含多个段落,每个项目下的段落都必须缩进 4 个空格或是 1 个制表符(显示效果与代码一致):
1 | * 轻轻的我走了, 正如我轻轻的来; 我轻轻的招手, 作别西天的云彩。 |
view
轻轻的我走了, 正如我轻轻的来; 我轻轻的招手, 作别西天的云彩。
那河畔的金柳, 是夕阳中的新娘; 波光里的艳影, 在我的心头荡漾。
软泥上的青荇, 油油的在水底招摇; 在康河的柔波里, 我甘心做一条水草!
那榆荫下的一潭, 不是清泉, 是天上虹; 揉碎在浮藻间, 沉淀着彩虹似的梦。
寻梦?撑一支长篙, 向青草更青处漫溯; 满载一船星辉, 在星辉斑斓里放歌。
但我不能放歌, 悄悄是别离的笙箫; 夏虫也为我沉默, 沉默是今晚的康桥!
悄悄的我走了, 正如我悄悄的来; 我挥一挥衣袖, 不带走一片云彩。
语法说明:
如果要在列表项目内放进引用,那 > 就需要缩进:
1 | * 阅读的方法: |
view
打开书本。
打开电灯。
语法说明:
如果要放代码区块的话,该区块就需要缩进两次,也就是 8 个空格或是 2 个制表符:
在特殊情况下,项目列表很可能会不小心产生,像是下面这样的写法:
1 | 1986. What a great season. |
会显示成:
换句话说,也就是在行首出现数字-句点-空白,要避免这样的状况,你可以在句点前面加上反斜杠:
1 | 1986\. What a great season. |
才会正常显示成:
1986. What a great season.
语法说明:
引用需要在被引用的文本前加上>符号。
1 | > 这是一个有两段文字的引用, |
view
这是一个有两段文字的引用,
无意义的占行文字1.
无意义的占行文字2.
无意义的占行文字3.
无意义的占行文字4.
区块引用可以嵌套(例如:引用内的引用),只要根据层次加上不同数量的 > :
1 | >>> 请问 Markdwon 怎么用? - 小白 |
请问 Markdwon 怎么用? - 小白
自己看教程! - 愤青
教程在哪? - 小白
引用的区块内也可以使用其他的 Markdown 语法,包括标题、列表、代码区块等:
1 | > 1. 这是第一行列表项。 |
- 这是第一行列表项。
- 这是第二行列表项。
给出一些例子代码:
return shell_exec("echo $input | $markdown_script");
图片的创建方式与超链接相似,而且和超链接一样也有两种写法,行内式和参考式写法。
语法中图片Alt的意思是如果图片因为某些原因不能显示,就用定义的图片Alt文字来代替图片。 图片Title则和链接中的Title一样,表示鼠标悬停与图片上时出现的文字。 Alt 和 Title 都不是必须的,可以省略,但建议写上。
语法说明:![图片Alt](图片地址 “图片Title”)
1 | 美丽风景: |
view
美丽风景:
语法说明:
在文档要插入图片的地方写![图片Alt][标记]
在文档的最后写上[标记]:图片地址 “Title”
1 | ![美丽风景](https://yuhongjun.github.io/assets/media/scenery.jpeg "美丽风景") |
view
在段落中填写 {toc}
以显示全文内容的目录结构。
效果参见最上方的目录
语法说明:
在需要添加注脚的文字后加上脚注名字[^注脚名字],称为加注。 然后在文本的任意位置(一般在最后)添加脚注,脚注前必须有对应的脚注名字。
注意:经测试注脚与注脚之间必须空一行,不然会失效。成功后会发现,即使你没有把注脚写在文末,经Markdown转换后,也会自动归类到文章的最后。
1 | 使用 Markdown[^1]可以效率的书写文档, 直接转换成 HTML[^2], 你可以使用 Leanote[^Le] 编辑器进行书写。 |
view
使用 Markdown[1]可以效率的书写文档, 直接转换成 HTML[2], 你可以使用 Leanote[3] 编辑器进行书写。
注:脚注自动被搬运到最后面,请到文章末尾查看,并且脚注后方的链接可以直接跳转回到加注的地方。
$
表示行内公式:1 | 质能守恒方程可以用一个很简洁的方程式 $E=mc^2$ 来表达。 |
view
质能守恒方程可以用一个很简洁的方程式 来表达。
$$
表示整行公式:1 | $$\sum_{i=1}^n a_i=0$$ |
访问 MathJax 参考更多使用方法。
1 | st=>start: Start|past:>https://yuhongjun.github.io[blank] |
更多语法参考:流程图语法参考
语法说明:
不管是哪种方式,第一行为表头,第二行分隔表头和主体部分,第三行开始每一行为一个表格行。
列于列之间用管道符|隔开。原生方式的表格每一行的两边也要有管道符。
第二行还可以为不同的列指定对齐方向。默认为左对齐,在-右边加上:就右对齐。
1 | 学号|姓名|分数 |
2.原生方式写表格:
1 | |学号|姓名|分数| |
3.为表格第二列指定方向:
1 | 产品|价格 |
view
学号 | 姓名 | 分数 |
---|---|---|
小明 | 男 | 75 |
小红 | 女 | 79 |
小陆 | 男 | 92 |
2.原生方式写表格:
学号 | 姓名 | 分数 |
---|---|---|
小明 | 男 | 75 |
小红 | 女 | 79 |
小陆 | 男 | 92 |
3.为表格第二列指定方向:
产品 | 价格 |
---|---|
Leanote 高级账号 | 60元/年 |
Leanote 超级账号 | 120元/年 |
你可以在一行中用三个以上的星号、减号、底线来建立一个分隔线,行内不能有其他东西。你也可以在星号或是减号中间插入空格。下面每种写法都可以建立分隔线:
1 | * * * |
显示效果都一样
对于程序员来说这个功能是必不可少的,插入程序代码的方式有两种,一种是利用缩进(Tab), 另一种是利用 ` 符号(一般在ESC键下方)包裹代码。
语法说明:
code
这样的形式插入。code “
,具体看示例。注意: 缩进式插入前方必须有空行
1 | C语言里的函数 `scanf()` 怎么使用? |
view
C语言里的函数 scanf()
怎么使用?
缩进 4 个空格或是 1 个制表符
一个代码区块会一直持续到没有缩进的那一行(或是文件结尾)。
1 | #include <stdio.h> |
view
#include <stdio.h>int main(void){ printf("Hello world\n");}
1 | <!-- 用 ``` 或 ~~~ 包裹多行代码 --> |
在代码区块里面, & 、 < 和 > 会自动转成 HTML 实体,这样的方式让你非常容易使用 Markdown 插入范例用的 HTML 原始码,只需要复制贴上,剩下的 Markdown 都会帮你处理,例如:
第一个例子:
1 | <div class="footer"> |
view
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
块)1 | # 通过实现魔术方法,来让内置函数支持你的自定义对象 |
可以容易地获得一个纸牌对象
1 | beer_card = Card('7', 'diamonds') |
和标准 Python 集合类型一样,使用 len()
查看一叠纸牌有多少张
1 | deck = FrenchDeck() |
可选取特定一张纸牌,这是由 __getitem__
方法提供的
1 | # 实现 __getitem__ 以支持下标操作 |
随机抽取一张纸牌,使用 python 内置函数 random.choice
1 | from random import choice |
实现特殊方法的两个好处:
__getitem__
方法把 [] 操作交给了 self._cards
列表,deck 类自动支持切片操作
1 | deck[12::13] |
同时 deck 类支持迭代
1 | for card in deck: |
迭代通常是隐式的,如果一个集合没有实现 __contains__
方法,那么 in 运算符会顺序做一次迭代搜索。
1 | Card('Q', 'hearts') in deck |
False
进行排序,排序规则:
2 最小,A最大。花色 黑桃 > 红桃 > 方块 > 梅花
1 | card.rank |
'A'
1 | suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) |
FrenchDeck 继承了 object 类。通过 __len__
, __getitem__
方法,FrenchDeck和 Python 自有序列数据类型一样,可体现 Python 核心语言特性(如迭代和切片),
Python 支持的所有魔术方法,可以参见 Python 文档 Data Model 部分。
比较重要的一点:不要把 len
,str
等看成一个 Python 普通方法:由于这些操作的频繁程度非常高,所以 Python 对这些方法做了特殊的实现:它可以让 Python 的内置数据结构走后门以提高效率;但对于自定义的数据结构,又可以在对象上使用通用的接口来完成相应工作。但在代码编写者看来,len(deck)
和 len([1,2,3])
两个实现可能差之千里的操作,在 Python 语法层面上是高度一致的。
特殊方法的存在是为了被 Python 解释器调用
除非大量元编程,通常代码无需直接使用特殊方法
通过内置函数来使用特殊方法是最好的选择
实现一个二维向量(Vector)类
1 | from math import hypot |
Vector 类中 6 个方法(除 __init__
外)并不会在类自身的代码中调用。一般只有解释器会频繁调用这些方法
内置函数 repr, 通过 __repr__
特殊方法来得到一个对象的字符串表示形式。
通过 __add__
, __mul__
, 向量类能够操作 + 和 * 两个算数运算符。
运算符操作对象不发生改变,返回一个产生的新值
__bool__
,则调用 __len__
, 若返回 0,则 bool 返回 False“实用胜于纯粹“
The Zen of Python
为了让 Python 自带的数据结构走后门, CPython 会直接从结构体读取对象的长度,而不会调用方法.
这种处理方式在保持内置类型的效率和语言一致性保持了一个平衡.
__repr__
, __str__
译者按: Dockerfile 的语法非常简单,然而如何加快镜像构建速度,如何减少 Docker 镜像的大小却不是那么直观,需要积累实践经验。这篇博客可以帮助你快速掌握编写 Dockerfile 的技巧。
本文采用意译,版权归原作者所有
我已经使用 Docker 有一段时间了,其中编写 Dockerfile 是非常重要的一部分工作。在这篇博客中,我打算分享一些建议,帮助大家编写更好的 Dockerfile。
示例 Dockerfile 犯了几乎所有的错(当然我是故意的)。接下来,我会一步步优化它。假设我们需要使用 Docker 运行一个 Node.js 应用,下面就是它的 Dockerfile(CMD 指令太复杂了,所以我简化了,它是错误的,仅供参考)。
1 | FROM ubuntu |
构建镜像:
docker build -t wtf
构建镜像时,Docker 需要先准备context ,将所有需要的文件收集到进程中。默认的context包含 Dockerfile 目录中的所有文件,但是实际上,我们并不需要.git 目录,node_modules 目录等内容。 .dockerignore 的作用和语法类似于 .gitignore,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少 Docker 镜像的大小。示例如下:
1 | .git/ |
从技术角度讲,你可以在 Docker 容器中运行多个进程。你可以将数据库,前端,后端,ssh,supervisor 都运行在同一个 Docker 容器中。但是,这会让你非常痛苦:
因此,我建议大家为每个应用构建单独的 Docker 镜像,然后使用 Docker Compose 运行多个 Docker 容器。
现在,我从 Dockerfile 中删除一些不需要的安装包,另外,SSH 可以用docker exec替代。示例如下:
1 | FROM ubuntu |
Docker 镜像是分层的,下面这些知识点非常重要:
Docker 镜像类似于洋葱。它们都有很多层。为了修改内层,则需要将外面的层都删掉。记住这一点的话,其他内容就很好理解了。
现在,我们将所有的RUN指令合并为一个。同时把apt-get upgrade删除,因为它会使得镜像构建非常不确定(我们只需要依赖基础镜像的更新就好了)
1 | FROM ubuntu |
记住一点,我们只能将变化频率一样的指令合并在一起。将 node.js 安装与 npm 模块安装放在一起的话,则每次修改源代码,都需要重新安装 node.js,这显然不合适。因此,正确的写法是这样的:
1 | FROM ubuntu |
当镜像没有指定标签时,将默认使用latest 标签。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。当时,当镜像更新时,latest 标签会指向不同的镜像,这时构建镜像有可能失败。如果你的确需要使用最新版的基础镜像,可以使用 latest 标签,否则的话,最好指定确定的镜像标签。
示例 Dockerfile 应该使用16.04作为标签。
1 | FROM ubuntu:16.04 # it's that easy! |
假设我们更新了 apt-get 源,下载,解压并安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。但是,运行应用时 Docker 镜像中并不需要这些文件。我们最好将它们删除,因为它会使 Docker 镜像变大。
示例 Dockerfile 中,我们可以删除/var/lib/apt/lists/目录中的文件(它们是由 apt-get update 生成的)。
1 | FROM ubuntu:16.04 |
在示例中,我们选择了ubuntu作为基础镜像。但是我们只需要运行 node 程序,有必要使用一个通用的基础镜像吗?node镜像应该是更好的选择。
1 | FROM node |
更好的选择是 alpine 版本的node镜像。alpine 是一个极小化的 Linux 发行版,只有 4MB,这让它非常适合作为基础镜像。
1 | FROM node:7-alpine |
apk是 Alpine 的包管理工具。它与apt-get有些不同,但是非常容易上手。另外,它还有一些非常有用的特性,比如no-cache和 --virtual选项,它们都可以帮助我们减少镜像的大小。
WORKDIR指令可以设置默认目录,也就是运行RUN / CMD / ENTRYPOINT指令的地方。
CMD指令可以设置容器创建是执行的默认命令。另外,你应该讲命令写在一个数组中,数组中每个元素为命令的每个单词(参考官方文档)。
1 | FROM node:7-alpine |
ENTRYPOINT指令并不是必须的,因为它会增加复杂度。ENTRYPOINT是一个脚本,它会默认执行,并且将指定的命令错误其参数。它通常用于构建可执行的 Docker 镜像。entrypoint.sh 如下:
1 |
|
示例 Dockerfile:
1 | FROM node:7-alpine |
可以使用如下命令运行该镜像:
docker run our-app dev
docker run out-app start
docker run -ti out-app /bin/bash
在前文的 entrypoint 脚本中,我使用了exec命令运行 node 应用。不使用exec的话,我们则不能顺利地关闭容器,因为 SIGTERM 信号会被 bash 脚本进程吞没。exec命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。
COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包(参考官方文档)。
1 | FROM node:7-alpine |
我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。
示例中,源代码会经常变化,则每次构建镜像时都需要重新安装 NPM 模块,这显然不是我们希望看到的。因此我们可以先拷贝package.json,然后安装 NPM 模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装 NPM 模块。
1 | FROM node:7-alpine |
运行 Docker 容器时很可能需要一些环境变量。在 Dockerfile 设置默认的环境变量是一种很好的方式。另外,我们应该在 Dockerfile 中设置映射端口和数据卷。示例如下:
1 | FROM node:7-alpine |
ENV指令指定的环境变量在容器中可以使用。如果你只是需要指定构建镜像时的变量,你可以使用ARG指令。
使用LABEL指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的 Dockerfile 语法使用MAINTAINER指令指定镜像创建者,但是它已经被弃用了。有时,一些外部程序需要用到镜像的元数据,例如nvidia-docker需要用到com.nvidia.volumes.needed
。
示例如下:
1 | FROM node:7-alpine |
运行容器时,可以指定–restart always选项。这样的话,容器崩溃时,Docker 守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让 Docker 周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回 0,否则返回 1。对 HEALTHCHECK 感兴趣的话,可以参考这篇博客。示例如下:
1 | FROM node:7-alpine |
当请求失败时,curl —fail
命令返回非 0 状态。
如果你想要了解更多,请参阅 STOPSIGNAL, ONBUILD, 和 SHELL 指令。还要提到在构建镜像中一个非常有用的指令 --no-cache
(特别是在 CI 服务器上),以及--squash
here).
以上,Have fun 😃
]]>