七录书斋

Nginx 笔记

Nginx 架构介绍

Nginx 的代码时有一个核心和一系列的模块组成。

核心主要用于提供 WebServer 的基本功能,以及 Web 和 Mail 反向代理的功能;还用于启用网络协议,创建必要的运行时环境以及确保不同模块之间平滑地进行交互。不过,大多跟协议相关的功能和应用特有的功能都由 Nginx 的模块实现。

模块大致分类:

  • 事件模块:主要用于提供 OS 独立的(不同操作系统的时间机制有所不同)事件通知机制如 kqueue 或 epoll 等;
  • 阶段性处理器
  • 输出过滤器
  • 变量处理器
  • 协议:负责实现 Nginx 通过 http、tls/ssl、smtp、pop3、imap 与对应的客户端建立会话
  • upstream
  • 负载均衡

上面的这些模块共通组成了 Nginx 的 http 功能。在 Nginx 内部,进程间的通信是通过模块的 Pipeline 或 chain 实现的;换句话说,每一个功能或操作都由一个模块来实现。

RPC Notes

RPC 是指远程过程调用,也就是说两台服务器 A、B,一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数和方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

  1. 要解决通讯的问题,主要是通过在客户端和服务器之间建立 TCP 连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程共享同一个连接。
  2. 要解决寻址的问题,也就是说,A 服务器上的应用怎么告诉底层的 RPC 框架,如何连接到 B 服务器(如主机或 IP 地址)以及特定的端口,方法的名是什么,这样才能完成调用。比如基于 Web 服务协议栈的 RPC,就要提供一个 endpoint URI,或者是从 UDDI 服务上查找。
  3. 当 A 服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议(如 TCP)传递到 B 服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给 B 服务器。
  4. B 服务器收到请求后,需要对参数进行反序列化操作,恢复为内存中的表达方式,然后找到对应的方法进行本地调用,然后得到返回值。
  5. 返回值还要发送回服务器 A 上的应用,也要经过序列化的方式发送,服务器 A 接到后,再反序列化,恢复为内存中的表达方式,交给 A 服务器上的应用。

Flask 中的上下文

笔记来源:http://cizixs.com/2017/01/13/flask-insight-context

在 Flask 中,视图函数需要知道它执行情况的请求信息(请求的 URL、参数、方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。

最直观的做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。

Flask 的做法是把这些信息作为类似全局变量的东西,视图函数需要的时候,可以使用 from flask import request 获取。但是这些对象和全局变量不同的是 —— 它们必须是动态的,因为在多线程或多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。

如果对 Python 多线程比较熟悉的话,应该知道多线程中有个非常类似的概念 threading.local,可以实现多线程访问某个变量的时候只看到自己的数据。内部的原理说起来也很简单,这个对象有一个字典,保存了线程 id 对应的数据,读取该对象的时候,它动态地查询当前线程 id 对应的数据。Flask 上下文的实现也类似。

上下文有关的内容定义在 globals.py 文件中。

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

Flask 提供两种上下文:application context 和 request context。application context 又演化出来两个变量:current_app 和 g,而 request context 则演化出来 request 和 session。

常见的面试题整理 —— 数据库篇

笔记来源:https://zhuanlan.zhihu.com/p/23713529

什么是存储过程,有哪些优缺点?

存储过程是一些预编译的 SQL 语句。

更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用它就行了。

  • 存储过程是一个预编译的代码块,执行效率比较高;
  • 一个存储过程替代大量 SQL 语句,可以降低网络通信量,提高通信效率;
  • 可以一定程度上确保数据安全。

索引是什么,有什么作用以及优缺点?

索引是对数据库表中一或多列的值进行排序的结构,是帮助 MySQL 高效获取数据的数据结构。

MySQL 数据库的几个基本的索引类型:普通索引、唯一索引、主键索引、全文索引。

  • 索引加快数据库的检索速度
  • 索引降低了插入、删除、修改等维护任务的速度
  • 唯一索引可以确保每一行数据的唯一性
  • 通过使用索引,可以在查询的过程中使用优化器,提高系统的性能
  • 索引需要占物理空间

什么是事务?

事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

事务具有以下四个基本特征:

  1. 原子性:事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
  2. 一致性:只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
  3. 隔离性:事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
  4. 持久性:事务结束后,事务处理的结果必须能够得到固化。

数据库的乐观锁和悲观锁是什么?

DBMS 中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发生冲突,屏蔽一起可能违反数据完整性的操作
  • 乐观锁:假设不会发生并发生冲突,只在提交操作时检查是否违反数据完整性

使用索引查询一定能提高查询性能吗?

通常,通过索引查询数据比全表扫描要快,但是我们也必须注意到它的代价。

索引需要空间来存储,也需要定期维护,每当有记录在表中增减或索引被修改时,索引本身也会被修改。这意味着每条记录的 Insert,Delete,Update 将为此多付出 4、5 次的磁盘 IO。因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(Index Range Scan)适用于两种情况:

  • 基于一个范围的检索,一般查询返回结果集小于表中记录数的 30%
  • 基于非唯一性索引的检索

简单说一说 drop、delete 与 truncate 的区别

  • delete 和 truncate 只删除表的数据不删除表的结构
  • 速度,一般来说:drop > truncate > delete
  • delete 语句是 dml,这个操作会放到 rollback segment 中,事务提交之后才生效;如果有相应的 trigger,执行的时候将触发;truncate、drop 是 ddl,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。

drop、delete 与 truncate 分别在什么场景下使用

  • 不需要一张表的时候,用 drop
  • 想伤处部分数据行的时候,用 delete,并带上 where 语句
  • 保留表而删除所有数据的时候用 truncate

超键、候选键、主键、外键分别是什么

  • 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
  • 候选键:是最小的超键,即没有冗余元素的超键。
  • 主键:数据库表中对储存数据对象给予唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键。且主键的取值不能缺失,即不能为空值(null)。
  • 外键:在一个表中存在的另一个表的主键称此表的外键。

什么是视图,以及视图的使用场景有哪些

视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增删改查操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。

  • 只暴露部分字段给访问者,所以就建一个虚表,就是视图;
  • 查询的数据来源于不同的表,而查询者希望以统一的方式查询,这样也可以建立一个视图,把多个表查询结果联合起来,查询者只需要直接从视图中获取数据,不必考虑数据来源于不同表所带来的差异

说一说三个范式

  • 第一范式(1NF):数据库表中的字段都是单一属性的,不可再分,这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等;
  • 第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选;
  • 第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如果存在 A -> B -> 的决定关系,则 C 传递函数依赖于 A。因此,满足第三范式的数据库表应该不存在如下依赖关系:关键字段 -> 非关键字段 x -> 非关键字段 y

连接的种类

外连接

包括左向外连接、右向外连接或完整外部连接。

左连接

left join 或 left outer join。

左向外连接的结果集包括 left outer 子句中指定的左表的所有行,而不仅仅是连接所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表均为空值。

右连接

right join 或 right outer join。

右向外连接是左向外连接的反向连接。将返回右表的所有行。如果右表的某行在坐标中没有匹配行,则将左表返回空值。

完整外部连接

full join 或 full outer join。

完整外部连接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。

内连接

内连接是用比较运算符比较要连接的值的连接。

join 或 inner join。

只返回符合条件的 table1 和 table 2 的列。

交叉连接(完全)

cross join(不带条件 where)。

没有 where 子句的交叉连接将产生连接所涉及的表的笛卡尔积。第一个表的行数乘以第二个表的行数等于笛卡尔积结果集的大小。

MySQL索引原理及慢查询优化

笔记来源:https://tech.meituan.com/mysql-index.html

一般的应用系统,读写比例在 10:1 左右,而且插入操作和一般的更新操作很少出现性能问题,遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,所以查询语句的优化显然重中之重。

MySQL 索引原理

索引目的

索引的目的在于提高查询效率,类似于查字典的操作。

索引原理

数据库的查询要复杂得多,因为不仅面临着等值查询,还有范围查询、模糊查询(like)、并集查询(or)等等。数据库应该选择怎样的方式来应对所有的问题呢?能不能把数据分成段,然后分段查询呢?稍有算法基础的同学会想到搜索树,其平均复杂度是 lgN,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成文大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。

磁盘 IO 与预读

这里先简单介绍一下磁盘 IO 和预读,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道的时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到制定磁道所需要的时间,主流磁盘一般在 5ms 以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘 7200 转,表示每分钟转 7200 次,也就是说 1 秒中能转 120 次,旋转延迟就是 1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘 IO 的时间约等于 5+4.17 = 9 ms 左右,听起来还是挺不错的,但是要知道一台 500-MIPS 的机器每秒可以执行 5 亿条指令,因为指令依靠的是电的性质,换句话说执行一次 IO 的时间可以执行 40 万条指令,数据库动辄百万千万级数据,每次 9ms 的时间,显然是个灾难。

考虑到磁盘 IO 是分厂高昂的操作,计算机操作系统做了一些优化,当一次 IO 时,不光把当前磁盘地址的数据,而是把相邻的数据也都督导内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也很快被访问到。每一次 IO 读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为 4k 或 8k,也就是我们读取一页内的数据的时候,实际上才发生了一次 IO,这个理论对于索引的数据结构设计非常有帮助。

索引的数据结构

前面讲了生活中索引的例子,索引的基本原理,数据库的复杂性,又讲了操作系统相关知识,目的就是要让大家了解,任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下,我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查找数据时把磁盘 IO 次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树能否满足需求能?就这样,b+ 树应运而生。

详解 b+ 树

如上图,这是一颗 b+ 树。浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块 1 包含数据项 17 和 35,包含指针 p1, p2, p3,p1 表示小于 17 的磁盘块,p2 表示在 17 和 35 之间的磁盘块,p3 表示大于 35 的磁盘块。真实的数据存在叶节点中。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如 17,35 并不真实存在数据表中。

b+ 树的查找过程

如图所示,如果要查找数据项 29,那么首先会把磁盘块 1 由磁盘加载到内存,此时发生一次 IO,在内存中用二分查找确定 29 在 17 和 35 之间,锁定磁盘块 1 的 p2 指针,内存时间因为非常短,可以忽略不计,通过磁盘块 1 的 p2 指针把磁盘块 3 由磁盘加载到内存,发生第二次 IO,29 在26 和 30 之间,锁定磁盘块 3 的 p2 指针,通过指针加载磁盘块 8 到内存,发生第三次 IO,同时内存中做二分查找找到 29,结束查询,总计 3 次 IO。

真实的情况是,3 层的 b+ 树可以表示上百万的数据,如果上百万的数据查找只需要三次 IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次 IO,那么总共需要百万次的 IO,显然陈本非常高。

b+ 树的性质

  1. 通过上面的分析,我们知道 IO 次数取决于 b+ 树的高度 h,假设当前数据表的数据为 N,每个磁盘块的数据项的数量是 m,则有 \(h=log(m+1)N\),当数据量 N 一定的情况下,m 越大,h 越小,而 m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的代销,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如 int 占 4 个字节,要比 bigint 8 字节少一半。这也是为什么 b+ 树要求吧真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于 1 时会退化成线性表。
  2. 当 b+ 树的数据项是复合的数据结构时,比如 (name, age, sex) ,b+ 树是按照从左到右的顺序来建立搜索树的,比如当 (张三, 20, F) 这样的数据来检索的时候,b+ 树会优先比较 name 阿里确定下一部的搜索方向,如果 name 相同再依次比较 age 和 sex,最后得到检索的数据;但当 (20, F) 这样的没有 name 的数据来的时候,b+ 树就不知道下一步该查哪个节点,因为建立搜索树的时候 name 就是第一个比较因子,必须要先根据 name 来搜索才能知道下一步去哪里查询。比如当 (张三, F) 这样的数据来检索时,b+ 树可以用 name 来指定搜索方向,但下一个字段 age 缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是 F 的数据了。这是个非常重要的性质,即索引的最左匹配特性

慢查询优化

建索引的几大原则

  1. 最左前缀匹配原则,非常重要的原则,mysql 会一直向右匹配知道遇到范围查询(>、<、between、like)就停止匹配,比如 a = 1 and b = 2 and c > 3 and d = 4 如果建立 (a, b, c, d) 顺序的索引,d 是用不到索引的,如果建立 (a, b, d, c) 的索引则都可以用到,a/b/d 的顺序可以任意调整。
  2. = 和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立 (a, b, c) 索引可以任意顺序,mysql 的查询优化器会帮你优化索引可以识别的形式。
  3. 尽量选择区分度高的列作为索引,区分度的公式是 count(distinct col) / count(*),表示字段不重复的比例,比例越大我们扫描的次数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条记录。
  4. 索引列不能参与计算,保持列「干净」,比如 from_unixtime(create_time) = '2014-05-29'就不能使用到索引,原因很简单,b+ 树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用到函数才能比较,显然成本太大。
  5. 尽量的扩展索引,不要新建索引。比如表中已有 a 的索引,现在要加 (a, b) 的索引,那么只需要修改原来的索引即可。

慢查询优化的基本步骤

  1. 先运行看看是否真的很慢,注意设置 SQL_NO_CACHE;
  2. where 条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的 where 都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高;
  3. explain 查看执行计划,是否与 2 预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的 sql 语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照索引的几大原则
  7. 观察结果,不符合预期继续从 1 开始分析

MySQL 20 个经典面试题

MySQL 的复制原理以及流程

基本的原理流程,3 个线程及之间的关联;

  • 主:binlog 线程 —— 记录下所有改变了数据库数据的语句,放进 master 上的 binlog 中;
  • 从:io 线程 —— 在使用 start slave 之后,负责从 master 上拉去 binlog 内容,放进自己的 relay log 中;
  • 从:sql 执行线程 —— 执行 relay log 中的语句。

MySQL 中 MyISAM 和 InnoDB 的区别

  1. 5 点不同
    • InnoDB 支持事务,MyISAM 不支持
    • InnoDB 支持行级锁,MyISAM 不支持
    • InnoDB 支持 MVCC,MyISAM 不支持
    • InnoDB 支持外键,MyISAM 不支持
    • InnoDB 不支持全文索引,MyISAM 支持
  2. InnoDB 引擎的 4 大特性
    • 插入缓冲(insert buffer)
    • 二次写(double write)
    • 自适应哈希索引(ahi)
    • 预读(read ahead)
  3. 二者 select count(*) 哪个更快
    • MyISAM 更快,因为 MyISAM 内部维护了一个计数器,可以直接调取。

MySQL 中 varchar 与 char 的区别以及 varchar(50) 中的 50 代表的含义

  1. varchar(50) 中的 50 的含义是最多存放 50 个字符,varchar(50) 和 varchar(200) 存储 hello 所占用的空间一样,但后者在排序时会消耗更多的内存,因为 order by col 采用 fixed_length 计算 col 的长度

InnoDB 的事务日志的实现方式

  1. 有多少中日志:1)错误日志:记录出错信息,也记录一些警告信息或正确信息;2)查询日志:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行;3)慢查询日志:设置一个阈值,将裕兴时间超过该值的所有 SQL 语句都记录到慢查询日志中;4)二进制日志:记录对数据库执行更改的所有操作;5)中继日志;6)事务日志。
  2. 事务的 4 中隔离级别:1)读未提交;2)读已提交;3)可重复读;4)串行。
  3. 事务是如何通过日志来实现的?事务日志是通过 redo 和 InnoDB 的存储引擎日志缓冲(InnoDB log buffer)来实现的,当开始一个事务的时候,会记录该事务的 LSN(log sequence number),当事务执行时,会往 InnoDB 存储引擎的日志的日志缓存里插入事务日志;当事务提交时,必须将存储引擎的日志缓冲写入磁盘,也就是写数据前,需要先写日志,这种方式称为「预写日志方式」。

MySQL 数据库 CPU 飙升到 500% 的话应该如何处理

列出所有进程,观察所有进程,多秒没有状态变化的 kill 掉。

备份计划,mysqldump 以及 xtranbackup 的实现原理

xtrabackup 的实现原理:在 InnoDB 内部会维护一个 redo 日志文件,我们也可以叫做事务日志文件。事务日志会存储每一个 InnoDB 表数据的记录修改,当 InnoDB 启动时,InnoDB 会检查数据文件和事务日志,并执行两个步骤:它应用(前滚)已经提交的数据进行回滚操作。

500 台 db,在最快时间之内重启

puppet,dsh

你是如何监控你们数据库的?你们的慢日志都是怎么查询的?

lepus

是否做过主从一致性校验,如果有,怎么做,如果没有,你打算怎么做?

使用主从一致性校验工具:checksum、mysqldiff、pt-table-checksum

数据库是否支持 emoji 表情,如果不支持,如何操作?

更改字符集为 utf8_mb4。

表中有大字段 X(例如:text 类型),且字段 X 不会经常更新,以读为主,请问是选择拆成子表,还是继续放一起?

拆带来的问题:连接消耗 + 存储拆分空间。

不拆带来的问题:查询性能。

如果能容忍拆分带来的空间问题,拆的话最好和经常要查询的表的主键在物理结构上放置在一起(分区)顺序 IO,减少连接消耗,

MySQL 中的 InnoDB 引擎的行锁是通过加在什么上完成的,为什么是这样子的?

InnoDB 是基于索引来完成行锁。

例如:

select * from tab_with_index where id = 1 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么 InnoDB 将完成表锁。

一个 6 亿的表 a,一个 3 亿的表 b,通过外键 tid 关联,你如何最快的查询出满足条件的第 50000 到第 50200 中的这 200 条数据记录?

如果 a 表的 tid 是自增长,并且是连续的,b 表的 id 为索引:

select * from a, b where a.tid = b.id and a.tid > 5000000 limit 200;

如果 a 表的 tid 不是连续的,那么就需要使用覆盖索引。tid 要么是主键,要么是辅助索引,b 表 id 也要有索引:

select * from b, (select tid from a limit 50000, 200)   a where b.id = a.tid;

MySQL 笔记

MySQL 数据库引擎种类

缺省情况下,MySQL 支持三个引擎:ISAM,MyISAM,HEAP,另外两种类型是 InnoDB,BerkleyDB。

ISAM:是一个定义明确且历经时间考验的数据表格管理方法,它在设计之时考虑到数据库被查询的次数要远大于更新的次数。因此 ISAM 执行读取操作的速度很快,而且不占用大量的内存和存储资源。ISAM 两个主要不足在于:它不支持事物处理,也不能容错,如果你的硬盘崩溃了,那么数据文件就无法恢复了。

MyISAM:除了提供 ISAM 里所没有的索引和字段管理的大量功能,MyISAM 还能使用一种表格锁定机制,来优化多个并发的读写操作。其代价是你需要经常运行 OPTIMIZE TABLE 命令,来回复被更新机制浪费的空间。

HEAP:允许只驻留在内存里的临时表格。驻留在内存使得 HEAP 比 ISAM 和 MyISAM 的速度都快,但是它所管理的数据时不稳定的,而且如果在关机之前没有进行保存,那么所有的数据都会丢失。HEAP 在你需要使用 SELECT 表达式的时候非常有用。

InnoDB:在使用 MySQL 的时候,你所面对的每一个挑战几乎都源于 ISAM 和 MyISAM 数据库引擎不支持事务处理,也不支持外键。尽管 InnoDB 会慢很多,但是 InnoDB 包括了对事务处理和外键的支持,这两点是前面两个引擎所没有的。

MySQL 锁类型

根据锁的类型分,可以分为共享锁、排他锁、意向共享锁和意向排他锁。

根据锁的粒度分,可以分为行锁、表锁。

对于 MySql 而言,事务机制更多是靠底层的存储引擎来实现,因此,MySql 层面只有表锁,而支持事务的 InnoDB 存储引擎则实现了 行锁(记录锁,在行相应的索引记录上的锁),gap 锁(在索引记录间歇上的锁),next-key 锁(是记录锁和在此索引记录之间的 gap 上的锁的结合)。MySql 的记录锁实质是索引记录的锁,因为 InnoDB 是索引组织表;gap 锁是索引记录间隙的锁,这种锁只在 RR 隔离级别下有效;next-key 锁是记录锁加上记录之前的 gap 锁的组合,MySql 通过 gap 锁和 next-key 锁实现 RR 隔离级别。

共享锁:由读表操作加上的锁,加锁后其他用户只能获取该表或行的共享锁,不能获取排它锁,也就说只能读不能写。

排它锁:由写表操作加上的锁,加锁后其他用户不能获取该表或行的任何锁,典型是 MySQL 事务中的更新操作。

意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁之前必须先去的该表的 IS 锁。

意向排它锁(IX):事务打算给数据行加行排它锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

数据库设计数据类型需要注意哪些地方?

  1. VARCHAR 和 CHAR:varchar 是变长的,需要额外的 1-2 个字节存储,能节约空间,可能会对性能有帮助。但是由于是变长,可能发生碎片(如更新数据)。
  2. 使用 ENUM(枚举类型)代替字符串类型,数据实际存储为整型。
  3. 字符串类型:要尽可能避免使用字符串来做标识符,因为它占用了很多空间并且通常比整数类型要慢。
  4. 还要特别注意完全「随机」的字符串,例如由 MDR5()、SHA1()、UUID()产生的字符串,它们产生的每一个新值都会被任意地保存在很大的空间范围内,这会减慢 Insert 及一些 select 查询。
  5. 如果保存 UUID 值,应该移除其中的短横线,更好的办法是使用 UHEX() 把 UUID 值转化为 16 字节的数字,并把它保存在 Binary(16) 列中。

MySQL 几种备份方式

  1. 逻辑备份:使用自带的 mysqldump 工具进行备份,备份成 sql 文件的形式。

    • 优点:最大的好处是能够与正在运行的 mysql 自动协同工作,在运行期间可以确保备份是当时的点,它会自动将对应操作的表锁定,不允许其他用户修改(只能访问)。
    • 缺点:备份的速度比较慢。如果是数据量很多的时候,比较耗费时间。如果数据库服务处在提供给用户服务状态,在这段长时间操作过程中,意味着要锁定表,那么服务就会受到影响。
  2. 物理备份:直接拷贝 mysql 的数据目录

  3. 双机热备份:mysql 数据库没有增量备份的机制,当数据量太大的时候备份是一个很大的问题。优点:适合数据量大的时候,大的互联网公司对于 mysql 数据别分,都是采用热机备份,搭建多台数据库服务器,进行主从复制。

理解 Python WSGI

笔记来源:http://www.letiantian.me/2015-09-10-understand-python-wsgi/

什么是 WSGI

WSGI 的全称是 Web Server Gateway Interface,这是一个规范,描述了 web server 如何与 web application 交互、web application 如何处理请求。该规范具体描述在 PEP3333。注意,WSGI 既要实现 web server,也要实现 web application。

实现了 WSGI 的模块/库有:wsgiref(Python 内置)、werkzeug.serving、twisted.web 等。

当前运行在 WSGI 之上的 web 框架有 Bottle、Flask、Django 等。

WSGI server 所做的工作紧紧是将从客户端收到的请求传递给 WSGI application,然后将 WSGI application 的返回值作为响应传送给客户端。WSGI application 可以是栈式的,这个栈的中间部分叫做中间件,两端必须要实现 application 和 server。

WSGI Tutorial

WSGI application 接口

WSGI application 接口应该实现为一个可调用对象,例如函数、方法、类、含 __call__ 方法的实例,这个可调用对象可以接受 2 个参数:

  • 一个字典,该字典可以包含了客户端请求的信息以及其他信息,可以认为是请求上下文,一般叫做 environment;
  • 一个用于发送 HTTP 响应状态(HTTP status)、响应头(HTTP headers)的回调函数。

同时,可调用对象的返回值是响应正文(response body),响应正文是可迭代的、并包含了多个字符串。

WSGI application 的结构如下:

def application(environ, start_response):
    response_body = 'Request method: %s' % environ['REQUEST_METHOD']
    
    # HTTP status
    status = '200 OK'
    
    # HTTP headers
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    
    # 将响应状态和响应头交给 WSGI server
    start_response(status, response_headers)
    
    # 返回响应正文
    return [response_body]

Environment

下面的程序可以将 environment 字典的内容返回给客户端:

# 导入 python 内置的 WSGI server
from wsgiref.simple_server import make_server

def application (environ, start_response):

    response_body = [
        '%s: %s' % (key, value) for key, value in sorted(environ.items())
    ]
    response_body = '\n'.join(response_body)  # 由于下面将Content-Type设置为text/plain,所以`\n`在浏览器中会起到换行的作用

    status = '200 OK'
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)

    return [response_body]

# 实例化 WSGI server
httpd = make_server(
    'localhost',
    8051, # port
    application # WSGI application
)

httpd.handle_request()

访问 http://127.0.0.1:8051/,可以看到 environment 的内容。

可迭代的响应

如果把上面的可调用对象 application 的返回值:

return [response_body]

改成:

return response_body

这回导致 WSGI 程序的响应变慢。原因是字符串 response_body 也是可迭代的,它的每一次迭代只能得到 1 byte 的数据量,这也意味着每一次只向客户端发送 1 byte 的数据,知道发送完毕为止。所以,推荐使用 return [response_body]

解析 GET 请求

运行 environment.py 在浏览器中访问 http://localhost:8051/?age=10&hobbies=software&hobbies=tunning,可以在响应中找到:

QUERY_STRING: age=10&hobbies=software&hobbies=tunning
REQUEST_METHOD: GET

cgi.parse_qs() 函数可以很方便的处理 QUERY_STRING,同时需要 cgi.escape() 函数处理特殊字符以防止脚本注入,下面是个例子:

# -*- coding: utf-8 -*-

from cgi import parse_qs, escape

QUERY_STRING = 'age=10&hobbies=software&hobbies=tunning'
d = parse_qs(QUERY_STRING)
print d.get('age', [''])[0]
print d.get('hobbies', [])
print d.get('name', ['unknown'])

print 10 * '*'
print escape('<script>alert(123);</script>')

输出如下:

10
['software', 'tunning']
['unknown']
**********
&lt;script&gt;alert(123);&lt;/script&gt;

然后,我们可以写一个基本的处理 GET 请求的动态网页了:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html中form的method是get,action是当前页面
html = """
<html>
<body>
   <form method="get" action="">
        <p>
           Age: <input type="text" name="age" value="%(age)s">
        </p>
        <p>
            Hobbies:
            <input
                name="hobbies" type="checkbox" value="software"
                %(checked-software)s
            > Software
            <input
                name="hobbies" type="checkbox" value="tunning"
                %(checked-tunning)s
            > Auto Tunning
        </p>
        <p>
            <input type="submit" value="Submit">
        </p>
    </form>
    <p>
        Age: %(age)s<br>
        Hobbies: %(hobbies)s
    </p>
</body>
</html>
"""

def application (environ, start_response):

    # 解析QUERY_STRING
    d = parse_qs(environ['QUERY_STRING'])

    age = d.get('age', [''])[0] # 返回age对应的值
    hobbies = d.get('hobbies', []) # 以list形式返回所有的hobbies

    # 防止脚本注入
    age = escape(age)
    hobbies = [escape(hobby) for hobby in hobbies]

    response_body = html % { 
        'checked-software': ('', 'checked')['software' in hobbies],
        'checked-tunning': ('', 'checked')['tunning' in hobbies],
        'age': age or 'Empty',
        'hobbies': ', '.join(hobbies or ['No Hobbies?'])
    }

    status = '200 OK'

    # 这次的content type是text/html
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]

    start_response(status, response_headers)
    return [response_body]

httpd = make_server('localhost', 8051, application)

# 能够一直处理请求
httpd.serve_forever()

print 'end'

解析 POST 请求

对于 POST 请求,查询字符串(query string)是放在 HTTP 请求的正文(request body)中的,而不是放在 URL 中。请求正文在 environment 字典变量中键 wsgi.input 对应的值中,这是一个类似 file 的变量。PEP 3333 指出,请求头中 Content-Length 字段表示正文的大小,但是可能为空、或者不存在,所以读取请求正文时要用 try/except。

下面是一个可以处理 POST 请求的动态网站:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html中form的method是post
html = """
<html>
<body>
   <form method="post" action="">
        <p>
           Age: <input type="text" name="age" value="%(age)s">
        </p>
        <p>
            Hobbies:
            <input
                name="hobbies" type="checkbox" value="software"
                %(checked-software)s
            > Software
            <input
                name="hobbies" type="checkbox" value="tunning"
                %(checked-tunning)s
            > Auto Tunning
        </p>
        <p>
            <input type="submit" value="Submit">
        </p>
    </form>
    <p>
        Age: %(age)s<br>
        Hobbies: %(hobbies)s
    </p>
</body>
</html>
"""

def application(environ, start_response):

    # CONTENT_LENGTH 可能为空,或者没有
    try:
        request_body_size = int(environ.get('CONTENT_LENGTH', 0))
    except (ValueError):
        request_body_size = 0

    request_body = environ['wsgi.input'].read(request_body_size)
    d = parse_qs(request_body)

    # 获取数据
    age = d.get('age', [''])[0] 
    hobbies = d.get('hobbies', []) 

    # 转义,防止脚本注入
    age = escape(age)
    hobbies = [escape(hobby) for hobby in hobbies]

    response_body = html % { 
        'checked-software': ('', 'checked')['software' in hobbies],
        'checked-tunning': ('', 'checked')['tunning' in hobbies],
        'age': age or 'Empty',
        'hobbies': ', '.join(hobbies or ['No Hobbies?'])
    }

    status = '200 OK'

    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]

    start_response(status, response_headers)
    return [response_body]

httpd = make_server('localhost', 8051, application)

httpd.serve_forever()

print 'end'

怎样理解阻塞非阻塞与同步异步的区别?

笔记来源:https://www.zhihu.com/question/19732473

同步与异步

同步和异步关注的是消息通信机制(synchronous communication / asynchronous communication)。所谓同步,就是在发出一个「调用」时,在没有得到结果之前,该「调用」就不返回。但是一旦调用返回,就得到返回值了。

换句话说,就是右「调用者」主动等待这个「调用」的结果。

而异步则是相反,「调用」在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在「调用」发出后,「被调用者」通过状态、通知来通知调用者,或者通过回调函数处理这个调用。

典型的异步编程模型比如 Node.js。

阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态

阻塞调用时指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

什么是 Event Loop?

笔记来源:http://www.ruanyifeng.com/blog/2013/10/event_loop.html

Event Loop 指的是计算机系统的一种运行机制。JavaScript 就采用这种机制,来解决单线程运行带来的一些问题。

想要理解 Event Loop,就要从程序的运行模式讲起。运行以后的程序叫做「进程」(process),一般情况下,一个进程一次只能执行一个任务。

如果有很多任务需要执行,有三种方法:

  1. 排队:因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务;
  2. 新建进程:使用 fork 命令,为每个任务新建一个进程;
  3. 新建线程:因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。

以 JavaScript 语言为例,它是一种单线程语言,所有任务都在一个线程上完成,即采用上面第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现「假死」,因为 JavaScript 停不下来,也就无法响应用户的行为。

如果某个任务很耗时,比如设计很多 IO 操作,那么线程的运行大概是下面这样:

上图的绿色部分是程序的运行时间,红色部分是等待时间。可以看到,由于 IO 操作很慢,所以这个线程的大部分运行时间都在空等 IO 操作返回结果。这种运行方式称为「同步模式」(synchronous IO)。

如果采用多线程,同时运行多个任务,那很可能就是下面这样:

上图表明,多线程不仅占用多倍的系统资源,也闲置多倍的资源,这显然不合理。

Event Loop 就是为了解决这个问题而提出的。

Event Loop 是一种程序结构,用于等待和发送消息和事件。

简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为「主线程」,另一个负责主线程和其他进程(主要是各种 IO 操作)的通信,被称为「Event Loop 线程」(消息线程)。

上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到 IO 的时候,主线程就让 Event Loop 线程去通知相应的 IO 程序,然后接着往后运行,所以不存在红色的等待时间。等到 IO 程序完成操作,Event Loop 线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的惹怒,这就提高了运行效率。这种运行法式称为「异步模式」(asynchronous IO)。


文中提到的「阻塞」和「非阻塞」其实与「同步」「异步」并没有太大的关系。

具体可以参考:怎样理解阻塞非阻塞与同步异步的区别?

I don't understand Python's Asyncio

笔记来源:https://linux.cn/article-8051-1.html

原语

asyncio 通过协程的帮助来实现异步 IO。最初它是通过 yield 和 yield from 表达式实现的一个库,因为 Python 语言本身演进的缘故,现在它已经变成了一个更复杂的怪兽。所以,为了在同一个频道讨论下去,你需要了解如下一些术语:

  • 事件循环: 什么是 Event Loop?
  • 时间循环策略
  • awaitable
  • 协程函数
  • 老式协程函数
  • 协程
  • 协程封装
  • 生成器
  • future
  • 并发的 future
  • 任务
  • 句柄
  • 执行器(executor)
  • 传输(transport)
  • 协议

此外,Python 还新增了一些新的特殊方法:

  • __aenter____aexit__,用于异步块的操作
  • __aiter____anext__,用于异步迭代器(异步循环和异步推导)。为了更强大些,协议已经改变过一次了。在 Python 3.5 它返回一个 awaitable(这是个协程);在 3.6 它返回一个新的异步生成器。
  • __await__,用于自定义的 awaitable

事件循环

asyncio 事件循环和你第一眼看上去的略有不同。表面看,每个线程都有一个事件循环,然而事实并非如此。我认为它们应该按照如下的方式工作:

  • 如果是主线程,当调用 asyncio.get_event_loop() 时创建一个事件循环
  • 如果是其他线程,当调用 asyncio.get_event_loop() 时返回运行时错误
  • 当前线程可以使用 asyncio.set_event_loop() 在任何事件节点绑定事件循环。该事件循环可由 asyncio.new_event_loop() 函数创建
  • 事件循环可以在不绑定到当前线程的情况下使用
  • asyncio.get_event_loop() 返回绑定线程的事件循环,而非当前运行的事件循环

这些行为的组合是很混淆的,主要有以下几个原因。首先,你需要知道这些函数被委托到全局设置的底层事件循环策略。默认是hi将事件循环绑定到线程。或者,如果需要的话,可以在理论上将事件循环绑定到一个 greenlet 或类似的。然而,重要是要知道库代码不控制策略,因此不能推断 asyncio 将适用于线程。

其次,asyncio 不需要通过策略将事件循环绑定到上下文。事件循环可以单独工作。但是这正是库代码的第一个问题,因为协同程序或类似的东西并不知道哪个事件循环负责调度它。这意味着,如果从协程中调用 asyncio.get_event_loop(),你可能没有机会取得事件循环。这也是所有 API 均采用可选的显式事件循环参数的原因。举例来说,要弄清楚当前哪个协程正在运行,不能使用如下调用:

def get_task():
    loop = asyncio.get_event_loop()
    try:
        return asyncio.Task.get_current(loop)
    except RuntimeError:
        return None

相反,必须显式地传递事件循环。这进一步要求你在库代码中显式地遍历事件循环,否则可能发生很奇怪的事情。

由于事件循环策略不提供当前上下文的标识符,因此库也不可能以任何方式「索引」到当前上下文。也没有回调函数用来监视这样的上下文拆除,这进一步限制了实际可以展开的操作。

Awaitables and Coroutines

In my humble opinion the biggest design mistake of Python was to overload iterators so much. They are now being used not just for iteration but also for various types of coroutines. Python 中迭代器最大的设计错误之一是:如果 StopIteration 没有被捕获形成的空泡,这可能导致非常令人沮丧的问题,其中某处的异常可能导致其他地方的生成器或协同程序终止。这是一个长期存在的问题,基于 Python 的模板引擎 Jinja 经常面临这种问题。该模板引擎在内部渲染为生成器,并且当由于某种原因的模板引起 StopIteration 时,渲染就停止在那里。

Python 慢慢认识到了过度重载的教训。首先在 3.x 版本加入了 asyncio 模块,并没有语言级支持。所以自始至终它不过仅仅是装饰器和生成器而已。为了实现 yield from 以及其他东西。StopIteration 再次重载,这导致了令人困惑的行为,像这样:

没有错误,没有警告,只是不是你期望的行为。这是因为从一个座位生成器的函数中 return 的值实际上引发了一个带有单个参数的 StopIteration,它不是由迭代器协议捕获的,而是在协程代码中处理。

在 3.5 和 3.6 有很多改变,因为现在除了生成器我们还有协程对象。除了通过封装生成器来生成协程,没有其他可以直接生成协程的单独对象。它是通过给函数加 async 前缀来实现。例如 async def x() 会产生这样的协程。在 3.6 中,将由单独的异步生成器,它通过触发 AsyncStopIteration 保持其独立性。此外,对于 Python 3.5 和更高的版本,导入新的 future 对象(generator_stop),如果代码在迭代步骤中触发 StopIteration,它将引发 RuntimeError。

老的实现方式并未真的消失。生成器仍然具有 sendthrow 方法,以及协程仍然在很大程度上表现为生成器。

为了统一很多这样的重复,现在我们在 Python 中有更多的概念了:

  • awaitable:具有 __await__ 方法的对象。由本地协同程序和旧式协同程序以及一些其他程序来实现;
  • 协程函数(coroutine function):返回原生协程的函数。不要与返回协程的函数混淆;
  • 协程(coroutine):原生协程,注意,目前为止,当前文档不认为老式 asyncio 协程时协程程序。至少 insepect.iscoroutine 不认为它是协程。尽管它被 future/awaitable 分支接纳。

协程封装器(coroutine wrapper)

每当你运行 async def,Python 就会调用一个线程局部的协程封装器。它由 sys.set_coroutine_wrapper 设置,并且它是可以包装这些东西的一个函数。看起来有点像如下代码:

在这种情况下,我从来没有实际调用原始的函数,只是给你一个提示,说明这个函数可以做些什么。目前我只能说它总是线程的局部有效,所以,如果替换事件循环策略,你需要搞清楚如何让协程封装器在相同的上下文同步更新。创建的新线程不会从父线程继承那些标识。

这不要与 asyncio 协程封装代码混淆。

全栈必备:网络编程基础

笔记来源:http://blog.jobbole.com/110041/

七层模型

七层模型(OSI,Open System Interconnection 参考模型),是国际标准化组织制定的一个用于计算机或通信系统间互联的标准体系。它是一个七层的抽象模型,不仅包括一些列抽象的术语和概念,也包括具体的协议。

每一层的含义:

  • 物理层(Physical Layer):建立、维护、断开物理连接;
  • 数据链路层(Link):逻辑连接、进行硬件地址寻址、差错校验等;
  • 网络层(Network):进行逻辑寻址,实现不同网络之间的路径选择;
  • 传输层(Transport):定义传输数据的协议端口号,及流控和差错校验。
  • 会话层(Session Layer):简历、管理、终止会话;
  • 表示层(Presentation Layer):数据的表示、安全、压缩;
  • 应用层(Application):网络服务于最终用户的一个借口。

每一层利用下一层提供的服务于对等层通信,每一层使用自己的协议。TCP/IP 协议栈是这一模型的具体实现。

TCP/IP 协议模型

TCP/IP 是 Internet 的基础,是一组协议的代名词,包括许多协议,组成了 TCP/IP 协议栈。TCP/IP 有四层和五层模型直说,区别在于数据链路层是否作为独立的一层存在。

数据时如何传递的呢?这就要了解网络层和传输层的协议,我们熟知的 IP 包结构是这样的:

IP 协议和 IP 地址是两个不同的概念,这里咩有设计 IPV6。传输层使用这样的数据包进行传输,传输层又氛围面向连接的可靠传输 TCP 和数据包 UDP。

TCP 的包结构:

TCP 连接建立的三次握手肯定是必知必会,在系统调优的时候,内核中关于网络的相关参数与这个图息息相关。UDP 是一种无连接的传输层协议,提供的是简单不可靠的信息传输。协议结构相对简单,包括源和目标的端口号,长度以及校验和。

基于 TCP 和 UDP 的数据封装及解析示例如下:

模型解读示例

FTP 是一个比较好的例子,假设两台计算机分别是 A 和 B,将使用 FTP 将 A 上的一个文件 X 传输到 B 上。

首先,计算机 A 和 B 之间要有物理层的连接,可以是有线(比如同轴电缆或双绞线通过 RJ-45 的电路接口连接),也可以是无线(WIFI)。先简化一下,考虑局域网,暂不讨论路由器和交换机以及 WiFi 热点。这些物理层的连接建立了比特流的原始传输通路。

接下来,数据链路层登场,建立两台计算机的数据链路。如果 A 和 B 所在的网络上同时连接着计算机 C、D、E等等,A 和 B 之间如何建立数据链路呢?这一过程就是物理寻址,A 要在众多的物理连接中找到 B,依赖的是计算机的物理地址即 Mac 地址。以太网采用 CSMA/CD 方式来传输数据,数据在以太网的局域网中都是以广播方式传输的,整个局域网中的所有节点都会受到该帧,只有目标 MAC 地址与自己的 MAC 地址相同的帧才会被接收。A 通过差错控制和接入控制找到了 B 的网卡,建立可靠的数据通路。

那 IP 地址呢?数据链路建立起来了,还需要 IP 地址吗?我们 FTP 命令中指定的是 IP 地址而不是 MAC 地址,IP 地址是逻辑地址,包括网络地址和主机地址。如果 A 和 B 在不同的局域网中,中间有着多个路由器,A 需要对 B 进行逻辑寻址才可以。物理地址用于底层的硬件的通信,逻辑地址用于上层的协议间的通信。在以太网中:逻辑地址就是 IP 地址,物理地址就是 MAC 地址。在使用中,两种地址是用一定的算法将它们联系起来的。所以,IP 是用来在网络上选择路由的,在 FTP 命令中,IP 中的原地址就是 A 的 IP 地址,目标地址就是 B 的 IP 地址。这是网络层,负责将分组数据从源端传输到目的端。

A 向 B 传输一个文件时,如果文件中有部分数据丢失,就可能造成在 B 上无法正常阅读或使用。所以需要一个可靠的连接,能够确保传输过程的完整性,这就是传输层的 TCP 协议,FTP 就是建立在 TCP 之上的。TCP 的三次握手确定了双方数据包的序号、最大接受数据的大小(window)以及 MSS(Maximum Segment Size)。TCP 利用 IP 完成寻址,TCP 中提供了端口号,FTP 中目的端口号一般是 21.传输层的端口号对应主机进程,指本地主机与远程主机正在进行的会话。

会话层用来建立、维护、管理应用程序之间的会话,主要工呢过是对话控制和同步,编程中所涉及的 session 是会话层的具体体现。表示层完成数据的解码和编码,加密解密,压缩解压缩等。应用层就是具体应用本身了。

Socket

在 Linux 世界,「一切皆文件」,操作系统把网络读写作为 IO 操作,就像读写文件那样,对外提供出来的编程接口就是 Socket,所以,Socket 是通信的基石,是支持 TCP/IP 协议网络通信的基本操作单元。socket 实质上提供了进程通信的断点,进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。一个完整的 socket 有一个本地唯一的 socket 号,这是由操作系统分配的。

从设计模式角度看,Socket 其实是一个外观模式,它把复杂的 TCP/IP 协议栈隐藏在 Socket 接口后面,对用户来说,一组简单的 Socket 接口就是全部。

当应用程序创建一个 socket 时,操作系统就返回一个整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收数据)。

以 TCP 为例,典型的 Socket 使用如下:

在许多操作系统中,Socket 描述符和其他 I/O 描述符是继承在一起的,操作系统把 Socket 描述符实现为一个指针数组,这些指针指向内部数据结构。进一步看,操作系统为每个运行的进程维护一张单独的文件描述符表,当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者。

进程进行 Socket 操作时,也有着多种处理方式,如阻塞式 IO,非阻塞式 IO,多路复用(select/poll/epoll),AIO 等。

多路复用往往在提升性能方面有着重要的作用。select 系统调用的功能是对多个文件描述符进行监视,当有文件描述符的文件读写操作完成以及发生异常或者超时,该调用会返回这些文件描述符。select 需要便利所有的文件描述符,就遍历操作而言,复杂度是 \(O(n)\)。

epoll 相关系统调用是在 Linux 2.5 后某个版本开始引入的。该系统调用针对传统的 select/poll 不足,设计上作了很大的改动。select/poll 的缺点在于:

  1. 每次调用时要重复地从用户模式读入参数,并重复地扫描文件描述符;
  2. 每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。

epoll 是把 select/poll 单个的操作拆分为 1 个 epoll_create,多个 epoll_ctrl 和一个 wait。此外,操作系统内核针对 epoll 操作添加了一个文件系统,每一个或者多个要监视的文件描述符都有一个对应的 inode 节点,主要信息保存在 eventpoll 结构中。而被监视的文件的重要信息则保存在 epitem 结构中,是一对多的关系。由于在执行 epoll_create 和 epoll_ctrl 时,已经把用户模式的信息保存到内核了,所以之后即便反复调用 epoll_wait,也不会重复地拷贝参数,不会重复扫描文件描述符,也不反复地把当前进程放入/拿出等待队列。

所以,当前主流的 Server Socket 实现大都采用了 epoll 的方式,例如 Nginx。

函数式编程

笔记来源:https://coolshell.cn/articles/10822.html

函数式编程的三大特性:

  1. immutable data 不可变数据:像 Clojure 一样,默认上变量是不可变的,如果你要改变变量,你需要把变量 copy 出去修改。这样一来,可以让你的程序少很多 Bug。因为,程序中的状态不好维护,在并发的时候更不好维护。
  2. first class function:这个技术可以让你的函数就像变量一样来使用。
  3. 尾递归优化:递归会有递归深度限制,性能会大幅度下降。所以我们使用尾递归优化技术——每次递归时都会重用 stack,这样一来就能提升性能,当然,这需要语言或者编译器内部的支持。

函数式编程的几个技术:

  1. map & reduce:函数式编程最常见的技术就是堆一个集合做 Map 和 Reduce 操作。这比起过程式语言来说,在代码上要更容易阅读。
  2. pipeline:这个技术的意思是,把函数实例成一个一个的 action,然后,把一组 action 放到一个数组或是列表中,然后把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。
  3. recursing 递归:递归最大的好处是简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
  4. currying:把一个函数的多个参数分解成多个函数,然后把函数多层封装起,每层函数都返回一个函数去接收下一个参数。这样,可以简化函数的多个参数。
  5. higher order function 高阶函数:所谓高阶函数就是函数当做参数,把传入的函数做成一个封装,然后返回这个封装函数。

函数式的一些好处:

  1. parallelization 并行:所谓并行的意思就是在并行环境下,各个线程之间不需要同步或互斥。
  2. lazy evaluation 惰性求值:这个需要编译器支持。表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。
  3. determinism 确定性:所谓确定性的意思就是像数学函数那样 \(f(x)=y\) ,这个函数无论在什么场景下,都会得到同样的结果。

函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新值给你

一个简单的例子:

def inc(x):
    def incx(y):
        return x + y
    return incx
    
inc2 = inc(2)
inc5 = inc(5)

inc2(5) #7
inc5(5) #10

我们可以看到上面那个例子 inc() 函数反悔了另一个函数 incx(),于是我们可以用 inc() 函数来构造各种版本的 inc 函数。这个计数就是 Curring。从这个技术上,你可能体会到函数式编程的理念:把函数当成变量来用,关注于描述问题而不是怎么实现。

如何判断是否 functional:

  • 它们之间没有共享的变量
  • 函数间通过参数和返回值来传递数据
  • 在函数里没有临时变量

Scheme 笔记

生成表

作为 Lisp 语言大家族中的一员,Scheme 同样擅长处理表,表在后面章节中的递归函数和告诫函数中扮演重要的角色。

Cons 单元和表

Cons 单元

表的元素:Cons 单元(Cons cells)。Cons 单元是一个存放了两个地址的内存空间,Cons 单元可以用函数 cons 生成。

系统返回(1 . 2)。函数 cons 给两个地址分配了内存空间,并把存放指向 1 的地址放在一个空间,把存放指向 2 的地址放在另一个空间。存放指向 1 的地址的内存空间被称作 car 部分,对应的,存放指向 2 的地址的内存空间被称作 cdr 部分。car 和 cdr 分别是寄存器地址部分(Contents of the Address part of the Register)和寄存器减量部分(Contents of the Decrement part of the Register)的简称。这些名字最初来源于 Lisp 首次实现所用用的硬件环境中内存空间的名字。这些名字同时也表明 Cons 单元的本质就是一个内存空间,cons 这个名字是术语「构造」(construction)的简称。

Cons 单元也可以被串起来:

(3 . (1 . 2)) 可以更方便地表示为 (3 1 . 2)。这种情况的内存空间如下图所示:

Cons 单元可以存放不同类型的数据,也可以嵌套。

(cons #\a (cons 3 "hello"))
;Value 17: (#\a 3 . "hello")

(cons (cons 0 1) (cons 2 3))
;Value 23: ((0 . 1) 2 . 3)

这是因为Scheme可以通过地址操作所有的数据。(#\c代表了一个字符c。例如,#\a就代表字符a)。

表示 Cons 单元通过用 cdr 部分连接到下一个 Cons 单元的开头实现的。表中包含的'()被作为空表。就算数据仅由一个 Cons 单元组成,只要它的 cdr 单元是 '(),那么它就是一个表,下图表示了 (1 2 3) 的内存结构。

事实上,表可以像下面这样递归地定义:

  1. '() 是一个表
  2. 如果 ls 是一个表并且 obj 是某种类型的数据,那么 (cons obj ls) 也是一个表。

正因为表示一种被递归定义的数据结构,将它用在递归的函数中显然是合理的。

原子

不使用 Cons 单元的数据结构称为原子(atom)。数字、字符、字符串、向量和空表 '() 都是原子。'() 既是原子,又是表。

引用

所有的记号都会依据 Scheme 的求值规则求值:所有记号都会从最内层的括号一次向外层括号求值,且最外层括号返回的值将作为 S-表达式的值。一个被称为「引用」(quote)的形式可以用来阻止记号被求值。它是用来讲符号或者表原封不动地传递给程序,而不是求值后变成其它的东西。

因为 quote 使用的频率很高,它被简写为 '

  • '(+ 2 3) 代表列表 (+ 2 3) 本身
  • '+ 代表 + 本身

实际上,'() 是对空表的引用,也就是说,尽管解释器返回 () 代表空表,你也应该用 '() 来表示空表。

特殊形式

Scheme 有两种不同类型的操作符:

  1. 函数:函数会对所有的参数求值并返回值
  2. 特殊形式:特殊形式不会对所有的参数求值

car 和 cdr 函数

返回一个 Cons 单元的 car 部分 和 cdr 部分的函数分别是 car 和 cdr 函数。如果 cdr 部分串联着 Cons 单元,解释器会打印出整个 cdr 部分。如果 Cons 单元的 cdr 部分不是 '(),那么其值稍后亦会被展示。

list 函数

list 函数使得我们可以构建包含数个元素的表。函数 list 有任意个数的参数,且返回由这些参数构成的表。

定义函数

Scheme 解释器通过内存空间中的数据地址操作所有的数据,因此,所有存在于内存空间中的对象都以相同额方式处理。

定义有参数的函数

可以通过在 lambda 后放一个参数表来定义有参数的函数。

分支

if 表达式

(if predicate then_value else_value)

如果 predicate 部分为真,那么 then_value 部分被求值,否则 else_value 部分被求值,并且求得的值会返回给 if 语句的括号外。true 是除 false 以外的任意值,true 使用 #t 表示,false 使用 #f 表示。

在 R5RS 中,false'() 是两个不同的对象。

cond 表达式

尽管所有的分支都可以用 if 表达式表达,但当条件有更多的可能性时,你就需要使用嵌套的 if 表达式了,这将使代码变得复杂。处理这种情况可以使用 cond 表达式:

(cond
    (predicate_1 clauses_1)
    (predicate_2 clauses_2)
    ...
    (predicate_n clauses_n)
    (else        clauses_else))

在 cond 表达式中,predicate_i 是按照从上到下的顺序求值,而当 predicate_i 为真时,clause_i 会被求值并返回。之后的不会被求值。

做出判断的函数

基本函数 eq?, eqv?, equal? 具有两个参数,用于检查这两个参数是否「一致」。这三个函数间有略微的区别。

  • eq?:该函数比较两个对象的地址。
  • eqv?:该函数比较两个存储在内存中的对象的类型和值。对于过程(lambda 表达式)的比较依赖于具体的实现。这个函数不能用于类似于表和字符串一类的序列比较,因为尽管这些序列看起来是一致的,但它们的值是存储在不同的地址中。
  • equal?:该函数用于比较类似于表或者字符串一类的序列。

用于检查数据类型的函数

函数 比较对象
pair? 是否是序对
list? 是否是表
null? 是否是空表
symbol? 是否是符号
char? 是否是字符
string? 是否是字符串
number? 是否是数字
complex? 是否是复数
real? 是否是实数
rational? 是否是有理数
integer? 是否是整数
exact? 是否是不是浮点数
inexact? 是否是浮点数

局部变量

let 表达式

使用 let 表达式可以定义局部变量:

(let binds body)

变量在 binds 定义的形式中被声明并初始化。body 由任意多个 S-表达式构成,格式如下:

[binds] -> ((p1 v1) (p2 v2) ...)

声明了变量 p1, p2,分别为它们赋初始值 v1,v2.变量的作用域(Scope)为 body 体,也就是说变量只在 body 中有效。

实际上,let 表达式知识 lambda 表达式的一个语法糖:

((lambda (p1 p2 ...)
        exp1 exp2 ...) v1 v2)

这是因为 lambda 表达式用于定义函数,它为变量建立了一个作用域。变量的作用域通过使用 let 表达式或 lambda 表达式来确定,在 Scheme 中,这个有效域由源代码的编写决定,这叫做「词法闭包」(lexical closure)。

重复

虽然 Scheme 中可以使用 do 表达式,但通常我们用递归来实现重复。

递归

在自己的定义中调用自己的函数叫做递归函数(Recursive Function)。

我们通常使用计算阶乘来解释递归。

(define (fact n)
  (if (= n 1)
      1
      (* n (fact (- n 1)))))

(fact 5) 的计算过程如下:

(fact 5)
⇒ 5 * (fact 4)
⇒ 5 * 4 * (fact 3)
⇒ 5 * 4 * 3 * (fact 2)
⇒ 5 * 4 * 3 * 2 * (fact 1)
⇒ 5 * 4 * 3 * 2 * 1
⇒ 5 * 4 * 3 * 2
⇒ 5 * 4 * 6
⇒ 5 * 24
⇒ 120

递归函数可以以一种简单的方式表达重复,表示被递归定义的,进而表和递归函数可以很好地配合。

尾递归

普通的递归调用并不高效因为它既浪费存储空间又具有函数调用开销。与之相反,尾递归函数包含了计算结果,当计算结束时直接将其返回。特别第,由于 Scheme 规范要求尾递归调用转化为循环,因此尾递归调用就不在函数调用开销。

fact 的尾递归调用版本:

(define (fact-tail n)
  (fact-rec n n))

(define (fact-rec n p)
  (if (= n 1)
      p
      (let ((m (- n 1)))
        (fact-rec m (* p m)))))

fact-tail 计算阶乘的过程如下:

(fact-tail 5)
⇒ (fact-rec 5 5)
⇒ (fact-rec 4 20)
⇒ (fact-rec 3 60)
⇒ (fact-rec 2 120)
⇒ (fact-rec 1 120)
⇒ 120

因为 fact-rec 并不等待其它函数的计算结果,因此当它计算结束时即从内存中释放。计算通过 fact-rec 的参数来演进,这基本等同于循环。如上文所述,Scheme 将尾递归转化为循环,Scheme 就无需提供循环的语法来实现重复。

命名 let

命名 let(named let)可以用来表达循环。

(define (fact-let n)
  (let loop((n1 n) (p n))           ; 1
    (if (= n1 1)                    
    p
    (let ((m (- n1 1)))
      (loop m (* p m))))))      ; 2

上例代码展示了如何使用命名 let 来计算阶乘。fact-let 函数使用了一个命名 let 表达式(loop),这与 fact-rec 函数时不同的。

在 Scheme 中,用命名 let 来表达循环是约定俗成的方法。

Python Tricks

1. __init__.py 里面可以写什么

有如下层级的代码:

graphics/
    __init__.py
    primitive/
        __init__.py
        line.py
        fill.py
        text.py
    formats/
        __init__.py
        png.py
        jpg.py

绝大部分时候让__init__.py空着就好,但是有些情况可能包含代码。例如用来自动加载子模块:

# graphics/formats/__init__.py
from . import jpg
from . import png

像这样一个文件,用户可以仅仅通过import graphics.formats来代替import graphics.formats.jpg以及import graphics.formats.png

2. 控制模块被全部导入的内容

在模块中定义变量 __all__(列表) 来明确地列出需要导出的内容。如果定义了 __call__,那么只有被列举出的东西会被导出。

3. 使用相对路径名导入包中子模块

使用绝对路径名导入的不利之处是这将顶层包名硬编码到你的源码中,如果你想要重新组织它,你的代码将很难工作。

假设在你的文件系统上有 mypackage 包,组织如下:

mypackage/
    __init__.py
    A/
        __init__.py
        spam.py
        grok.py
    B/
        __init__.py
        bar.py

如果模块 mypackage.A.spam 要导入同目录吓得模块 grok:

from . import grok

如果模块 mypackage.A.spam 要导入不同目录吓得模块 B.bar:

from ..B import bar

两个 import 都没有包含顶层包名,而是使用了 spam.py 的相对路径。

S-表达式

S-表达式(其中 S 代表「符号的」)是一种以人类可读的文本形式表达半结构化数据的约定。S-表达式可能以其在 Lisp 家族的编程语言中的使用而为人所知。语法细节和所支持的数据类型虽淫语言而异,但这些语言件最通用的特性则是使用 S-表达式 作为括号化的前缀表示法。

数据类型和语法

S-表达式格式有多种变体,支持不同数据类型的各种不同语法。最广泛支持的是:

  • 列表和点对
  • 符号
  • 字符串
  • 整数
  • 浮点数

在 Lisp 编程中的使用

S-表达式在 Lisp 中即用作代码,也用作数据。

S-表达式可以是如数字这样的单个对象,包括特殊原子nilt在内的 Lisp 原子,或写作(x . y)的 cons pair。更长的列表则有嵌套的 cons pair 组成,例如(1 . (2 . (3 . nil)))(亦可以写作更易理解的 (1 2 3))。

使用前缀表示方法,程序代码课写作 S-表达式。书写 Lisp 程序中额外的语法糖规则是,一般的表达式(quote x)可以省略为'x

S-表达式的源码示例

(define (factorial x)
    (if (zero? x) 1
          (* x (factorial (- x 1)))))

解析

S-表达式经常与 XML 进行比较,一个关键的区别是 S-表达式在语法上要简单的多,因此更容易解析。

# 一个简单的 S-表达式解析器
def parse_sexp(string):
    """
    >>> parse_sexp("(+ 5 (+ 3 5))")
    [['+', '5', ['+', '3', '5']]]
    """
    sexp = [[]]
    word = ''
    in_str = False
    for char in string:
        if char == '(' and not in_str:
            sexp.append([])
        elif char == ')' and not in_str:
            if word:
                sexp[-1].append(word)
                word = ''
            temp = sexp.pop()
            sexp[-1].append(temp)
        elif char in (' ', '\n', '\t') and not in_str:
            if word:
                sexp[-1].append(word)
                word = ''
        elif char = '\"':
            in_str = not in_str
        else:
            word += char
    return sexp[0]

S-表达式是二叉树的一种线性编码。

原子 -> 数字|符号
S表达式 -> 原子|(S表达式 . S表达式)

书写 S-表达式,还要同时写很多「.」号,因此,Lisp 语言定义了一套 S-表达式的化简规则:

  1. 如果一个点号右邻一个左括号,那么就可以将这个点号,左括号以及匹配的右括号一起去掉:(a . (b . c)) <=> (a b . c)
  2. 如果一个点号右邻原子 nil,那么就可以把这个点号和原子 nil 一起去掉:(a . (b . nil)) <=> (a b . nil) <=> (a b)

列表是一种特殊类型的 S-表达式,如果一个 S-表达式不是原子,而且经过化简可以把点号都去掉,就说这个 S-表达式是一个列表。

例如:(a .(b .(c . nil))) <=> (a b c)

为了使用方便,还允许有空表,而且还定义空表等价于原子 nil,空表的记法,使 nil 身兼三职:

  1. nil 表示一个原子
  2. nil 表示空表
  3. nil 表示逻辑值「假」

nil 成为了 Lisp 语言中唯一的既是原子又是列表的表达式。

在 Scheme 中,它没有 nil 的概念,只有空表 ()

Scheme 解释器相关笔记

Scheme 程序的语法和语义

  • 语法(syntax):指的是字母排列成正确表达式或声明的顺序;
  • 语义(semantics):指的是这些表达式或声明的意义。

Scheme 的语法非常简单:

  • Scheme 程序中只有表达式,表达式和声明之间并无区别;
  • 数字(例如 10)和符号(例如 A)被称为原子表达式(atomic expression),它们无法被拆分成更小的表达式。
  • 除此以外的一切都是列表表达式(list expression):以 ( 为首,以 ) 为尾,中间包括这零个或更多表达式。列表的第一个元素决定了它的含义:
    • 若第一个元素是关键字,例如 (if ...),那么这个列表是一个特殊形式(special form);特殊形式的意义取决于关键字;
    • 若第一个元素并非关键字,例如(fn ...),那这个列表则是函数调用。

语言解释器做些什么

一个计算机语言的解释器分为两部分:

  1. 分析(parse):解释器的分析部分将程序以一串字符串形式读入,依照语法规则(syntactic rules)验证其正确性并将程序转换成一种内部表达形式。在一个简单的解释器中,内部表达形式是一个树形结构,人们一般将其称为抽象语法树(abstract syntax tree)。抽象语法树的结构和程序中层层嵌套的声明及表达式非常接近,几乎可以说是完美对应。在编译器之中万网存在多个内部表达式,一开始先转换成抽象语法树,随后再转换成可以直接被计算器执行的指令序列。
  2. 执行(execution):内部表达式被按照语言的语法规则进行处理,以此来进行计算。
程序 --> [parser] --> 抽象语法树 --> [eval] --> 结果

分析:parser, tokenize, read_from_tokens

依照传统,分析被分成两个部分:

  1. 词法分析(lexical analysis):在这一部分中,输入的字符串被拆分为一系列的 token;
  2. 语法分析(syntactic analysis):将 token 汇编为抽象语法树。

环境(Environments)

eval 函数接收两个参数:一个我们想要求值的表达式 x,还有一个环境 env,x 将在这个环境中被求值。环境指的是变量名和它们值之间的映射。eval 默认会使用全局环境(global environment)进行求值,全局环境包含着一系列的标准函数(比如 sqrt, max, * 这类操作符)。这一环境可以用用户定义的变量扩展,语法为(define variable value)。我们可以用 Python 自带的字典来实现环境,字典中的键对为 {变量: 值} 的形式。

Scheme 的语法规则

表达式(Expression) 语法(Syntax) 语义(Semantics)和范例
变量引用(variable reference) var 该符号被认为是变量名;它的值是变量的值。
字面常量(constant literal) number 一个数字(number)求值得到它自身。
条件(conditional) (if test conseq alt) 对 test 进行求值;如果结果为真,对 conseq 进行求值并返回结果;否则对 alt 进行求值并返回结果。
定义(definition) (define var exp) 定义一个新的变量,将 var 的值定义为 exp 求值得到的结果。
过程调用(procedure call) (proc arg...) 如果 proc 不是 if/define/quote 其中之一,那它就被认为是一个过程(procedure)。对 proc 和所有的 args 求值,然后将 proc 过程应用于所有的 args 之上。
引用(quotation) (quote exp) 直接按字面返回 exp,不对其进行求值。范例:(quote (+ 1 2)) --> (+ 1 2)
赋值(assignment) (set! var exp) 对 exp 进行求值并将结果赋值给 var,exp 必须在之前定义过(被 define 定义过或者是包含 set! 表达式的过程中的一个参数)
过程(procedure) (lambda (var...) (exp)) 创造一个过程,参数为 var...,exp为过程的主体,范例:(lambda (r) (* pi (* r r)))

lambda 特殊形式会创建一个过程(procedure):

>>> (define circle-area (lambda (r) (* pi (* r r))))
>>> (circle-area 10)
314.159265359

过程调用 (circle-area 10) 使我们队过程的主体部分 (* pi (* r r)) 进行求值。求值所在的环境中 pi 与 r 的值同全局环境相同,而 r 的值为 10.事实上,解释器并不会简单地在全局环境之中将 r 的值设为 10。如果我们将 r 用于其他用途会怎么样?我们不希望对 circle-area 的调用改变 r 的值,因此我们希望讲一个局部变量 r 设为 10,这样就不会影响到其他同名的变量。因此,我们需要构建一种新的环境,允许同时创建局部和全局变量。

想法如下:在我们对 (circle-area 10) 求值时,首先提取过程主体部分 (* pi (* r r)),随后在仅有一个本地变量 r 的环境中求值,但该环境同时也能访问全局环境。下图演示了这种环境模型,局部环境(蓝色)嵌套在全局环境(红色)之中:

当我们在一个被嵌套的环境中查找变量时,首先在本层查找,如果没有找到对应值的话就到外一层查找。

class Procedure(object):
    "用户定义的Scheme过程。"
    def __init__(self, parms, body, env):
        self.parms, self.body, self.env = parms, body, env
    def __call__(self, *args): 
        return eval(self.body, Env(self.parms, args, self.env))

class Env(dict):
    "环境是以{'var':val}为键对的字典,它还带着一个指向外层环境的引用。"
    def __init__(self, parms=(), args=(), outer=None):
        self.update(zip(parms, args))
        self.outer = outer
    def find(self, var):
        "寻找变量出现的最内层环境。"
        return self if (var in self) else self.outer.find(var)

global_env = standard_env()

我们看到每个过程有 3 个组成部分:一个包含变量名的列表,一个主体表达式,以及一个外层环境。外层环境使得我们在局部环境中无法找到变量时有下一个地方可以寻找。

环境是 dict 的子类,因此它含有 dict 拥有的所有方法。除此之外还有两个额外的方法:

  1. 构造器 __init__ 接受一个变量名列表以及对应的变量值列表,构造一个新环境,内部形式为 {variable: value} 键对,并拥有一个纸箱外层环境的引用;
  2. find 函数用于找到某个变量所在的正确环境,可能是内层环境也可能是更外层的环境。

要想知道这部分的工作原理,我们首先来看看 eval 的定义。注意,现在我们需要调用 env.find(x) 来寻找变量处于哪一层环境之中;随后我们才能从那一层环境中提取 x(define 分支的定义没有改变,因为 define 总是向最内一层的环境添加变量)。同时我们还增加了两个判定分支:set! 分支中,我们寻找变量所处的环境并将其设为新的值;通过 lambda,我们可以传入参数列表、主体以及环境以创建一个新的过程。

《流利的 Python》笔记

第 17 章:使用 futures 处理并发

抨击线程的往往是系统程序员,他们考虑的使用场景对一般的应用程序员来说,也许一生都不会遇到……应用程序员遇到的使用场景,99% 的情况下只需知道如何派生一堆独立的线程,然后用队列收集结果。—— Michele Simionato

本章主要讨论 Python 3.2 引入的 concurrent.futures 模块,这个库封装了前面引文中 Michele 所述的模式,特别易于使用。

这一章还会介绍 futures 的概念,futures 指一种对象,表示异步执行的操作。这个概念的作用很大,是 corcurrent.futures 模块和 asyncio 包的基础。

示例:网络下载的三种风格

为了高效处理网络 I/O,需要使用并发,因为网络有很高的延迟,所以为了不浪费 CPU 周期去等待,最好在收到网络相应之前做些其他事情。

为了通过代码说明这一点,我写了三个示例程序:从网上下载 20 个国家的国旗图像。第一个示例程序 flag.py 是依序下载的:下载完一个图像,并将其保存在硬盘中之后,才请求下载下一个图像;另外两个脚本是并发下载的:几乎同时请求所有图像,每下载完一个文件就保存一个文件。flags_threadpool.py 脚本使用 concurrent.futures 模块,而 flags_asyncio.py 脚本是会用 asyncio 包。

依序下载的脚本

# flags.py:依序下载的脚本;另外两个脚本会重用其中几个函数

import os
import time
import sys

import requests

POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split()

BASE_URL = 'http://flupy.org/data/flags'

DEST_DIR = '/Users/jiayuan/Downloads/'


def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)
        

def get_flag(cc):
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)
    return resp.content


def show(text):
    print(text, end=' ')
    sys.stdout.flush()
    
    
def download_many(cc_list):
    for cc in sorted(cc_list):
        image = get_flag(cc)
        show(cc)
        save_flag(image, cc.lower() + '.gif')
    return len(cc_list)


def main(download_many):
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloads in {:.2f}s'
    print(msg.format(count, elapsed))

测试结果:

>>> main(download_many)
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 
20 flags downloads in 30.90s

使用 concurrent.futures 模块下载

concurrent.futures 模块的主要特色是 ThreadPoolExecutor 和 ProcessPollExecutor 类,这两个类实现的接口能分别在不同的线程或进程中执行可调用的对象。这两个雷子啊内部维护这一个工作线程或进程池,以及要执行的任务队列。不过,这个接口抽象层级很高,像下载国旗这种简单的案例,无需关心任何实现细节。

# flags_threadpool.py: 使用 futures.ThreadPoolExecutor 类实现多线程下载的脚本
from concurrent import futures

MAX_WORKERS = 20


def download_one(cc):
    image = get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc


def download_many(cc_list):
    workers = min(MAX_WORKERS, len(cc_list))
    with futures.ThreadPoolExecutor(workers) as executor:
        res = executor.map(download_one, sorted(cc_list)) # 1
        
    return len(list(res)) # 2

测试结果:

>>> main(download_many)
IN VN BD FR DE JP CN ID BR PH EG NG ET MX IR RU US CD PK TR 
20 flags downloads in 70.05s

关于这个示例的一些注解:

  • 1)map 方法的作用与内置的 map 函数类似,不过 download_one 函数会在多个线程中并发调用;map 方法返回一个生成器,因此可以迭代,获取各个函数返回的值。
  • 2)返回获取的结果数量;如果有线程抛出异常,异常会在这里抛出,这与隐式调用 next() 函数从迭代器中获取相应的返回值一样

futures 在哪里

futures 是 concurrent.futures 模块和 asyncio 包的重要组件,作为这两个库的用户,我们有时却见不到 Future。

从 Python 3.4 起,标准库中有两个名为 futures 的类:concurrent.futures.Future 和 asyncio.Future。这两个类的作用相同:两个 futures 类的实例都表示可能已经完成或者尚未完成的延迟计算。这与 Twisted 引擎中的 Deferred 类、Tornado 框架中的 Future 类,以及多个 JavaScript 库中的 Promise 对象类似。

futures 封装待完成的操作,可以放入队列,完成的状态可以查询,得到结果(或抛出异常),后可以获取结果(或异常)

我们要记住一件事:通常情况下自己不应该创建 futures,而只能由并发框架(concurrent.futures 或 asyncio)实例化。原因很简单:futures 表示终将发生的事情,而确定某件事会发生的唯一方式是执行的时间已经排定。因此,只有排定把某事件交给 concurrent.futures.Executor 子类处理时,才会创建 concurrent.futures.Future 实例。例如,Executor.submit() 方法的参数时一个可调用的对象,调用这个方法后会为传入的可调用对象排期,并返回一个 future。

客户端代码不应该改变 futures 的状态,并发框架在 future 表示的延迟计算结束后会改变 futures 的状态,而我们无法控制计算何时结束。

这两种 future 都有 .done() 方法,这个方法不阻塞,返回值是布尔值,指明 future 链接的可调用对象是否已经执行。客户端代码通常不会询问 future 是否运行结束,而是会等待通知。因此,两个 future 类都有 .add_done_callback() 方法:这个方法只有一个参数,类型是可调用的对象,futures 运行结束后会调用指定的可调用对象。

此外,还有 .result() 方法。在 Future 运行结束后调用的话,这个方法在两个 Future 类中的作用相同:返回可调用对象的结果,或者重新抛出执行可调用的对象时抛出的异常。

但是,如果 Future 没有运行结束,result 方法在两个 Future 类中的行为相差很大。对于 concurrenct.futures.Future 实例来说,调用 f.result() 方法会阻塞调用方所在的线程,直到有结果可返回。此时,result 方法可以接收可选的 timeout 参数,如果在指定的时间内 Future 没有运行完毕,会抛出 TimeoutError 异常。asyncio.Future.result 方法不支持设定超时时间,在那个库中获取 Future 的结果最好使用 yield from 结构。不过,对 concurrent.futures.Future 实例不能这么做。

这两个库中有几个函数会返回 Future,其他函数则使用 Future,以用户易于理解的方式实现自身。使用 Executor.map 方法属于后者:返回值是一个迭代器,迭代器的 __next__ 方法调用各个 Future 的 result 方法,因此我们得到的是各个 Future 的结果,而非 Future 本身。

为了从使用的角度理解 Future,我们可以使用 concurrent.futures.as_completed 函数重写上节的示例,这个函数的参数是一个 Future 列表,返回值是一个迭代器,在 Future 运行结束后产出 Future。

为了使用 futures.as_completed 函数,只需修改 download_many 函数,把较抽象的 executor.map 调用换成两个 for 循环:一个用于创建并排定 Future,另一个用于获取 Future 的结果。同时,我们会添加几个 print 调用,显式运行结束前后的 Future。

# flags_threadpool_ac.py: 把 download_many 函数中的 executor.map 方法换成 executor.submit 方法和 futures.as_completed 函数

def download_many(cc_list):
    cc_list = cc_list[:5]
    with futures.ThreadPoolExecutor(max_workers=3) as executor:
        to_do = []
        for cc in sorted(cc_list):
            future = executor.submit(download_one, cc)
            to_do.append(future)
            msg = 'Scheduled for {}: {}'
            print(msg.format(cc, future))
            
        results = []
        for future in futures.as_completed(to_do):
            res = future.result()
            msg = '{} result: {!r}'
            print(msg.format(future, res))
            results.append(res)
    return len(results)

测试结果:

>>> main(download_many)
Scheduled for BR: <Future at 0x109a12be0 state=running>
Scheduled for CN: <Future at 0x109a12da0 state=running>
Scheduled for ID: <Future at 0x109a620f0 state=running>
Scheduled for IN: <Future at 0x1099b9710 state=pending>
Scheduled for US: <Future at 0x106ac29e8 state=pending>
IDCN BR  <Future at 0x109a12da0 state=finished returned str> result: 'CN'
<Future at 0x109a620f0 state=finished returned str> result: 'ID'
<Future at 0x109a12be0 state=finished returned str> result: 'BR'
IN <Future at 0x1099b9710 state=finished returned str> result: 'IN'
US <Future at 0x106ac29e8 state=finished returned str> result: 'US'

5 flags downloads in 3.81s

注意,在这个示例中调用 future.result() 方法绝不会堵塞,因为 future 由 as_completed 函数产出。

严格来说,我们目前测试的并发脚本都不能并行下载,使用 concurrent.futures 库实现的两个示例收到 GIL 的限制。

有个问题:既然 Python 线程受 GIL 的限制,任何时候都只允许运行一个线程,那个为什么 flags_threadpool.py 脚本的下载速度会比 flags.py 脚本快 5 倍?

阻塞型 I/O 和 GIL

CPython 解释器本身就不是线程安全的,因此有全局解释器锁,一次只允许使用一个线程执行 Python 字节码。因此,一个 Python 进程通常不能同时使用多个 CPU 核心(语言无关,解释器相关)。

编写 Python 代码时无法控制 GIL,不过,执行耗时的任务时,可以使用一个内置的函数或一个使用 C 语言编写的扩展释放 GIL。

然而,标准库中所有执行阻塞型 I/O 操作的函数,在等待操作系统返回结果时都会释放 GIL。这意味着在 Python 语言这个层次上可以使用多线程,而 IO 密集型 Python 程序能从中受益:一个 Python 线程等待网络响应时,阻塞型 IO 函数会释放 GIL,再运行一个线程。

Python 标准库中所有阻塞型 IO 函数都会释放 GIL,允许其他线程运行。因此,尽管有 GIL,Python 线程还是能在 IO 密集型应用中发挥作用。

下面加单说明如何在 CPU 密集型作业中使用 concurrent.futures 模块轻松绕开 GIL。

使用 concurrent.futures 模块启动进程

concurrent.futures 模块实现的是真正的并行计算,因为它使用 ProcessPoolExecutor 类把工作分配给多个 Python 进程处理,因此,如果需要做 CPU 密集型处理,使用这个模块能绕开 GIL,利用所有可用的 CPU 核心。

ProcessPoolExecutor 和 ThreadPoolExecutor 类都实现了通用的 Executor 接口,因此使用 concurrent.futures 模块能轻松地把基于线程的方案转换成基于进程的方案。

下载国旗的示例或其他 IO 密集型作业使用 ProcessPoolExecutor 类得不到任何好处,因为对于 CPU 密集型的处理来说,不吭呢要求使用超过 CPU 数量的进程,而对于 IO 密集型来说,可以在一个 ThreadPoolExecutor 实例中使用 10 个、100 个或 1000 个线程,最佳线程数取决于做的是什么事,以及可用内存是多少,因此要自习测试才能找到最佳的线程数。

ProcessPoolExecutor 的价值体现在 CPU 密集型作业上。有两个 CPU 密集型的脚本:

  • arcfour_futures.py: 这个脚本纯粹使用 Python 实现 RC4 算法;
  • sha_futures.py: 这个脚本使用标准库中的 haslib 模块(使用 OpenSSL 库实现)实现 SHA-256 算法。

这两个脚本除了显示汇总结果外,没有使用 IO,测试的结果如下:

可以看出,对加密算法来说,使用 ProcessPoolExecutor 类派生 4 个工作进程后(如果有 4 个 CPU 核心的话),性能可能提高两倍。

如果使用 Python 处理 CPU 密集型工作,应该试试 PyPy,使用 PyPy 运行上面脚本,速度快乐 3.8~5.1 倍。

实现 Executor.map 方法

下面通过一个演示程序来研究线程池的行为,这个程序会创建一个包含 3 个 worker 的线程池,运行 5 个可调用对象,输出带有时间戳的消息。

若想并发运行多个可调用对象,最简单的方式是使用 Executor.map 方法。Executor.map 函数易于使用,不过有个特性可能有用,也可能没用,具体情况取决于需求:这个函数返回结果的顺序与调用开始的顺序一致。如果第一个调用生成结果用时 10 秒,而其他调用只用 1 秒,代码会阻塞 10 秒,获取 map 方法返回的生成器产出的第一个结果。在此之后,获取后续结果时不会阻塞,因为后续的调用已经结束。如果必须等到获取所有结果后再处理,这种行为没有问题;不过,通常更可取的做法是,不管提交的顺序,只要有结果就获取。为此,要把 Executor.submit 方法和 futures.as_completed 函数结合起来使用。

executor.submit 和 futures.as_completed 这个组合比 executor.map 更灵活,因为 submit 方法能处理不同的可调用对象和参数,而 executor.map 只能处理参数不同的同一个可调用对象。此外,传给 futures.as_completed 函数的 futres 集合可以来自多个 Executor 实例,例如一些由 ThreadPoolExecutor 实例创建,另一些由 ProcessPoolExecutor 实例创建。

显示下载进度并处理错误

之前的示例脚本中没有处理错误,这么做是为了便于阅读和比较三种方案(依序、多线程和异步)的结构。

这三个示例在负责下载一个文件的函数(download_one)中使用相同的策略处理 HTTP 404 错误,其他异常则向上冒泡,交给 download_many 函数处理。

# flags2_sequential.py: 负责下载的基本函数;flags2_threadpool.py 脚本重用了这两个函数

def get_flag(base_url, cc):
    url = '{}/{cc}.gif'.format(base_url, cc=cc.lower())
    resp = requests.get(url)
    if resp.status_code != 200:
        resp.raise_for_status()
    return resp.content


def download_one(cc, base_url, verbose=False):
    try:
        image = get_flag(base_url, cc)
    except requests.exceptions.HTTPError as exc:
        res = exc.response
        if res.status_code == 404:
            status = HTTPSatus.not_fount
            msg = 'not found'
        else:
            raise
    else:
        save_flag(image, cc.lower() + '.gif')
        status = HTTPStatus.ok
        msg = 'OK'
        
    if verbose:
        print(cc, msg)
        
    return Result(status, cc)

Python 线程特别适合 IO 密集型应用,concurrent.futures 模块大大简化了某些使用场景下 Python 线程的用法。下面讨论不适合使用 ThreadPoolExecutor 或 ProcessPoolExecutor 类时,有哪些替代方案。

杂谈

concurrent.futures 包把线程、进程和队列视作服务的基础设施,不用自己动手直接处理。当然,这个包针对的是简单的作业,也就是所谓的「高度并行」问题,编写应用(而非操作系统或数据库服务器)时,遇到的大部分并发问题都属于这一种。

GIL 简化了了 CPython 解释器和 C 语言扩展的实现,得益于 GIL,Python 有很多 C 语言扩展。GIL 并不会导致 Python 线程没有用武之地,因为标准库中每一个阻塞型 IO 函数都会释放 GIL,Python 线程特别适合在 IO 密集型系统中使用。

第 18 章:使用 asyncio 包处理并发

并发是指一次处理多件事;
并行是指一次做多件事;
二者不同,但是有联系;
一个关于结构,一个关于执行;
并发用于制定方案,用来解决可能并行的问题。
—— Rob Pike(Go 语言的创造者之一)

真正的并行需要多个核心。现代的笔记本电脑有 4 个 CPU 核心,但是通常不经意间就有超过 100 个进程同时运行。因此,实际上大多数过程都是并发处理的,而不是并行处理。

本章介绍 asyncio 包,这个包使用事件循环驱动的协程实现并发。asyncio 大量使用 yield from 表达式,因此与 Python 旧版不兼容。

本章讨论以下话题:

  • 对比一个简单的多线程程序和对应的 asyncio 版,说明多线程和异步任务之间的关系
  • asyncio.Futures 类与 concurrent.futures.Future 类之间的区别
  • 第 17 章中下载国旗的那些示例的异步版
  • 摒弃线程或进程,如何使用异步编程管理网络应用中的高并发
  • 在异步编程中,与回调相比,协程显著提升性能的方式
  • 如何把阻塞的操作交给线程池处理,从而避免阻塞事件循环
  • 使用 asyncio 编写服务器,重新审视 Web 应用对高并发的处理方式
  • 为什么 asyncio 已经准备好了对 Python 生态系统产生重大影响

线程与协程对比

有一次讨论线程和 GIL 时,Michele Simionato 发布了一个简单但有趣的示例:在长时间计算的过程中,使用 multiprocessing 包在控制台中显示一个由 ASCII 字符「\/-\」构成的动画旋转指针。

我们改写了 Simionato 的示例,一个借由 threading 模块使用线程实现,一个借由 asyncio 包使用协程实现。我们这么做是为了让你对比两种实现,理解如何不使用线程来实现并发行为。

# spinner_thread.py: 通过线程以动画形式显示文本格式旋转指针
import threading
import itertools
import time
import sys


class Signal:
    go = True


def spin(msg, signal):
    write, flush = sys.stdout.write, sys.stdout.flush
    for char in itertools.cycle('|/-\\'):
        status = char + ' ' + msg
        write(status)
        flush()
        write('\x08' * len(status))  # 这是显示文本动画的诀窍所在:使用退格符(\x08)把光标移回来
        time.sleep(.1)
        if not signal.go:
            break
    write(' ' * len(status) + '\x08' * len(status))  # 使用空格键清除状态信息,把光标移回开头


def slow_function():
    # 假装等待 I/O 一段时间
    time.sleep(3)  # 调用 sleep 函数会阻塞主线程,不过一定哟啊这么做,以便释放 GIL,创建从属线程
    return 42


def supervisor():
    signal = Signal()
    spinner = threading.Thread(
        target=spin,
        args=('thinking', signal)
    )
    print('spinner object: ', spinner)
    spinner.start()
    result = slow_function()
    signal.go = False
    spinner.join()
    return result


def main():
    result = supervisor()
    print('Answer: ', result)


if __name__ == '__main__':
    main()

注意,Python 没有提供终止线程的 API,这是有意为之的。若想关闭线程,必须给线程发送消息。这里我们使用的是 signal.go 属性:在主线程中把它设置为 False 后,spinner 线程最终会注意到,然后干净地退出。

下面来看如何使用 @asyncio.coroutine 装饰器替代线程,实现相同的行为。asyncio 包使用的「协程」是较严格的定义。适合 asyncio API 的协程在定义体中必须使用 yield from,而不能使用 yield。此外,适合 asyncio 的协程要由调用方驱动,并由调用方通过 yield from 调用;或者把协程传给 asyncio 包中的某个函数,例如 asyncio.async(...) 和本章要介绍的其他函数,从而驱动协程。最后,@asyncio.coroutine 装饰器应该用在协程上,如下述示例所示。

# spinner_asyncio.py: 通过协程以动画形式显示文本式旋转指针
import asyncio
import itertools
import sys


@asyncio.coroutine
def spin(msg):
    write, flush = sys.stdout.write, sys.stdout.flush
    for char in itertools.cycle('|/-\\'):
        status = char + ' ' + msg
        write(status)
        flush()
        write('\x08' * len(status))
        try:
            yield from asyncio.sleep(.1)
        except asyncio.CancelledError:
            break
    write(' ' * len(status) + '\x08' * len(status))


@asyncio.coroutine
def slow_function():
    # 假装等待 I/O 一段时间
    yield from asyncio.sleep(3)
    return 42


@asyncio.coroutine
def supervisor():
    spinner = asyncio.async(spin('thinking!'))
    print('spinner object: ', spinner)
    result = yield from slow_function()
    spinner.cancel()
    return result


def main():
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(supervisor())
    loop.close()
    print('Answer: ', result)


if __name__ == '__main__':
    main()

若非想阻塞主线程,从而冻结事件循环或整个应用,否则不要在 asyncio 协程中使用 time.sleep(...)。如果协程需要在一段时间内什么也不做,应该使用 yield from asyncio.sleep(DELAY)。

使用 @asyncio.coroutine 装饰器不是强制要求,但是强烈建议这么做,因为这样能在一众普通的函数中把协程凸显出来,也有助于调试:如果还没从中产出值,协程就被垃圾回收了(意味着有操作未完成,因此有可能是个缺陷),那就可以发出警告。这个装饰器不会预激协程。

上面两个例子中的 supervisor 实现之间的主要区别概述如下:

  • asyncio.Task 对象差不多与 threading.Thread 对象等效,「Task 对象像是实现协作式多任务库(如 gevent)中的绿色线程(green thread)」。
  • Task 对象用于驱动协程,Thread 对象用于调用可调用的对象。
  • Task 对象不由自己手动实例化,而是通过把协程传给 asyncio.async(...) 函数或 loop.create_task(...) 方法获取。
  • 获取的 Task 对象已经排定了运行时间;Thread 实例则必须调用 start 方法,明确告知让它运行。
  • 在线程版 supervisor 函数中,slow_function 函数是普通的函数,直接由线程调用。在异步版 supervisor 函数中,slow_function 函数时协程,由 yield from 驱动。
  • 没有 API 能从外部终止线程,因为线程随时可能被终端,导致系统处于无效状态。如果想终止任务,可以使用 Task.cancel() 实例方法,在协程内部抛出 CancelledError 异常。协程可以在暂停的 yield 处捕获这个异常,处理终止请求。
  • supervisor 协程必须在 main 函数中由 loop.run_until_complete 方法执行。

使用线程编程,因为调度程序任何时候都能终端线程,所以必须记住保留锁,去保护程序中的重要部分,防止多步操作在执行的过程中终端,防止数据处于无效状态。而协程默认会做好全方位保护,以防止中断。我们必须显式产出才能让程序的余下部分运行。对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任意时刻只有一个协程运行。想交出控制权时,可以使用 yield 或 yield from 把控制权交还给调度程序。这就是能够安全地取消协程的原因:按照定义,协程只能在暂停的 yield 处取消,因此可以处理 CancelledError 异常,执行清理操作。

asyncio.Future:故意不阻塞

asyncio.Future 类与 concurrent.futures.Future 类的接口基本一致,不过实现方式不同,不可以互换。

期物只是调度执行某物的结果,在 asyncio 包中, BaseEventLoop.create_task() 方法接收一个协程,排定它的运行时间,然后返回一个 asyncio.Task 实例——也是 asyncio.Future 类的实例,因为 Task 是 Future 的子类,用于包装协程。这与调用 Executor.submit() 方法创建 concurrent.futures.Future 实例是一个道理。

使用 yield from 处理期物,等待期物运行完毕这一步无需我们关心,而且不会阻塞事件循环,因为在 asyncio 包中,yield from 的作用是把控制权交还给事件循环。

注意,使用 yield from 处理期物与使用 add_done_callback 方法处理协程的作用一样:延迟的操作结束后,事件循环不会触发回调对象,而是设置期物的返回值;而 yield from 表达式则在暂停的协程中产生返回值,回复执行协程。

总之,因为 asyncio.Future 类的目的是与 yield from 一起使用,所以通常不需要使用以下方法:

  • 无需调用 my_future.add_done_callback(),因为可以直接把像在期物运行结束后执行的操作放在协程中 yield from my_future 表达式的后面。这是协程的一大优势:协程时可以暂停和恢复的函数。
  • 无需调用 my_future.result(),因为 yield from 从期物中产出的值就是结果(例如 result = yield from my_future)。

第 19 章:动态属性和特性

在 Python 中,数据的属性和处理数据的方法统称「属性」(attribute)。其实,方法只是「可调用」的「属性」。除了这二者之外,我们还可以创建「特性」(property),在不改变类接口的前提下,使用「存取方法」(即读值方法和设值方法)修改数据属性。这与「统一访问原则」相符:

不管服务是由存储还是计算机实现的,一个模块提供的所有服务都应该通过统一的方式使用。

除了特性,Python 还提供了丰富的 API,用于控制属性的访问权限,以及实现动态属性。使用点号访问属性时(如 obj.attr),Python 解释器会调用特殊的方法(如 __getattr____setattr__)计算属性。用户自己定义的类可以通过 __getattr__ 方法实现「虚拟属性」,当访问不存在的属性时(如 obj.no_such_attribute),即时计算属性的值。

动态创建属性是一种元编程。

使用动态属性转化数据

在接下来的几个示例中,我们要使用动态属性处理 O'Reilly 为 OSCON 2016 大会提供的 JSON 格式数据源。

数据集:http://www.oreilly.com/pub/sc/osconfeed

我们编写的第一个脚本只用于下载那个 OSCON 数据源。示例 19-2 没有用到元编程。

# 示例 19-2 osconfeed.py: 下载 osconfeed.json
from urllib.request import urlopen
import warnings
import os
import json

URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'data/osconfeed.json'


def load():
    if not os.path.exists(JSON):
        msg = 'downloading {} to {}'.format(URL, JSON)
        warnings.warn(msg)
        with urlopen(URL) as remote, open(JSON, 'wb') as local:
            local.write(remote.read())
            
    with open(JSON) as fp:
        return json.load(fp)

有了 19-2 中的代码,我们可以审查数据源中的任何字段。

使用动态属性访问 JSON 类数据

示例 19-2 十分简单,不过,feed['Schedule']['speakers'][-1]['name'] 这种句法很冗长。在 JavaScript 中,可以使用 feed.Schedule.events[40].name 来获取那个值。在 Python 中,可以实现一个近似字典的类,达到同样的效果。我自己实现了 FrozenJSON 类,比大多数实现都简单,因为只支持读取,即只能访问数据。不过,这个类能递归,自动处理嵌套的映射和列表。

FrozenJSON 类的关键是 __getattr__ 方法。我们要记住重要的一点,仅当无法使用常规的方法获取属性(即在实例、类或超类中找不到指定的属性),解释器才会调用特殊的 __getattr__ 方法

如示例 19-5 所示,FrozenJSON 类只有两个方法(__init____getattr__)和一个实例属性 __data。因此,尝试获取其他属性会触发解释器调用 __getattr__ 方法。这个方法首先查看 self._data 字典中有没有指定名称的属性(不是键),这样 FrozenJSON 实例便可以处理字典中的所有方法,例如把 items 方法委托给 self.__data.items() 方法。如果 self.__data 没有指定名称的属性,那么 __getattr__ 方法一那个名称为键,从 self.__data 中获取一个元素,传给 FrozenJSON.build 方法。这样就能深入 JSON 数据的嵌套结构,使用类方法 build 把每一层嵌套转化成一个 FrozenJSON 实例。

# explore0.py: 把一个 JSON 数据集转换成一个嵌套着 FrozenJSON 
# 对象、列表和简单类型的 FrozenJSON 对象

from collections import abc


class FrozenJSON:
    """一个只读接口,使用属性表示法访问 JSON 类对象"""
    def __init__(self, mapping):
        self.__data = dict(mapping)
        
    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON.build(self.__data[name])
        
    @classmethod
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            return obj

解释一下 build 这个类方法:如果 obj 是 Mapping,那就构造一个 FrozenJSON 对象;如果是 MutableSequence 对象,必然是列表,因此,我们把 obj 中每个元素递归地传给 .build() 方法,构造一个列表;如果既不是字典也不是列表,那么原封不动地返回元素。

注意,我们没有缓存或转换原始数据源。在迭代数据源的过程中,嵌套的数据结构不断被转换成 FrozenJSON 对象。这么做没问题,因为数据集不大,而且这个脚本只用于访问或转换数据。

处理无效的属性名

FrozenJSON 类有个缺陷:没有对名称为 Python 关键字的属性做特殊处理。

FrozenJSON 类的目的是为了便于访问数据,因此更好的方法是检查传给 FrozenJSON.__init__ 方法的映射中是否有键的名称为关键字,如果有,那么在键名后加上 _,然后通过 xxx.class_ 这样的方式读取。

解决方法:

class FrozenJSON:
    """一个只读接口,使用属性表示法访问 JSON 类对象"""
    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if keyword.iskeyword(key):
                key += '_'
            self.__data[key] = value

对动态属性的名称做了一些处理之后,我们要分析 FrozenJSON 类的另一个重要功能——类方法 build 的逻辑。这个方法吧嵌套结构转换成 FrozenJSON 实例或 FrozenJSON 实例列表,因此 __getattr__ 方法使用这个方法访问属性时,能为不同的值返回不同类型的对象。

使用 __new__ 方法以灵活的方式创建对象

除了在类方法实现 build 的逻辑之外,还可以在 __new__ 方法中实现。

我们通常把 __init__ 称为构造方法,这是从其他语言借鉴过来的术语。其实,用于构造实例的是特殊方法 __new__:这是个类方法(使用特殊方式处理,因此不必使用 @classmethod 装饰器),必须返回一个实例。返回的实例会作为第一个参数(即 self)传给 __init__ 方法。因为调用 __init__ 方法时要传入实例,而且禁止返回任何值,所以 __init__ 方法其实是「初始化方法」,真正的构造方法时 __new__。我们几乎不需要自己编写 new 方法,因为从 object 类继承的实现已经足够了。

刚才说明的过程,即从 new 方法到 init 方法,是最常见的,但不是唯一的。new 方法也可以返回其他类的实例,此时,解释器不会调用 init 方法。

也就是说,Python 构建对象的过程可以使用下述伪代码描述:

def object_maker(the_class, some_arg):
    new_object = the_class.__new__(some_arg)
    if isinstance(new_object, the_class):
        the_class.__init__(new_object, some_arg)
    return new_object
    
# 下述两个语句的作用基本等效
x = Foo('bar')
x = object_maker(Foo, 'bar')

下面是 FrozenJSON 的另一个版本,把之前在类方法 build 中的逻辑移动到了 new 方法中。

# explore2.py: 使用 new 方法取代 build 方法,构建可能是也可能不是 FrozenJSON 实例的新对象

from collections import abc
import keyword


class FrozenJSON:
    """一个只读接口,使用属性表示法访问 JSON 类对象"""
    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if keyword.iskeyword(key):
                key += '_'
            self.__data[key] = value
        
    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON(self.__data[name])
        
    def __new__(cls, obj):
        if isinstance(obj, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            return obj

new 方法的第一个参数时类,因为创建的对象通常是那个类的实例。所以,在 FrozenJSON.__new__ 方法中,super().__new__(cls) 表达式会调用 object.__new__(FrozenJSON),而 object 类构建的实例其实是 FrozenJSON 实例,即那个实例的 __class__ 属性存储的是 FrozenJSON 类的引用。

使用 shelve 模块调整 OSCON 数据源的结构

pickle 是 Python 对象序列化格式的包,shelve 模块提供了 pickle 存储方式。

shelve.open 高阶函数返回一个 shelve.Shelf 实例,这是简单的键值对象数据库,背后由 dbm 模块支持,具有下述特点:

  • shelve.Shelf 是 abc.MutableMapping 的子类,因此提供了处理映射类型的重要方法;
  • 此外,shelve.Shelf 类还停工了几个管理 I/O 的方法,如 sync 和 close;它也是一个上下文管理器;
  • 只要把新值赋予键,就会保存键和值;
  • 键必须是字符串;
  • 值必须是 pickle 模块能处理的对象。

现在值得关注的是,shelve 模块为识别 OSCON 的日程数据提供了一种简单有效的方式。我们将从 JSON 文件中读取所有记录,将其存在一个 shelve.Shelf 对象中,键由记录类型和编号组成(例如,'event.33050'),而值使我们即将定义的 Record 类的实例。

# schedule1.py: 访问保存在 shelve.Shelf 对象里的 OSCOn日程数据

import warnings
import osconfeed

DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'


class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        

def load_db(db):
    raw_data = osconfeed.load()
    warnings.warn('loading ' + DB_NAME)
    for collections, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1]
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            db[key] = Record(**record)

Record.__init__ 方法展示了一个流行的 Python 技巧。我们知道,对象的 __dict__ 属性中存储着对象的属性——前提是类中没有声明 __slots__ 属性。因此,更新实例的 dict 属性,把值设为一个映射,能快速地在那个实例中创建一堆属性。

像上面那样调整日程和数据集之后,我们可以扩展 Record 类,让它提供一个有用的服务:自动获取 event 记录引用的 venue 和 speaker 记录。这与 Django ORM 访问 models.ForeignKey 字段时所做的事类似:得到的不是键,而是链接的模型对象。

使用特性获取链接的记录

下一个版本的目标是,对于从 Shelf 对象中获取的 event 记录来说,读取它的 venue 或 speakers 属性时返回的不是编号,而是完整的记录对象,用法如下所示:

上图绘出了本节要分析的几个类。

  • Record
    • __init__ 方法与 schedule1.py 脚本中的一样;为了辅助测试,添加了 __eq__ 方法;
  • DbRecord
    • Record 类的子类,添加了 __db 类属性,用于设置和获取 __db 属性的 set_db 和 get_db 静态方法,用于从数据库中获取记录的 fetch 类方法,以及辅助调试和测试的 __repr__ 实例方法;
  • Event
    • DBRecord 类的子类,添加了用于获取所链接记录的 venue 和 speakers 属性,以及特殊的 repr 方法。

DbRecord.__db 类属性的作用是存储打开的 shelve.Shelf 数据库引用,以便在需要使用数据库的 DbRecord.fetch 方法以及 Event.venue 和 Event.speakers 属性中使用。我把 __db 设为私有类属性,然后定义了普通的读值方法和设值方法,以防不小心覆盖 __db 属性的值。基于一个重要的原因,我没有使用特性去管理 __db 属性:特性是用于管理实例属性的类属性。

《算法》笔记(二)· 排序

2.1 初级排序算法

2.1.1.3 额外的内存使用

排序算法的额外内存开销和运行时间是同等重要的。排序算法可以分为两类:

  • 原地排序算法
  • 需要额外内存空间来存储的其他排序算法

2.1.2 选择排序

选择排序两个鲜明的特点:

  1. 运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶地发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长。
  2. 数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了 N 次交换——交换次数和数组的大小是线性关系。

选择排序实现:

def selection_sort(a):
    N = len(a)
    for i in range(N):
        mi = i
        for j in range(i+1, N):
            if a[j] < a[mi]:
                mi = j
        a[i], a[mi] = a[mi], a[i]
    return a

2.1.3 插入排序

与选择排序一样,当前索引左边的所有元素都是有序的,但它们最终的最终位置还不确定,为了给更小元素腾出空间,它们可能会被移动。但是当前索引到达数组的右端时,数组排序就完成了。

与选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。

插入排序对于实际应用中常见的某些类型的非随机数组很有效。当插入排序对一个有序数组进行排序,运行时间是线性的。

def insertion_sort(a):
    for i in range(1, len(a)):
        for j in range(i, 0, -1):
            if a[j] < a[j-1]:
                a[j], a[j-1] = a[j-1], a[j]
    return a

对于 1 到 N-1 之间的每一个 i,将 a[i] 与 a[0] 到 a[i-1] 中比它小的所有元素依次有序地交换。在索引 i 由左向右变化的过程中,它左侧的元素总是有序的,所以当 i 到达数组的右端时排序就完成了。

我们要考虑的更一般的情况是部分有序的数组。倒置指的是数组中的两个顺序颠倒的元素。比如 E X A M P L E 中有 11 对倒置:E-A X-A X-M X-P X-L X-E M-L M-E ... 如果数组中倒置的数量小于数组大小的某个倍数,那么我们说这个数组是部分有序的。

下面是几种典型的部分有序的数组:

  • 数组中每个元素都距离它的最终位置都不远
  • 一个有序的大数组接一个小数组
  • 数组中只有几个元素的位置不正确

插入排序对这样的数组很有效,而选择排序则不然。事实上,当倒置数量很少时,插入排序很可能比本章中的其他任何算法都要快。

要大幅提高插入排序的速度并不难,只需要在内循环中将较大的元素都向右移动而不总是交换两个元素。

2.1.6 希尔排序

为了展示初级排序算法性质的价值,我们将学习一种基于插入排序的快速排序算法。对于大规模乱序数组插入排序很慢,因为它智慧交换相邻的元素,因此元素只能一点一点地从数组的一段移动到另一端。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终插入排序将局部有序的数组排序。

希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。这样的数组被称为 h 有序数组。换句话说,h 有序数组就是 h 个独立的有序数组编织在一起组成的一个数组,如下图所示。

实现希尔排序的一种方法是对于每个 h,用插入排序将 h 个子数组独立地排序。但因为子数组是相互独立的,一个更简单的方法是在 h-子数组中将每个元素交换到比它大的元素之前去(将比它大的元素向右移动一格)。只需要在插入排序的代码中将移动元素的距离由 1 改为 h 即可。这样,希尔排序的实现就转化为了一个类似于插入排序但是用不同增量的过程。

希尔排序更高效的原因是它权衡了子数组的规模性和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。

def shell_sort(a):
    h = 1
    while h < len(a) / 3:
        h = 3 * h + 1
    while h >= 1:
        for i in range(int(h), len(a)):
            for j in range(i, int(h)-1, -1):
                if a[j] < a[j - int(h)]:
                    a[j], a[j - int(h)] = a[j - int(h)], a[j]
        h = h / 3
    return a

2.2 归并排序

归并:即将两个有序数组归并成一个更大的有序数组。

2.2.1 原地归并的抽象方法

实现归并的一种直截了当的方法是将两个不同的有序数组归并到第三个数组中。

但是,当用归并将一个很大的数组排序时,我们需要进行很多次归并,因此在每次归并时都创建一个新数组来存储排序结果会带来问题。我们更希望有一种能够在原地归并的方法,这样就可以先将前半部分排序,再将后半部分排序,然后在数组中移动元素而不需要使用额外的空间。

原地归并的抽象方法:


《Python 核心编程》笔记(一)· 网络编程

2.3 套接字

本节内容:

  • 介绍套接字(socket),以及其背景知识
  • 讨论各种类型的套接字
  • 如何利用它们与运行在不同计算机上的进程相互通信

2.3.1 套接字

套接字是计算机网络数据结构,它体现了上节中所描述得「通信端点」1的概念。在任何类型的通信开始之前,网络应用程序必须创建套接字。

套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个进程与另一个运行的进程进行通信,这就是所谓的进程间通信(Inter Process Communication,IPC)。有两种类型的套接字:基于文件的和面向网络的。

UNIX 套接字使我们所讲的套接字的第一个家族,并且拥有一个「家族名字」AF_UNIX(又名 AF_LOCAL,在 POSIX1.g 标准中指定),它代表地址家族(address family):UNIX。包括 Python 在内的大多数受欢迎的平台都使用术语「地址家族」及其所写 AF。

因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。

第二种类型的套接字是基于网络的,它也有自己的家族名字 AF_INET,或者「地址家族」:因特网。另一个地址家族 AF_INET6 用于第 6 版因特网协议(IPv6)寻址。

2.3.2 套接字地址:主机-端口对

一个网络地址由主机名和端口号组成,而这是网络通信所需要的。

有效地端口号范围为 0~65536(小于 1024 的端口号预留给了系统)。

可以查看 \etc\services 文件查看预留端口号的列表(以及服务器、协议和套接字类型)。

2.3.3 面向连接的套接字与无连接的套接字

1. 面向连接的套接字

不管采用哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在通信前必须先建立一个连接。这种类型的通信业称为「虚拟电路」或「流套接字」。

面向连接的通信提供序列化的、可靠的和不重复的数据交互,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每条消息片段都确保能够达到目的,然后按照顺序将它们组合到一起,最后将完整的消息传递给正在等待的应用程序。

实现这种连接类型的主要协议是「传输控制协议」(TCP 协议)。为了创建 TCP 套接字,必须使用 SOCK_STREAM 作为套接字类型。TCP 套接字的名字 SOCK_STREAM 是基于流套接字的一种表示。因为这些套接字(AF_INET) 的网络版本使用「因特网协议」(IP) 来搜寻网络中的主机,所以整个系统通常结合着两种协议(TCP 和 IP) 来进行。

2. 无连接的套接字

与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。这意味着,在通信开始之前不需要建立连接。

此时,在数据传输的过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而非首先分成多个片段。

由于面向连接的的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。数据报不需要这些,所以它的成本更加低廉。因此,它们通常能提供更好的性能。

实现这种无连接类型的主要协议是「用户数据报协议」(UDP)。为了创建 UDP 套接字,必须使用 SOCK_DGRAM (来源于单词 datagram)作为套接字类型。因为这些套接字也使用因特网协议来寻找网络中的主机,所以这个系统也有一个更加普通的名字,即 UDP/IP。

2.4 Python 中的网络编程

2.4.1 socket() 模块函数

要创建套接字,必须使用 socket.socket() 函数,它的一般语法如下:

socket(socket_family, socket_type, protocol=0)

其中,socket_family 是 AF_UNIX 或 AF_INET,socket_type 是 SOCKET_STREAM 或 SOCK_DGRAM。protocol 通常省略,默认为 0。

tcpSock = socket.socket(socket.AF_INET, sock.SOCK_STREAM)
udpSock = socket.socket(socket.AF_INET, sock.SOCK_DGRAM)

一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步交互。

2.4.2 套接字对象(内置)方法

  • 服务器套接字方法
    • s.bind(): 将地址(主机名、端口号对)绑定到套接字上
    • s.listen(): 设置并启动 TCP 监听器
    • s.accept(): 被动接受 TCP 客户端连接,一直等待知道连接到达
  • 客户端套接字方法
    • s.connect(): 主动发起 TCP 服务器连接
    • s.connect_ex(): connect() 的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常
  • 普通套接字方法
    • s.recv(): 接受 TCP 消息
    • s.recv_into(): 接受 TCP 消息到指定的缓冲区
    • s.send(): 发送 TCP 消息
    • s.sendall(): 完整地发送 TCP 消息
    • s.recvfrom(): 接受 UDP 消息
    • s.recvfrom_into(): 接收 UDP 消息到指定的缓冲区
    • s.sendto(): 发送 UDP 消息
    • s.getpeername(): 连接到套接字(TCP) 的远程地址
    • s.getsockname(): 当前套接字地址
    • s.getsockopt(): 返回给定套接字选项的值
    • s.setsockopt(): 设置给定套接字选项的值
    • s.shutdown(): 关闭连接
    • s.close(): 关闭套接字
    • s.detach(): 在未关闭文件描述符的情况下关闭套接字,返回文件描述符
    • s.ioctl(): 控制套接字的模式(仅支持 Windows)
  • 面向阻塞的套接字方法
    • s.setblocking(): 设置套接字的阻塞或非阻塞模式
    • s.settimeout(): 设置阻塞套接字操作的超时时间
    • s.gettimeout(): 获取阻塞套接字操作的超时时间
  • 面向文件的套接字方法
    • s.fileno(): 套接字文件描述符
    • s.makefile(): 创建于套接字关联的文件对象
  • 数据属性
    • s.family: 套接字家族
    • s.type: 套接字类型
    • s.proto: 套接字协议

  1. 在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作准备。首先会创造一个通信端点,它能够使服务器监听请求。 

Python 面试笔记

Python 语言特性

Python 的函数参数传递

所有的变量都可以理解为内存中一个对象的引用。

类型是属于对象的,而不是变量。而对象有两种,「可更改」(mutable) 与「不可更改」(inmutable) 对象。==在 Python 中,strings, tuples, numbers 是不可更改的对象,list, dict 则是可以更改的对象。

当一个引用传递给函数的时候,函数自动复制一份引用,这个函数里的引用就和外部的引用没有关系了。而在函数内的引用指向的是可变对象的时候,对它的操作就和定位了指针地址一样,可以直接在内存里进行修改。

参考阅读:http://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference

Python 中的元类

ORM 中会用到。

详细的解释:http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python

@staticmethod 和 @classmethod

Python 有三个方法:即静态方法(staticmethod)、类方法(classmethod)和实例方法。

def foo(x):
    print "executing foo(%s)"%(x)

class A(object):
    def foo(self,x):
        print "executing foo(%s,%s)"%(self,x)

    @classmethod
    def class_foo(cls,x):
        print "executing class_foo(%s,%s)"%(cls,x)

    @staticmethod
    def static_foo(x):
        print "executing static_foo(%s)"%x

a=A()

selfcls 是对类或者实例的绑定。对于一般的函数我们可以这么调用 foo(x),这个函数就是最常用的,它的工作和任何东西(类、实例)无关。对于实例方法,我们知道在类中每次定义方法都需要绑定这个实例,即 foo(self, x) ,因为实例方法的调用离不开实例,我们需要把实例自己传给函数,调用的时候是这样的 a.foo(x)(其实是 foo(a, x))。类方法一样,只不过它传递的是类而不是实例,A.class_foo(x)。注意这里的 self 和 cls 可以替换成别的参数,但是 Python 的约定就是这两个,所以还是不要改比较好。

对于静态方法其实和普通方法一样,不需要对谁进行绑定,唯一的区别就是需要使用 a.static_foo(x) 或者 A.static_foo(x) 这样的方式来调用。

实例方法 类方法 静态方法
a = A() a.foo(x) a.class_foo(x) a.static_foo(x)
A 不可用 A.class_foo(x) A.static_foo(x)

详细讨论:http://stackoverflow.com/questions/136097/what-is-the-difference-between-staticmethod-and-classmethod-in-python

类变量和实例变量

class Person:
    name="aaa"

p1=Person()
p2=Person()
p1.name="bbb"
print p1.name  # bbb
print p2.name  # aaa
print Person.name  # aaa

类变量就是供类使用的变量,实例变量就是供实例使用的。

这里的 p1.name="bbb" 是实例调用了类变量,这和上面的一个问题一样,就是函数传参的问题,p1.name 一开始指向的是类变量 Person.name,但在实例的作用域里把类变量的引用改变了,就变成了一个实例变量。

可以对比一下下面这个例子:

class Person:
    name=[]

p1=Person()
p2=Person()
p1.name.append(1)
print p1.name  # [1]
print p2.name  # [1]
print Person.name  # [1]

参考:http://stackoverflow.com/questions/6470428/catch-multiple-exceptions-in-one-line-except-block

Python 自省

自省就是面向对象的语言所写的程序在运行时,所能知道对象的类型。简单讲就是运行时能够获得对象的类型,比如 type(), dir(), getattr(), hasattr(), isinstance()

字典推导式

2.7 中加入的内容。

d = {key: value for (key, value) in iterable}

Python 中单下划线与双下划线的区别

>>> class MyClass():
...     def __init__(self):
...             self.__superprivate = "Hello"
...             self._semiprivate = ", world!"
...
>>> mc = MyClass()
>>> print mc.__superprivate
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: myClass instance has no attribute '__superprivate'
>>> print mc._semiprivate
, world!
>>> print mc.__dict__
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}
  • __foo__:一种约定,Python 内部的名字,用来区别其他用户自定义的命名,以防冲突
  • _foo:一种约定,用来指定变量是私有变量
  • __foo:解析器用 _classname__foo 来代替这个名字,以区别和其他类相同的命名。

参考:

迭代器和生成器

详细解释:https://taizilongxu.gitbooks.io/stackoverflow-about-python/content/1/README.html

*args**kwargs

使用 *args**kwargs 只是为了方便,并没有强制使用它们。

当你不确定函数中将要传入多少个参数时可以使用 *args ,它可以传递任意数量的参数。

**kwargs 允许你使用事先未定义的参数名。

你也可混着使用,命名参数首先获得参数值,然后其他所有的参数都传给 *args**kwargs。命名参数在列表的最前端:

def table_things(titlestring, **kwargs)

*args**kwargs 可以同时在函数的定义中,但是 *args 必须在 **kwargs 前面。

在调用函数时也可以使用 *** 语法,例如:

>>> def print_three_things(a, b, c):
...     print 'a = {0}, b = {1}, c = {2}'.format(a,b,c)
...
>>> mylist = ['aardvark', 'baboon', 'cat']
>>> print_three_things(*mylist)

a = aardvark, b = baboon, c = cat

它可以传递列表(或元组)并将它们解包。注意必须与它们在函数中的参数相吻合

参考:http://stackoverflow.com/questions/3394835/args-and-kwargs

面向切面编程 AOP 和装饰器

装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数本身无关的雷同代码并继续重用。概括来讲,装饰器的作用就是为已经存在的对象添加额外的功能

详细参考:

什么是 AOP:

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。

AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

鸭子类型

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

我们不关心对象是什么类型,到底是不是鸭子,只关心行为。

比如在 Python 中,有很多 file-like 的东西,StringIO/GzipFile/socket。它们有很多相同的方法,我们把它们都当做文件使用。

鸭子类型在动态语言中经常使用,非常灵活。使得 Python 不必像 Java 那样专门弄一堆设计模式。

Python 中的重载

Python 不支持函数重载

函数重载主要是为了解决两个问题:

  • 可变参数类型
  • 可变参数个数

另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数其实功能不同,那么不应当使用重载,而应当使用一个名字不同的函数。

对于情况 1,函数功能相同,但参数类型不同, 在 Python 中根本不需要处理,因为 Python 可以接受任何类型的参数,如果函数功能相同,那么不同的参数类型在 Python 中很可能是相同的代码,没有必要写成两个不同的函数。

对于情况 2,函数功能相同,但参数个数不同,Python 中处理的方式是使用缺省参数。对那些缺少的参数设定为缺省参数即可。

鉴于情况 1 和情况 2 Python 都有解决方案,所以就不需要重载了。

但是 Python 也可以实现重载的功能。

参考:http://www.zhihu.com/question/20053359

新式类和旧式类

详细参考:

__new____init__

  • __new__ 是一个静态方法,而 __init__ 是一个实例方法
  • __new__ 方法会返回一个创建的实例,而 __init__ 什么都不返回
  • 只有在 __new__ 返回一个 cls 实例时后面的 __init__ 才能被调用
  • 当创建一个新实例时调用 __new__,初始化一个实例时用 __init__

参考:http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init

单例模式

Important!

什么是单例模式

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

1. 使用 __new__ 方法

class Singleton(object):
    def __new__(cls, *args, **kw):
        if not hasattr(cls, '_instance'):
            orig = super(Singleton, cls)
            cls._instance = orig.__new__(cls, *args, **kw)
        return cls._instance

class MyClass(Singleton):
    a = 1

2. 共享属性

创建实例时把所有实例的 __dict__ 指向同一个字典,这样它们具有相同的属性和方法。

class Borg(object):
    _state = {}
    def __new__(cls, *args, **kw):
        ob = super(Borg, cls).__new__(cls, *args, **kw)
        ob.__dict__ = cls._state
        return ob

class MyClass2(Borg):
    a = 1

3. 装饰器版本

def singleton(cls, *args, **kw):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]
    return getinstance

@singleton
class MyClass:
  ...

4. import 方法

# mysingleton.py
class My_Singleton(object):
    def foo(self):
        pass

my_singleton = My_Singleton()

# to use
from mysingleton import my_singleton

my_singleton.foo()

作为 Python 的模块是天然的单例方法。

Python 中的作用域

Python 中,一个变量的作用域总是由在代码中被赋值的地方所决定的。

当 Python 遇到一个变量的话它会按照如下顺序进行搜索:

本地作用域(Local) -> 当前作用域没嵌入的作用域(Enclosing locals) -> 全局/模块作用域(Global) -> 内置作用域(Built-in)

GIL 线程全局锁

线程全局锁(Global Interpreter Lock),即 Python 为了保证线程安全而采取的独立线程运行的机制,说白了就是一个核只能在同一时间运行一个线程。

解决办法就是多进程和下面的协程(协程也只是单 CPU,但能减小切换代价提升性能)。

参考:http://www.oschina.net/translate/pythons-hardest-problem

协程

待补充

闭包

闭包(Closure)是函数式编程中的重要语法结构。

当一个内嵌函数引用其外部作用域的变量,我们就会得到一个闭包。 创建一个闭包必须满足以下几点:

  • 必须有一个内嵌函数
  • 内嵌函数必须引用外部函数的变量
  • 外部函数的返回值必须是内嵌函数

lambda 函数

Python 中的 lambda 函数即匿名函数。

参考:https://www.zhihu.com/question/20125256

Python 函数式编程

详细参考:http://coolshell.cn/articles/10822.html

Python 里的拷贝

引用和 copy(), deepcopy() 的区别:

import copy
a = [1, 2, 3, 4, ['a', 'b']]  #原始对象

b = a  #赋值,传对象的引用
c = copy.copy(a)  #对象拷贝,浅拷贝
d = copy.deepcopy(a)  #对象拷贝,深拷贝

a.append(5)  #修改对象a
a[4].append('c')  #修改对象a中的['a', 'b']数组对象

print 'a = ', a
print 'b = ', b
print 'c = ', c
print 'd = ', d

输出结果:
a =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
b =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
c =  [1, 2, 3, 4, ['a', 'b', 'c']]
d =  [1, 2, 3, 4, ['a', 'b']]

Python 垃圾回收机制

Python GC 主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用基础上,通过「标记-清除」(mark and sweep)解决容器对象可能产生的循环引用问题,通过「分代回收」(generation collection)以空间换时间的方法提高垃圾回收效率。

1. 引用计数

PyObject 是每个对象必有的内容,其中,ob_refcnt 就是作为引用计数。当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象呗删除,它的 ob_refcnt 就会减少。引用计数为 0 时,该对象的生命就结束了。

优点:

  • 简单
  • 实时性

缺点:

  • 维护引用计数消耗资源
  • 循环引用

2. 「标记-清除」机制

基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为结点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。

3. 分代技术

分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个「代」,垃圾收集频率随着「代」存货时间的增大而减小,存货时间通常利用经过几次垃圾回收来度量。

Python 默认定义了三代对象集合,索引数越大,对象存货时间越长。

举例:当某些内存块 M 经过了 3 此垃圾收集的清洗之后还存活时,我们就将内存块 M 划分到一个集合 A 中去,而新分配的内存都划分到集合 B 中去。当垃圾收集开始工作时,大多数情况都只对集合 B 进行垃圾回收,而对集合 A 进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存减少了,效率自然就提高了。在这个过程中,集合 B 中的某些内存块由于存货时间长而会被转移到集合 A 中,当然,集合 A 中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代机制而被延迟。

Python 中 list 的实现

详细参考:Python 中 List 的实现

Python 的 is

is 是对比地址,== 是对比值

read/readline/readlines

  • read 读取整个文件
  • readline 读取下一行,使用生成器的方式
  • readlines 读取整个文件到一个迭代器以供我们遍历

Python 2 和 3 的区别

详细参考:http://chenqx.github.io/2014/11/10/Key-differences-between-Python-2-7-x-and-Python-3-x/

操作系统

select、poll 和 epoll

待补充。

调度算法

  1. 先来先服务(FCFS, First Come First Server)
  2. 短作业优先(SJF, Shortest Job First)
  3. 最高优先权调度(Priority Scheduling)
  4. 时间片轮转(RR, Round Robin)
  5. 多级反馈队列调度(multilevel feedback queue scheduling)

实时调度算法:

  1. 最早截止时间优先 EDF
  2. 最低松弛度优先 LLF

死锁

原因:

  1. 竞争资源
  2. 程序推进顺序不当

必要条件:

  1. 互斥条件
  2. 请求和保持条件
  3. 不剥夺条件
  4. 环路等待条件

处理死锁的基本方法:

  1. 预防死锁
  2. 避免死锁(银行家算法)
  3. 检测死锁(资源分配图)
  4. 解除死锁
    • 剥夺资源
    • 撤销进程

程序的编译和链接

待补充。

静态链接和动态链接

静态链接方法:静态链接的时候,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来,静态库的链接可以使用静态链接,动态链接库也可以使用这种方法链接导入库。

动态链接方法:使用这种方式的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序有需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以这种方式使程序初始化的时间较短,但运行期间的性能比不上静态链接的程序。

虚拟内存技术

虚拟存储器是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储系统。

分页和分段

分页:用户程序的地址空间被划分为若干个固定大小的区域,称为「页」,相应的,内存空间分成若干个物理块,页和块的大小相等,可将用户程序的任一页放在内存的任一块中,实现了离散分配。

分段:将用户程序的地址空间分成若干个大小不等的「段」,每段可以定义一组相对完整的逻辑信息。存储分配时,以段位单位,段与段在内存中可以不相邻接,也实现了离散分配。

分页与分段的主要区别:

  1. 页的信息是物理单位,分页是为了实现非连续分配,以便解决内存碎片问题,或者说分页是由于系统管理的需要,段是信息的逻辑单位,它含有一组意义相对完整的信息,分段的目的是为了更好得实现共享,满足用户的需要。
  2. 页的大小固定,由系统决定,将逻辑地址划分为页号和页内地址是由及其硬件实现的。而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时根据信息的性质来划分。
  3. 分页的作业地址空间是一维的,分段的地址空间是二维的。

页面置换算法

  1. 最先置换算法 OPT:不可能实现
  2. 先进先出 FIFO
  3. 最近最久未使用算法 LRU:最近一段时间里最久没有使用过的页面予以置换
  4. clock 算法

边缘触发和水平触发

边缘触发是指每当状态发生变化时发生一个 IO 事件,条件触发是只要满足条件就发生一个 IO 事件。

数据库

事务

数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作,要么完全得执行,要么完全得不执行。

数据库索引

参考资料:http://blog.codinglabs.org/articles/theory-of-mysql-index.html

Redis 原理

待补充。

乐观锁和悲观锁

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
  • 乐观锁:假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性

网络

ARP 协议

地址解析协议(Address Resolution Protocol),其基本功能为透过目标设备的 IP 地址,查询目标的 Mac 地址,以保证通信的顺利进行。它是 IPv4 网络层必不可少的协议,不过在 IPv6 中已不再适用,并被邻居发现协议(NDP)替代。

POST 和 GET

参考阅读:

Cookie 和 Session

Cookie Session
存储位置 客户端 服务器端
目的 跟踪会话,也可以保存用户偏好设置或者保存用户名密码等 跟踪会话
安全性 不安全 安全

Session 技术是要使用到 Cookie 的,之所以出现 Session 技术,主要是为了安全。

Apache 和 Nginx 的区别

Nginx 相对于 Apache 的优点:

  • 轻量级:同样的 Web 服务,比 Apache 占用更少的内存及资源
  • 抗并发:Nginx 处理请求时异步非阻塞的,支持更多的并发连接,而 Apache 则是阻塞型的,在高并发下 Nginx 能保持低资源低消耗高性能
  • 配置简洁
  • 高度模块化的设计,编写模块相对简单
  • 社区活跃

Apache 相对 Nginx 的优点:

  • rewrite 比 Nginx 的 rewrite 强大
  • 模块很多,基本可以想到的都能找到
  • bug 少,Nginx bug 多
  • 很稳定

网站用户密码保存

  • 明文保存
  • 明文 hash 后保存,如 md5
  • MD5 + salt 方式,这个 salt 可以随机
  • 知乎使用了 Bcrypy 加密(待考)

HTTP 和 HTTPS

待补充。

XSRF 和 XSS

参考 计算机网络

CGI 和 WSGI

CGI 是通用网关接口,是连接 web 服务器和应用程序的接口,用户通过 CGI 来获取动态数据或文件等。CGI 程序是一个独立的程序,它可以用几乎所有语言来写。

WSGI(Web Server Gateway Interface),是 Python 应用程序或 Web 框架与服务器之间的一种接口,WSGI 的其中一个目的就是让用户可以用统一的语言(Python)来编写前后端。

中间人攻击

中间人攻击(Man in the middle attack, MITM)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为它们正在通过一个私密的连接与对方直接对话,但事实上整个对话都被攻击者完全控制。

CK10 问题

指的是服务器同时支持成千上万个客户端的问题。

参考阅读:http://www.kegel.com/c10k.html

Ajax

Ajax(Asynchronous JavaScript and Xml,异步的 JavaScript 和 XML),是指在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术。

计算机网络

HTTP 协议

HTTP 的特性

  • HTTP 构建于 TCP/IP 协议之上
  • HTTP 是无连接状态的

HTTP 报文

请求报文

HTTP 定义了与服务器交互的不同方法,最基本的方法有 4 种,分别是 GET, POST, PUT, DELETE. URL 全称是资源描述符,我们可以这样任务,一个 URL 地址,它用于描述一个网络上的资源,而 HTTP 中的 GET、POST、PUT、DELETE 就对应着对这个资源的差、改、增、删 4 个操作。

  1. GET 用于信息获取,而且应该是安全的幂等的

    所谓安全的意味着该操作用于获取信息而非修改信息。换句话说,GET 请求一般不应产生副作用,不会影响资源的状态。

    幂等的意味着对同一 URL 的多个请求应该返回同样的结果。

    GET 请求报文示例:

    GET /books/?sex=man&name=Professional HTTP/1.1
    Host: www.wrox.com
    User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)
    Gecko/20050225 Firefox/1.0.1
    Connection: Keep-Alive
    
  2. POST 表示可能修改服务器上的资源

    POST / HTTP/1.1
    Host: www.wrox.com
    User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)
    Gecko/20050225 Firefox/1.0.1
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 40
    Connection: Keep-Alive
    
    sex=man&name=Professional
    
  3. 注意

    • GET 可提交的数据量受 URL 长度的限制,HTTP 协议规范没有对 URL 长度进行限制。这个限制是特定的浏览器对它的限制
    • 理论上讲,POST 是没有大小限制的,HTTP 协议规范也没有进行大小限制,处于安全考虑,服务器软件在实现时会做一定限制
    • 参考上面的报文示例,可以发现 GET 和 POST 数据内容是一模一样的,只是位置不同,一个在 URL 里,一个在 HTTP 包的包体里

响应报文

HTTP 响应与 HTTP 请求相似,HTTP 响应也由 3 个部分构成,分别是:

  • 状态行
  • 响应头(Response Header)
  • 响应正文

状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。

常见的状态码有如下几种:

  • 200 OK:客户端请求成功
  • 302 Moved Temporirily:请求重定向
  • 304 Not Modified:文件未修改,可以直接使用缓存文件
  • 400 Bad Request:由于客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthonzed:请求未经授权。这个状态码必须和 WWW-Authenticate 报头域一起使用
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
  • 404 Not Found:请求的资源不存在,例如输入了错误的 URL
  • 500 Internal Server Error:服务器发生不可预期的错误,导致无法完成客户端的请求
  • 503 Service Unavailable:服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会回复正常

下面是一个 HTTP 响应的例子:

HTTP/1.1 200 OK
Server:Apache Tomcat/5.0.12
Data: Mon,60ct2003 13:23:52 GMT
Content-Length:112

<html>...

条件 GET

HTTP 条件 GET 是 HTTP 协议为了减少不必要的带宽浪费,提出的一种方案。详见:RFC2616

  1. HTTP 条件 GET 使用的时机

    客户端之前已经访问过某网站,并打算再次访问该网站

  2. HTTP 条件 GET 使用的方法

    客户端想服务器发送一个包询问是否在上一次访问网站的时间后更改了页面,如果服务器没有更新,显然不需要把整个网页传给客户端,客户端只要使用本地缓存即可,如果服务器对照客户端给出的时间已经更新了客户端请求的网页,则发送这个更新了的网页给用户。

下面是一个具体的发送接收报文的示例:

客户端发送请求:

GET / HTTP/1.1
Host: www.sina.com.cn:80
If-Modified-Since:Thu, 4 Feb 2010 20:39:13 GMT
Connection: Close

第一次请求时,服务器端发送请求数据,之后的请求,服务器根据请求中的 If-Modified-Since 字段判断响应文件没有更新,如果没有更新,服务器返回一个 304 Not Modified 响应,告诉浏览器请求的资源在浏览器上没有更新,可以使用已缓存的上次获取的文件。

HTTP/1.0 304 Not Modified
Date: Thu, 04 Feb 2010 12:38:41 GMT
Content-Type: text/html
Expires: Thu, 04 Feb 2010 12:39:41 GMT
Last-Modified: Thu, 04 Feb 2010 12:29:04 GMT
Age: 28
X-Cache: HIT from sy32-21.sina.com.cn
Connection: close

如果服务器端资源已经更新的话,就返回正常的响应。

持久连接

我们知道 HTTP 协议采用 「请求-应答」模式,当使用普通模式,即非 Keep-Alive 模式时,每个请求/应答客户和服务器都要新建一个连击,完成之后立即断开连接(HTTP 协议为无连接协议);当使用 Keep-Alive 模式(又称持久连接、连接重用)时,Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后续请求时,Keep-Alive 功能避免了建立或者重新建立连接。

在 HTTP 1.0 版本中,并没有官方的标准来规定 Keep-Alive 如何工作,因此实际它是被附加到 HTTP 1.0 协议上,如如过客户端浏览器支持 Keep-Alive,那么就在 HTTP 请求头中添加一个字段 Connection: Keep-Alive,当服务器收到附带有 Connection: Keep-Alive 的请求时,它也会在响应头中添加一个同样的字段来使用 Keep-Alive。这样一来,客户端和服务器之间的 HTTP 连接就会被保持,不会断开,当客户端发送另外一个请求时,就是用这条已经建立的连接。

在 HTTP 1.1 版本中,默认情况下所有连接都被保持,如果加入 Connection: close 才关闭。目前大部分浏览器都使用 HTTP 1.1 协议,也就是说默认都会发起 Keep-Alive 的连接请求了,所以是否能完成一个完整的 Keep-Alive 连接就看服务器设置情况。

注意:

  • HTTP Keep-Alive 简单说就是保持当前 TCP 连接,避免了重新建立连击
  • HTTP 长连接不可能一直保持,例如 Keep-Alive: timeout=5, max=100, 表示这个 TCP 通道可以保持 5 秒,max=100 表示这个长连接最多接受 100 次请求就断开
  • HTTP 是一个无状态协议,这意味着每个请求都是独立的,Keep-Alive 没能改变这个结果。另外,Keep-Alive 也不能保证客户端和服务器之间的连接一定是活跃的,在 HTTP 1.1 也是如此。唯一能保证的就是当连接被关闭时你能得到一个通知,所以不应该让程序依赖于 Keep-Alive 的保持连击特性
  • 使用长连接之后,客户端、服务端怎么知道本次传输结束呢?两部分:1)判断传输数据是否达到了 Content-Length 指示的大小;2)动态生成的文件没有 Content-Length,它是分块传输(chunked),这时候就要根据 chunked 编码来判断,chunked 编码的数据在最后又一个空 chunked 块,表明本次传输数据结束。

HTTP Pipelining(HTTP 管线化)

默认情况下 HTTP 协议中每个传输层连接只能承载一个 HTTP 请求和响应,浏览器会在收到上一个请求的响应之后,再发送下一个请求。在使用持久连接的情况下,某个连接上消息的传递类似于「请求1 -> 响应1 -> 请求2 -> 响应2 -> 请求3 —> 响应3」。

HTTP Pipelining(管线化)是将多个 HTTP 请求整批提交的技术,在传送过程中不需要等待服务端的回应。使用 HTTP Pipelining 技术之后,某个连接上的消息变成了类似这样:「请求1 -> 请求2 -> 请求3 -> 响应1 -> 响应2 -> 响应3」。

注意下面几点:

  • 管线化机制通过持久连接(persistent connection)完成,仅 HTTP/1.1 支持此技术(HTTP/1.0 不支持)
  • 只有 GET 和 HEAD 请求可以进行管线化,而 POST 则有所限制
  • 初次创建连接时不应启动管线机制,因为对方(服务器)不一定支持 HTTP/1.1 版本的协议
  • 管线化不会影响响应到来的顺序,如上面的例子所示,响应返回的顺序并未改变
  • HTTP/1.1 要求服务器端支持管线化,但并不要求服务器端也对响应进行管线化处理,只是要求对于管线化的请求不失败即可
  • 由于上面提到的服务器端问题,开启管线化很可能并不会带来大幅度的性能提升,而且很多服务器端和代理程序对管线化的支持并不好,因此现代浏览器如 Chrome 和 Firefox 默认并未开启管线化支持

会话跟踪

  • 什么是会话?

    客户端打开与服务器的连接发出请求到服务器响应客户端请求的全过程称之为会话。

  • 什么是会话跟踪?

    会话跟踪指的是对一个用户对服务器的连续的请求和接受响应的监视。

  • 为什么需要会话跟踪?

    浏览器与服务器之间的通信是通过 HTTP 协议进行通信的,而 HTTP 协议是「无状态」的协议,它不能保存客户的信息,即一次响应完成之后连接就断开了,下一次的请求需要重新连接,这样就需要判断是否是同一个用户,所以才会有会话跟踪技术来实现这种要求。

    1. 会话跟踪的常用方法

      • URL 重写。URL 是 Web 上特定页面的地址,URL 重写的技术就是在 URL 结尾添加一个附加数据以标识该会话,把会话 ID 通过 URL 的信息传递过去,以便在服务器端进行识别不同的用户
    2. 隐藏表单域

      • 将会话 ID 添加到 HTML 表单元素中提交到服务器,此标案元素并不在客户端显示
    3. Cookie

      • Cookie 是 Web 服务器发送给客户端的一小段信息,客户端请求时可以读取该信息发送到服务端,进而进行用户的识别。对于客户端的每次请求,服务器都会将 Cookie 发送到客户端,在客户端可以进行保存,以便下次使用。
      • 客户端可以采用两种方式来保存这个 Cookie 对象,一种方式是保存在客户端内存中,称为临时 Cookie,浏览器关闭后这个 Cookie 对象将消失。另外一种方式是保存在客户机的磁盘上,称之为永久 Cookie。以后客户端只要访问该网站,就会将这个 Cookie 再次发送到服务器上,前提是这个 Cookie 在有效期内,这样就实现了对客户的跟踪。
      • Cookie 是可以被禁止的。
    4. Session

      • 每一个用户都有一个不同的 Session,各个用户之间是不能共享的,是每个用户所独享的,在 Session 中可以存放信息
      • 在服务器端会创建一个 session 对象,产生一个 session ID 来标识这个 session 对象,然后将这个 session ID 放到 Cookie 中发送到客户端,下一次访问时,session ID 会发送到服务器,在服务器端进行标识不同的用户
      • session 的实现依赖于 Cookie,如果 Cookie 被禁用,那么 session 也将失效

跨站攻击

CSRF(Cross-site request forgery,跨站请求伪造)

CSRF(XSRF)顾名思义,是伪造请求,冒充用户在站内的正常操作。

例如,一论坛网站的发帖是通过 GET 请求访问,点击发帖之后 JS 把发帖内容拼接成目标 URL 并访问:

http://example.com/bbs/create_post.php?title=标题&content=内容

那么,我们紫瑶在论坛中发一贴,包含一链接:

http://example.com/bbs/create_post.php?title=我是脑残&content=哈哈

只要有用户点击了这个连接,那么他们的账户就会在不知情的情况下发不了这一帖子。既然发帖的请求可以伪造,那么删帖、转账、改密码、发邮件全部可以伪造。

如何防范 CSRF 攻击?

可以注意以下几点:

  • 关键操作只接受 POST 请求
  • 验证码。CSRF 攻击的过程中,往往是在用户不知情的情况下构造网络请求。所以如果使用验证码,那么每次操作都需要用户进行互动,从而简单有效地防御了 CSRF 攻击。但是如果你在一个网站做出任何举动都要输入验证码会严重影响用户体验,所以验证码一般只出现在特殊操作里面,或者在注册时使用
  • 检测 Referer
    • 常见的互联网页面与页面之间是存在联系的
    • 通过检查 Referer 的值,我们就可以判断这个请求是合法的还是非法的,但是问题出在服务器不是任何时候都能接受到 Referer 的值,所以 Referer Check 一般用于监控 CSRF 攻击的发生,而不用来抵御攻击
  • Token
    • 目前主流的做法是使用 Token 抵御 CSRF 攻击
    • CSRF 攻击要成功的条件在于攻击者能够预测所有的参数从而构造出合法的请求。所以根据不可预测性原则,我们可以对参数进行加密从而防止 CSRF 攻击
    • 另一个更通用的做法是保持原有参数不变,另外添加一个参数 Token,其值是随机的,这样攻击者因为不知道 Token 而无法构造出合法的请求进行攻击

Token 使用原则:

  • Token 要足够随机(只有这样才算不可预测)
  • Token 是一次性的,即每次请求成功后要更新 Token(这样可以增加攻击难度,增加预测难度)
  • Token 要注意保密性(敏感操作使用 POST,防止操作出现在 URL)中

XSS(Cross Site Scripting,跨站脚本攻击)

XSS 全称「跨站脚本」,是注入攻击的一种。其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到页面上,其他用户访问这个页面的时候就会运行这个脚本。

XSS 是实现 CSRF 的诸多途径中的一条,但绝对不是唯一的一条。一般习惯上把通过 XSS 来实现的 CSRF 称为 XSRF。

如何防御 XSS 攻击?

理论上,所有可输入的地方没有对输入数据进行处理的话,都会存在 XSS 漏洞,漏洞的危害却绝育攻击代码的威力,攻击代码也不限于 script。防御 XSS 攻击最简单的方法,就是过滤用户的输入。

如果不需要用户输入 HTML,可以直接对用户输入进行 HTML escape。

当我们需要用户输入 HTML 的时候,需要对用户输入的内容做更加小心细致的处理。仅仅粗暴地去掉 script 标签是没有用的,任何一个合法的 HTML 标签都可以添加 onclick 一类的事件属性来执行 JavaScript。更好的方法可能是,将用户输入使用 HTML 解析库进行解析,获取其中的数据。然后根据用户原有的标签属性,重新构建 HTML 元素树。构建的过程中,所有的标签、属性都只从白名单中拿去。

TCP 协议

TCP 协议的特性

  • TCP 提供一种面向连接的、可考的字节流服务
  • 在一个 TCP 连接中,仅有两方进行彼此的通信。广播和多播不能用于 TCP
  • TCP 使用校验和,确认和重传机制来保证可靠传输
  • TCP 使用累积确认
  • TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制

三次握手与四次握手

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送 3 个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connec() 时,将触发三次握手。

  • 第一次握手(SYN=1, seq=x)
    • 客户端发送一个 TCP 的 SYN 标志位置 1 的包,指明客户端打算连接的服务器端口,以及初始序列号 X,保存在包头的序列号(Sequence Number)字段里。
    • 发送完毕后,客户端进入 SYN_SEND 状态
  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)
    • 服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为 1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序列号(Acknowledgement Number) 设置为客户的 ISN 加 1,即 x+1
    • 发送完毕后,服务器端进入 SYN_RCVD 状态
  • 第三次握手(ACK=1, ACKnum=y+1)

    • 客户端再次发送确认包(ACK),SYN 标志位为 0,ACK 标志位为 1,并且把服务器发来 ACK 的序列号字段 +1,放在确定字段中发送给对方,并且在数据段放写 ISN 的 +1
    • 发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

TCP 的连接的拆除需要发送四个包,因此成为四次握手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动法切握手动作,在 socket 编程中,任何一方执行 close() 操作即可产生握手动作。

  • 第一次握手(FIN=1, seq=x)
    • 假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为 1 的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据
    • 发送完毕后,客户端进入 FIN_WAIT_1 状态
  • 第二次握手(ACK=1, ACKnum=x+1)
    • 服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接
    • 发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接受到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接
  • 第三次握手(FIN=1, seq=y)
    • 服务器端准备好关闭连接时,向客户端发送结束连接的请求,FIN 置为 1
    • 发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个 ACK
  • 第四次握手(ACK=1, ACKnum=y+1)

    • 客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT 状态,等待可能出现的要求重传的 ACK 包
    • 服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态
    • 客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime) 之后,没有收到服务器端的 ACK,认为服务器端已经正常关闭连接,于是自己也关闭连接,计入 CLOSED 状态

SYN 攻击

什么是 SYN 攻击(SYN Flood)?

在三次握手过程中,服务器发送 SYN-ACK 之后,收到客户端的 ACK 之前的 TCP 连接成为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态

SYN 攻击指的是,攻击客户端在段时间内伪造大量不存在的 IP 地址,向服务器不断地发送 SYN 包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,正常的 SYN 请求会被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。

SYN 攻击是一种典型的 DoS/DDoS 攻击。

如何检测 SYN 攻击?

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

如何防御 SYN 攻击?

SYN 攻击不能完全被阻止,除非将 TCP 协议重新设计。我们所做的是尽可能的减轻 SYN 攻击的危害,常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN Cookie 技术

IP 协议

广播与多播

广播和多播仅用于 UDP(TCP是面向连接的)。

广播

一共有四种广播地址:

  1. 受限的广播

    受限的广播地址为 255.255.255.255。该地址用于珠玑配置过程中 IP 数据报的目的地址,在任何情况下,router 不转发目的地址为 255.255.255.255 的数据报,这样的数据报近出现在本地网络中。

  2. 指向网络的广播

    • 指向网络的广播地址是主机号为全 1 的地址。A 类网络广播地址为 netid.255.255.255,其中 netid 为 A 类网络的网络号。
    • 一个 router 必须转发指向网络的广播,但它也必须有一个不进行转发的选择
  3. 指向子网的广播

    指向子网的广播地址为主机号为全 1 且有特定子网号的地址。作为子网直接广播地址的 IP 地址需要了解子网的掩码。例如,router 收到 128.1.2.255 的数据报,当 B 类网路 128.1 的子网掩码为 255.255.255.0 时,该地址就是指向子网的广播地址;但是如果子网掩码为 255.255.254.0,该地址就不是指向子网的广播地址。

  4. 指向所有子网的广播

    指向所有子网的广播也需要了解目的网络的子网掩码,以便与指向网络的广播地址区分开来。指向所有子网的广播地址的子网号和主机号为全 1. 例如,如果子网掩码为 255.255.255.0,那么 128.1.255.255 就是一个指向所有子网的广播地址。

    当前看法是这种广播是陈旧过时的,更好的方式是使用多播而不是对所有子网的广播。

广播示例:

PING 192.168.0.255 (192.168.0.255): 56 data bytes 64 bytes from 192.168.0.107: icmp_seq=0 ttl=64 time=0.199 ms 64 bytes from 192.168.0.106: icmp_seq=0 ttl=64 time=45.357 ms 64 bytes from 192.168.0.107: icmp_seq=1 ttl=64 time=0.203 ms 64 bytes from 192.168.0.106: icmp_seq=1 ttl=64 time=269.475 ms
64 bytes from 192.168.0.107: icmp_seq=2 ttl=64 time=0.102 ms 64 bytes from 192.168.0.106: icmp_seq=2 ttl=64 time=189.881 ms

可以看到的确受到了来自两个主机的答复,其中 192.168.0.107 是本机地址。

多播

多播又叫组播,使用 D 类地址,D 类地址分配的 28bit 均用作多播组号而不再表示其他。

多播组地址包括 1110 的最高 4 bit 和多播组号。它们通常可以表示为点分十进制数,范围从 224.0.0.0 到 239.255.255.255.

多播的出现减少了对应用不感兴趣主机的处理负荷。

多播的特点:

  • 允许一个或多个发送者(组播源)发送单一的数据包到多个接收者(一次的、同时的)的网络技术
  • 可以大大的节省网络带宽,因为无论有多少个目标地址,在整个网络的任何一条链路上只传送单一的数据包
  • 多播技术的核心就是针对如何节约网络资源的前提下保证服务质量

多播示例:

PING 224.0.0.1 (224.0.0.1): 56 data bytes
64 bytes from 192.168.0.107: icmp_seq=0 ttl=64 time=0.081 ms
64 bytes from 192.168.0.106: icmp_seq=0 ttl=64 time=123.081 ms
64 bytes from 192.168.0.107: icmp_seq=1 ttl=64 time=0.122 ms
64 bytes from 192.168.0.106: icmp_seq=1 ttl=64 time=67.312 ms
64 bytes from 192.168.0.107: icmp_seq=2 ttl=64 time=0.132 ms
64 bytes from 192.168.0.106: icmp_seq=2 ttl=64 time=447.073 ms
64 bytes from 192.168.0.107: icmp_seq=3 ttl=64 time=0.132 ms
64 bytes from 192.168.0.106: icmp_seq=3 ttl=64 time=188.800 ms

BGP

  • 边界网关协议(BGP)是运行于 TCP 上的一种自治系统的路由协议
  • BGP 是唯一一个用来处理像因特网大小的网络的协议,也是唯一能够妥善处理好不想管路由域间的多路连接的协议
  • BGP 是一种外部网关协议(Exterior Gateway Protocol, EGP),与 OSPF、RIP 等 内部网关协议(Interior Gateway Protocol, IGP) 不同,BGP 不在于发现和计算路由,而在于控制路由的传播和选择最佳路由
  • BGP 使用 TCP 作为其传输层协议(端口号 179),提高了协议的可靠性
  • BGP 支持 CIDR(Classless Inter-Domain Routing,无类别域间路由)
  • 路由更新时,BGP 只发送更新的路由,大大减少了 BGP 传播路由所占用的贷款,适用于在 Internet 上传播大量的路由信息
  • BGP 路由通过携带 AS 路径信息彻底解决路由环路问题
  • BGP 提供了丰富的路由策略,能够对路由实现灵活的过滤和选择
  • BGP 易于扩展,能够实行网络新的发展

Socket 编程

Socket 是对 TCP/IP 协议族的一种封装,是应用层与 TCP/IP 协议族通信的中间软件抽象层。从设计模式的角度来看,Socket 其实是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

Socket 还可以认为是一种网络间不同计算机上的进程通信的一种方法,利用三元组(ip 地址、协议、端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其他进程进行交互。

Socket 起源于 Unix,Unix 基本哲学之一就是「一切皆文件」,都可以用「open -> read/write -> close」 模式来进行操作。因此,Socket 也被处理为一种特殊的文件。

《算法》笔记

1.3 背包、队列和栈

研究方法:1)学习其 API 和用例;2)讨论数据类型的值和所有可能的表示方法;3)各种操作的实现。

1.3.1 API

每份 API 都含有一个无参数的构造函数、一个向集合中添加单个元素的方法、一个测试集合是否为空的方法和一个返回集合大小的方法。Stack 和 Queue 都含有一个能够删除集合中的特定元素的方法。

背包:

Bag() 创建一个空背包
void add(item) 添加一个空元素
bool isEmpty() 背包是否为空
int size() 背包中元素的数量

先进先出(FIFO)队列:

Queue() 创建空队列
void enqueue(item) 添加一个元素
Item dequeue() 删除最早添加的元素
bool isEmpty() 队列是否为空
int size() 队列中元素数量

下压(后进先出,LIFO)栈:

Stack() 创建一个空栈
void push(item) 添加一个元素
Item pop() 删除最近添加的元素
bool isEmpty() 栈是否为空
int size() 栈中的元素数量

1.3.1.1 泛型

因为这里使用的 Python 语言来实现书中的代码,所以不存在泛型的问题,Python 是鸭子类型。

1.3.1.2 自动装箱

自动讲一个原始数据类型转换为一个封装类型被称为自动装箱,自动将一个封装类型转换为原始数据类型被称为自动拆箱。

1.3.1.3 可迭代的几何类型

对应 Python 中的 Iterable 类型。

1.3.1.4 背包

背包是一种不支持从中删除元素的几何数据类型——它的目的就是帮助用例收集元素并迭代遍历所有收集到的元素。迭代的顺序不确定且与用例无关。

1.3.1.5 先进先出队列

先进先出队列(简称队列)是一种基于先进先出(FIFO)策略的集合类型。

1.3.1.6 下压栈

下压栈(简称栈)是一种基于后进先出(LIFO)策略的几何类型。典型例子:1)邮件系统;2)浏览器。

1.3.1.7 算数表达式求值

递归定义:算数表达式可能是一个数、或者是一个由左括号、一个算数表达式、一个运算符、另一个算数表达式和一个右括号组成的表达式。(简单起见,这里定义的是未省略括号的算数表达式)。

E.W.Dijkstra 在 20 世纪 60 年代发明了一个非常简单的算法,用两个栈(一个用于保存运算符,一个用于保存操作数)完成了这个任务。

我们根据以下 4 种情况从左到右逐个将这些实体送入栈处理:

  1. 将操作数压入操作数栈;
  2. 将运算符压入运算符栈;
  3. 忽略左括号;
  4. 在遇到右括号时,弹出一个运算符,弹出所需数量的操作数,并将运算符和操作数的运算结果压入操作数栈。

一个用栈实现的简单解释器例子:

def evaluate(expression):
    ops = Stack()
    vals = Stack()
    
    for s in expression:
        if s == '(':
            continue
        elif s == '+':
            ops.push(s)
        elif s == '-':
            ops.push(s)
        elif s == '*':
            ops.push(s)
        elif s == '/':
            ops.push(s)
        elif s == ')':
            op = ops.pop()
            v = vals.pop()
            if op == '+':
                v = vals.pop() + v
            elif op == '-':
                v = vals.pop() - v
            elif op == '*':
                v = vals.pop() * v
            elif op == '/':
                v = vals.pop() / v
            vals.push(v)
        else:
            vals.push(s)
    return vals[0]

1.3.2 集合数据类型的实现

1.3.2.1 定容栈

定容栈是一种表示容量固定的字符串栈的抽象数据类型,它的 API 和 Stack 的 API 有所不同:它只能处理 String 值,它要求用例制定一个容量且不支持迭代。实现一份 API 的第一步就是选择数据的表示方式,对于 FixedCapacityStackOfStrings,我们可以选用数组。

API:

FixedCapacityStackOfStrings(size) 创建一个容量为 size 的空栈
void push(item:str) 添加一个字符串
str pop() 删除最近添加的字符串
bool isEmpty() 栈是否为空
int size() 栈中字符串数量

数据类型的实现:

class FixedCapacityStackOfStrings:
    def __init__(self, size):
        _a = []     # stack entries
        _size = size
        N = 0      # stack index
        

    def isEmpty(self):
        return N == 0

    def size(self):
        return N
    
    def push(self, item):
        N += 1
        _a[N] = item

    def pop(self):
        N -= 1
        return _a[N]
  • 数组中的元素顺序和它们被插入的顺序相同
  • 当 N 为 0 时栈为空
  • 栈的顶部位于 _a[N-1]

1.3.2.2 泛型

如果上面代码是用 Java 写的话,确实只能处理 String 对象,但是 Python 是动态类型的语言,不存在泛型的问题。

1.3.2.3 调整数组的大小

选择用数组表示栈内容意味着用例必须预先估计栈的最大容量(在 Python 中数组是动态变化的,所以不需要,这里为了模拟)。在 Java 中,数组一旦创建,其大小是无法改变的,因此栈使用的空间只能是这个最大容量的一部分。选择大容量的用例在栈为空或几乎为空时会浪费大量的内存。push() 方法需要在代码中检测栈是否已满,我们的 API 中也应该含有一个 isFull() 的方法来允许用例检测栈是否已满。我们在此省略了它实现的代码,因为我们希望用例从处理栈已满的问题中解脱出来,如我们原始的 Stack API 所示。因此,我们修改了数组的实现,动态调整数组的大小,使得它既足以保存所有元素,又不至于浪费过多的空间。

首先,实现一个方法将栈移动到另一个大小不同的数组中:

def resize(self, max):
   temp = []
   _size = max
   for i in range(N):
       temp[i] = _a[i]
   a = temp

现在,在 push() 中,检查数组是否太小。具体来说,我们会通过检查栈代销 N 和数组大小 _size 是否相等来检查数组是否能容纳新的元素。如果没有多余的空间,我们会将数组的长度加倍,然后既可以和之前一样用 a[N++] = item 插入新元素了:

def push(self, item):
   if N == _size:
       self.resize(2*_size)
   N += 1
   _a[N] = item

类似,在 pop() 中,首先删除栈顶的元素,然后如果数组太大我们就将它的长度减半。只要稍加思考,技能明白正确的检测条件是栈大小是否小鱼数组的四分之一。在数组长度被减半之后,它的状态为半满,在下次需要改变数组大小之前人能够进行多次 push()pop() 操作。

def pop(self):
   N -= 1
   item = _a[N]
   _a[N] = None
   if N > 0 and N == _size/4:
       resize(_size/2)
   return item

在这个实现中,栈永远都不会溢出,使用率也永远不会低于四分之一。

1.3.2.4 对象游离

Java 的垃圾收集策略是回收所有无法被访问的对象的内存。在我们队 pop() 的实现中,被弹出的元素引用仍然存在于数组中。这个元素实际上已经是一个孤儿了——它永远不会被再访问了,但 Java 的垃圾收集器没法知道这一点,除非该引用被覆盖。即使用例已经不再需要这个元素了,数组中的引用仍然可以让它继续存在,这种情况(保存一个不需要的对象的引用)成为游离

1.3.2.5 迭代

集合类数据类型的基本操作之一就是,能够使用 Python 的 for-in 语句通过迭代遍历并处理集合中的每个元素。这种方式的代码既清晰又简介,且不依赖与集合数据类型的具体实现。

好处:1)我们无需改变任何用例代码就可以随意切换不同的表示方法;2)更重要的是,从用例的角度来说,无需知晓类的实现细节用例也能使用迭代。

在 Python 中,需要在类中实现 __iter__()__next__()

def __iter__(self):
     return self

def __next__(self):
     index = 0
     if index > _size:
         raise StopIteration
     else:
         index += 1
         return self._a[index-1]

例如,我们在实现 Queue 的 API 时,可以使用两个实例变量作为索引,一个变量 head 指向队列的开头,一个变量 tail 指向队列的结尾。在删除一个元素时,使用 head 访问它并将 head 加 1;在插入一个元素时,使用 tail 保存它并将 tail 加 1.如果某个索引在增加之后越过了数组的边界则将它重置为 0.

下压(LIFO)栈(能够动态调整数组大小的实现):

class ResizingArrayStack(object):
    a = []
    N = 0

    def isEmpty(self):
        return N == 0

    def size(self):
        return N

    def resize(self, max):
        temp = []
        for i in range(N):
            temp[i] = a[i]
        a = temp

    def push(self, item):
        if N == len(self.a):
            self.resize(2*len(self.a))
        N += 1
        a[N] = item

    def pop(self):
        N -= 1
        item = a[N]
        a[N] = None
        if N > 0 and N == len(self.a):
            self.resize(len(self.a)/2)

    def __iter__(self):
        return self

    def __next__(self):
        index = N - 1
        if index == 0:
            raise StopIteration
        else:
            index -= 1
            return a[index]

这份支持迭代的 Stack API 的实现是所有集合抽象数据类型实现的模板。它将所有元素保存在数组中,并动态调整数组大小以保持数组大小和栈大小之比小于一个常数。

1.3.3 链表

定义:链表是一种递归的数据结构,它为空(None),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另一条链表的引用。

在这个定义中,结点是一个可能含有任意类型数据的抽象实体,它所包含的指向结点的应用显示了它在构造链表之中的作用。

1.3.3.1 结点记录

class Node(object):
        def __init__(self, item):
             self.item = item
             self.next = None
             
        def get_item(self):
                return self.item
                
        def get_next(self):
                return self.next
                
        def set_item(self, item):
                self.item = item
                
        def set_next(self, next):
                self.next = next

1.3.3.8 栈的实现

下压堆栈(链表实现):

class Node(object):
    def __init__(self, item):
        self.item = item
        self.next = None

class Stack(object):
    def __init__(self):
        self.first = None
        self.N = 0
        
    def isEmpty(self):
        return self.N == 0
    
    def size(self):
        return self.N
    
    def push(self, item):
        oldfirst = self.first
        self.first = Node(item)
        self.first.next = oldfirst
        self.N += 1
        
    def pop(self):
        item = self.first.item
        self.first = self.first.next
        self.N -= 1
        return item

1.3.3.9 队列的实现

基于链表数据结构实现 Queue API 也很简单。它将队列表示为一条从最早插入的元素到最近插入的元素的链表,实例变量 first 指向队列的开头,实例变量 last 指向 队列的结尾。这样,要讲一个元素入列(enqueue()),我们就将它添加到表尾(但是在链表为空时需要将 first 和 last 都指向新节点);要将一个元素出列(dequeue()),我们就删除表头的结点(代码和 Stack 的 pop() 相同,只是当链表为空时需要更新 last 的值)。size()isEmpty() 方法的实现和 Stack 相同。

和刚才一样,我们用链表达到了最优设计目标:它可以处理任意类型数据,所需的空间总是和集合大小成正比,操作所需时间总是和集合大小无关

先进先出队列的实现:

class Queue(object):
    def __init__(self):
        self.first = None  # 指向最早添加的结点的链接
        self.last = None  # 指向最近添加的结点的链接
        self.N = 0
        
    def isEmpty(self):
        return self.N == 0
    
    def size(self):
        return self.N
    
    def enqueue(self, item):
        oldlast = self.last
        self.last = Node(item)
        self.last.next = None
        if self.isEmpty():
            self.first = self.last
        else:
            oldlast.next = self.last
        self.N += 1
        
    def dequeue(self):
        item = self.first.item
        self.first = self.first.next
        if self.isEmpty():
            self.last = None
        self.N -= 1
        return item

在结构化存储数据集时,链表是数组的一种重要的替代方式。事实上,编程语言历史上的一块里程碑就是 McCathy 在 20 世纪 50 年代发明的 LISP 语言,而链表则是这种语言组织程序和数据的主要结构。

1.3.3.10 背包的实现

用链表数据结构实现我们的 Bag API 只需要将 Stack 中的 push() 方法改名为 add(),并去掉 pop() 的实现即可。

对于 Stack,链表的访问顺序是后进先出;对于 Queue,链表的访问顺序是先进先出;对于 Bag,它正好也是后进先出的顺序,但顺序并不重要。

背包的实现:

class Bag(object):
    def __init__(self):
        self.first = None
        self.N = 0
        
    def isEmpty(self):
        return self.first is None
    
    def add(self, item):
        oldfirst = self.first
        self.first = Node(item)
        self.first.next = oldfirst
        self.N += 1
        
    def __len__(self):
        return self.N
        
    def __iter__(self):
        return _BagIterator(self.first)

class _BagIterator(object):
    def __init__(self, listhead):
        self.current = listhead

    def __iter__(self):
        return self

    def __next__(self):
        if self.current is None:
            raise StopIteration
        item = self.current.item
        self.current = self.current.next
        return item

这份代码中实现了迭代器(可以使用 for-in 来遍历),Stack 和 Queue 可以使用同样的方法来实现。

1.3.4 综述

基础数据结构:

数据结构 优点 缺点
数组 通过索引可以直接访问任意元素 在初始化时就需要知道元素的数量
链表 使用的空间大小和元素数量成正比 需要通过引用访问任意元素

我们在本节中研究背包、队列和栈时描述数据结构和算法的方式是全书的原型。在研究一个新的应用领域时,我们将会按照以下步骤识别目标并使用数据抽象解决问题:

  1. 定义 API
  2. 根据特定的应用场景开发用例代码
  3. 描述一种数据结构(一组值得表示),并在 API 所对应的抽象数据类型的实现中根据它定义类的实例变量
  4. 描述算法(实现一组操作的方式),并根据它实现类中的实例方法
  5. 分析算法的性能特点

本书中所给出的数据结构举例:

数据结构 抽象数据类型 数据表示
父链接数 UnionFind 整形数组
二分查找树 BST 含有两个链接的结点
字符串 String 数组、偏移量和长度
二插堆 PQ 对象数组
散列表(拉链法) SeparateChainingHashST 链表数组
散列表(线性探测法) LinerProbingHashST 两个对象数组
图的邻接链表 Graph Bag 对象数组
单词查找树 TrieST 含有链接数组的结点
三向单词查找树 TST 含有三个链接的结点

1.4 算法分析

1.4.1 科学方法

科学家用来理解自然世界的方法对于研究计算机程序的运行时间同样有效:

  • 细致地观察真实世界的特点,通常还要有精确的测量
  • 根据观察结果提出假设模型
  • 根据模型预测未来的事件
  • 继续观察并核实预测的准确性
  • 如此反复直到确认预测和观察一致

1.4.3 数学模型

一个程序运行的总时间主要和两点有关:

  • 执行每条语句的耗时
  • 执行每条语句的频率

1.4.4 增长数量级的分类

对增长数量级常见假设的总结:

描述 增长的数量级 说明 举例
常数级别 1 普通语句 将两个数相加
对数级别 \(logN\) 二分策略 二分查找
线性级别 \(N\) 循环 找出最大元素
线性对数级别 \(NlogN\) 分治 归并排序
平方级别 \(N^2\) 双层循环 检查所有元素对
立方级别 \(N^3\) 三层循环 检查所有三元组
指数级别 \(2^N\) 穷举查找 检查所有子集

1.4.5 设计更快的算法

学习程序的增长数量级的一个重要动力就是为了帮助我们为同一个问题设计更快地算法。

1.4.7 注意事项

1.4.7.1 大常数

在首项近似中,我们一般会忽略低级项中的常数系数,但这可能是错的。例如,当我们取函数 \(2N^2+cN\) 的近似为 ~\(2N^2\) 时,我们的假设是 c 很小。如果事实不是这样(比如 c 可能是 \(10^6\)),该近似就是错误的。因此,我们要对可能的大常数保持敏感。

1.4.7.2 非决定性的内循环

内循环是决定性因素的假设并不是总正确的。错误的成本模型可能无法得到真正的内循环,问题规模 \(N\) 也许没有大到对指令的执行频率的数学描述中的首项大大超过其他低级项并可以忽略它们的程度。有些程序在内循环之外也有大量指令需要考虑。

1.4.7.3 指令时间

每条指令执行所需的时间总是相同的假设并不总是正确的。例如,大多数现在计算机系统都会使用缓存技术来组织内存,在这种情况下访问大数组中的若干个并不相邻元素所需的时间可能很长。

1.4.8 处理对于输入的依赖

1.4.8.1 输入模型

一种方法是更加小心地对我们所要解决的问题所处理的输入建模。使用这种方法的困难主要有两点:

  1. 输入模型可能是不切实际的
  2. 对输入的分析可能极端困难

1.4.8.3 随机化算法

为性能提供保证的一种重要的方法是引入随机性。例如,快速排序算法在最坏情况系的性能是平方级别的,但通过随机打乱输入,根据概率我们能够保证它的性能是线性对数的。每次运行该算法,它所需的时间均不相同,但它的运行时间超过超过线性对数级别的可能性小到可以忽略。与此类似,用于符号表的散列算法在最坏情况下的性能是线性级别的,但根据概率我们可以保证它的运行时间是常数级别的。

1.4.8.5 均摊分析

相应地,提供性能保证的另一种方法是通过记录所有操作的总成本并除以操作总数来将成本均摊。在这里,我们可以允许执行一些昂贵的操作,但保持所有操作的平均数成本较低。

1.5 案例研究:union-find 算法

为了说明我们设计和分析算法的基本方法,我们现在来学习一个具体的例子。我们的目的是强调以下几点:

  • 优秀的算法因为能够解决实际问题而变得更为重要;
  • 高效算法的代码也可以很简单;
  • 理解某个实现的性能特地拿是一项有趣而令人满足的挑战;
  • 在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具;
  • 迭代式改进能够让算法的效率越来越高。

2.1 初级排序算法

2.1.1.3 额外的内存使用

排序算法的额外内存开销和运行时间是同等重要的。排序算法可以分为两类:

  • 原地排序算法
  • 需要额外内存空间来存储的其他排序算法

2.1.2 选择排序

选择排序两个鲜明的特点:

  1. 运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶地发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长。
  2. 数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了 N 次交换——交换次数和数组的大小是线性关系。

选择排序实现:

def selection_sort(a):
    N = len(a)
    for i in range(N):
        mi = i
        for j in range(i+1, N):
            if a[j] < a[mi]:
                mi = j
        a[i], a[mi] = a[mi], a[i]
    return a

2.1.3 插入排序

与选择排序一样,当前索引左边的所有元素都是有序的,但它们最终的最终位置还不确定,为了给更小元素腾出空间,它们可能会被移动。但是当前索引到达数组的右端时,数组排序就完成了。

与选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。

插入排序对于实际应用中常见的某些类型的非随机数组很有效。当插入排序对一个有序数组进行排序,运行时间是线性的。

def insertion_sort(a):
    for i in range(1, len(a)):
        for j in range(i, 0, -1):
            if a[j] < a[j-1]:
                a[j], a[j-1] = a[j-1], a[j]
    return a

对于 1 到 N-1 之间的每一个 i,将 a[i] 与 a[0] 到 a[i-1] 中比它小的所有元素依次有序地交换。在索引 i 由左向右变化的过程中,它左侧的元素总是有序的,所以当 i 到达数组的右端时排序就完成了。

我们要考虑的更一般的情况是部分有序的数组。倒置指的是数组中的两个顺序颠倒的元素。比如 E X A M P L E 中有 11 对倒置:E-A X-A X-M X-P X-L X-E M-L M-E ... 如果数组中倒置的数量小于数组大小的某个倍数,那么我们说这个数组是部分有序的。

下面是几种典型的部分有序的数组:

  • 数组中每个元素都距离它的最终位置都不远
  • 一个有序的大数组接一个小数组
  • 数组中只有几个元素的位置不正确

插入排序对这样的数组很有效,而选择排序则不然。事实上,当倒置数量很少时,插入排序很可能比本章中的其他任何算法都要快。

要大幅提高插入排序的速度并不难,只需要在内循环中将较大的元素都向右移动而不总是交换两个元素。

2.1.6 希尔排序

为了展示初级排序算法性质的价值,我们将学习一种基于插入排序的快速排序算法。对于大规模乱序数组插入排序很慢,因为它智慧交换相邻的元素,因此元素只能一点一点地从数组的一段移动到另一端。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终插入排序将局部有序的数组排序。

希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。这样的数组被称为 h 有序数组。换句话说,h 有序数组就是 h 个独立的有序数组编织在一起组成的一个数组,如下图所示。

实现希尔排序的一种方法是对于每个 h,用插入排序将 h 个子数组独立地排序。但因为子数组是相互独立的,一个更简单的方法是在 h-子数组中将每个元素交换到比它大的元素之前去(将比它大的元素向右移动一格)。只需要在插入排序的代码中将移动元素的距离由 1 改为 h 即可。这样,希尔排序的实现就转化为了一个类似于插入排序但是用不同增量的过程。

希尔排序更高效的原因是它权衡了子数组的规模性和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。

def shell_sort(a):
    h = 1
    while h < len(a) / 3:
        h = 3 * h + 1
    while h >= 1:
        for i in range(int(h), len(a)):
            for j in range(i, int(h)-1, -1):
                if a[j] < a[j - int(h)]:
                    a[j], a[j - int(h)] = a[j - int(h)], a[j]
        h = h / 3
    return a

2.2 归并排序

归并:即将两个有序数组归并成一个更大的有序数组。

2.2.1 原地归并的抽象方法

实现归并的一种直截了当的方法是将两个不同的有序数组归并到第三个数组中。

但是,当用归并将一个很大的数组排序时,我们需要进行很多次归并,因此在每次归并时都创建一个新数组来存储排序结果会带来问题。我们更希望有一种能够在原地归并的方法,这样就可以先将前半部分排序,再将后半部分排序,然后在数组中移动元素而不需要使用额外的空间。

原地归并的抽象方法:


March 2017

2017/03/04

How do I as a student start contributing to open source?

Nimit Shah:

  1. Start contributing to an open source project that you use in your day to day life
  2. Select a project from Google Summer of Code or GNOME Outreach Program for Women

How can a person get selected in Google Summer of Code?

Ashwyn Sharma:

Do not apply for GSoC if you are not an Open Source Enthusiast!

Crucial steps involved in the process:

  1. Start with the list of accepted organisations in the past years and pick one of them.
  2. Contact the organisation telling them about yourself (in most cases - that you are a software developer who is new to their products/applications/platforms etc.) and how you wish to contribute to their codebase.
  3. Get yourself familiar with the codebase. A little perseverance at this stage may prove decisive of your future with both GSoC and Open Source in general. Pick up a simple bug/feature-request from the bug tracker of the given application and try to solve it. Not only this will increase your chances of acceptance dramatically, it will also help you get familiar with the developers and of course, the code itself. An important thing to remember is to make your presence felt amongst the developer community. This will help you at later stages of the application process.
  4. Provided you manage to solve the bug or develop that feature, the next step will be to commit your code to the Trunk/Central Repository.
  5. Next is the application period. As soon as Google announces the list of the accepted organisations, start looking for the prospective project ideas. Think over how you can go about executing these ideas. Talk to the prospective mentors about what you think of the idea and may be fetch more details about the ins and outs of project. Though Google allows you to submit your own original ideas as well, I have always believed that its better to choose from the list of the ideas provided by the organization. However, if the idea is truly meaningful and has a feasible implementation plan, there have been instances where original ideas have been accepted as well. If you haven't already, keep working on that bug and solve it, because if you haven't really proved your coding skills to the organisation yet, then your chances of getting accepted gets even simmer, no matter how strong your application maybe.
  6. Start writing the proposal as early as possible. Make sure that before submitting the proposal, you get your proposal reviewed by your mentors as much as possible. The main ingredients of a good proposal are that it should address two kinds of audiences - one which is completely familiar with the technical details of the project (that would be your mentors and organisation devs) and the second one is a neutral not-so-technical audience which is able to understand the deliverables of the project (in most cases that would be the user base). Start coding up your idea.

平台后端开发(Python)面试记录

  1. 手写 Python 生成器(带 yield 的函数)
  2. 手写链表翻转
  3. Tornado 框架中的异步实现原理
  4. select, poll, epoll, epoll 改进了 select/poll 的什么地方
  5. 线程与进程的区别
  6. 进程间通信方式
  7. 数据库原理(索引,B 树)
  8. TCP 四次握手过程
  9. Nginx 的转发原理
  10. Linux 文件系统

如何为你的 GitHub 开源项目写一份优秀的 README.md 文档?

  • Project Title: one paragraph of description goes here.
  • Getting Started: These istructions will get you a copy of the project up and running on your local machine for development and testing purposes.
  • Prerequisties: what things you need to install the software and how to install them.
  • Installing: A step by step series of examples that you have to get a development env running.
  • Running the tests: Explain how to run the automated tests for this system.
  • Break down into end to end tests: explain what these tests test and why
  • And coding style tests: expalin what these tests test and why
  • Deployment: add additional notes about how to deploy this on a live system
  • Built with
  • Contributing: please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
  • Authors
  • License
  • Acknowledgemets

中英文 readme 各写一份。

Python Cookbook

调用父类的方法

super() 的常见用法:

  1. __init__() 方法中确保父类被正确初始化了
  2. 另外一个常见用法出现在覆盖 Python 特殊方法的代码中

Ex:

class Proxy:
    def __init__(self, obj):
        self._obj = obj

    # Delegate attribute lookup to internal obj
    def __getattr__(self, name):
        return getattr(self._obj, name)

    # Delegate attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value) # Call original __setattr__
        else:
            setattr(self._obj, name, value)

在上面代码中,__setattr__() 的实现包含一个名字检查。如果某个属性名以 _ 开头,就通过 super() 调用原始的 __setattr__(),否则的话就委派给类内部的代理对象 self._obj 去处理。这看上去有点意思,因为就算没有显式指明某个类的父类,super() 仍然可以有效地工作。

子类中扩展 Property

在自雷中扩展一个 property 可能会引起很多不易察觉的问题,因为一个 property 其实是 gettersetterdeleter 方法的几何,而不是单个的方法。因此,当你扩展一个 property 的时候,你需要先确定你是否要重新定义所有的方法还是说只修改其中的一个。

创建新的类或实例属性

描述器:一个实现了三个核心的属性访问操作(get, set, delete)的类,分别为 __get__(), __set__()__delete__() 这三个特殊的方法。这些方法接受一个实例作为输入,之后相应的操作实例实例底层的字典。

为了使用一个描述器,需要将这个描述器的实例作为类属性放到一个类的定义中。

一个基于描述器的高级代码:

# Descriptor for a type-checked attribute
class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected ' + str(self.expected_type))
        instance.__dict__[self.name] = value
    def __delete__(self, instance):
        del instance.__dict__[self.name]

# Class decorator that applies it to selected attributes
def typeassert(**kwargs):
    def decorate(cls):
        for name, expected_type in kwargs.items():
            # Attach a Typed descriptor to the class
            setattr(cls, name, Typed(name, expected_type))
        return cls
    return decorate

# Example use
@typeassert(name=str, shares=int, price=float)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Python 风格指南笔记

函数与方法装饰器

  • 优点:优雅的在函数上指定一些转换,该转换可能减少一些重复代码,保持已有函数不变(enforce invariants)
  • 缺点:装饰器可以在函数的参数或返回值上执行任何操作,这可能导致让人惊异的隐藏行为。而且,装饰器在导入时执行。从装饰器代码的失败中恢复更加不可能。
  • 结论:如果好处很显然,就明智而谨慎的使用装饰器。装饰器应该遵守和函数一样的导入和命名规则。装饰器的 Python 文档应该清晰的说明函数是一个装饰器。请为装饰器编写单元测试。避免装饰器自身对外界的依赖(即不要依赖于文件、socket、数据库连接等)

线程

优先使用 Queue 模块的 Queue 数据类型作为线程间的数据通信方式。另外,使用 threading 模块及其锁原语(locking primitives)。了解条件变量的合适使用方式,这样你就可以使用 threading.Condition 来取代低级级别的锁了。

威力过大的特性

  • Tip:避免使用这些特性
  • 优点:强大的语言特性,能让你的代码更紧凑
  • 缺点:使用这些很 cool 的特性十分诱人,但不是绝对必要。使用奇技淫巧的代码将更加难以阅读和调试。开始可能还好,但当你回顾代码,它们可能比那些稍长一点但是更直接的代码更加难以理解。
  • 结论:在你的代码中避免使用这些特性。

注释

Python 有一种独一无二的注释方式:使用文档字符串。文档字符串是包、模块、类或函数的第一个语句。这些字符串可以通过对象的 __doc__ 成员被自动提取,并且被 pydoc 所用。

组织方式:

  • 第一行以句号、问号或惊叹号结尾的概述(或者改文档字符串只有单纯的一行)
  • 接着是一个空行
  • 接着是文档字符串的剩余部分,它应当与文档字符串的第一行的第一个引号对齐

模块

每个文件应该包含一个许可样板。根据项目使用的许可(例如:Apache 2.0、BSD、LGPL、GPL)选择合适的样板。

函数和方法

一个函数必须要有文档字符串,除非它满足以下条件:

  1. 外部不可见
  2. 非常短小
  3. 简单明了

文档字符串应该包含函数做什么,以及输入和输出的详细描述。通常,不应该描述「怎么做」,除非是一些复杂的算法。文档字符串应该提供足够的信息,当别人编写代码调用该函数时,他不需要看一行代码,只要看文档字符串就可以了。对于复杂的代码,在代码旁边加注释胡比使用文档字符串更有意义

关于函数的几个方面应该在特定的小节中进行描述记录,这几个方面入下文所示,每节应该以一个标题行开始。标题行以冒号结尾,除标题行外,节的其他内容应被缩进 2 个空格。

  • Args:列出每个每个参数的名字,并在名字后面使用一个冒号和空格,分隔对该参数的描述。如果描述太长超过了单行 80 个字符,使用 2 或者 4 个空格的悬挂缩进。描述应该包括所需的类型和含义。如果一个函数接受可变长参数列表或者任意关键字参数,应该详细列出这两者。
  • Returns(或者 Yields,用于生成器):描述返回值的类型和语义,如果函数返回 None,这一部分可以省略。
  • Raises:列出与接口有关的所有异常。

例子:

def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
    """Fetches rows from a Bigtable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by big_table.  Silly things may happen if
    other_silly_variable is not None.

    Args:
        big_table: An open Bigtable Table instance.
        keys: A sequence of strings representing the key of each table row
            to fetch.
        other_silly_variable: Another optional variable, that has a much
            longer name than the other args, and which does nothing.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

        {'Serak': ('Rigel VII', 'Preparer'),
         'Zim': ('Irk', 'Invader'),
         'Lrrr': ('Omicron Persei 8', 'Emperor')}

        If a key from the keys argument is missing from the dictionary,
        then that row was not found in the table.

    Raises:
        IOError: An error occurred accessing the bigtable.Table object.
    """
    pass

类应该在其定义下有一个用于描述该类的文档字符串。如果你的类有公有属性,那么文档中应该有一个属性段,并且应该遵守和函数参数相同的格式。

class SampleClass(object):
    """Summary of class here.

    Longer class information....
    Longer class information....

    Attributes:
        likes_spam: A boolean indicating if we like SPAM or not.
        eggs: An integer count of the eggs we have laid.
    """

    def __init__(self, likes_spam=False):
        """Inits SampleClass with blah."""
        self.likes_spam = likes_spam
        self.eggs = 0

    def public_method(self):
        """Performs operation blah."""

块注释和行注释

最需要些注释的是代码中那些技巧性的部分。对于复杂的操作,应该在其操作开始前写上若干行注释,对于不是一幕了然的代码,应该在其行尾添加注释。

为了提高可读性,注释至少应该离开代码 2 个空格。

绝对不要描述代码,假设阅读代码的人比你更懂 Python,他只是不知道你的代码要做什么。

如果一个类不继承自其它类,就显式的从 object 继承,嵌套类也一样。

TODO 注释

TODO 注释应该在所有开头处包含「TODO」字符串,紧跟着的是用括号括起来的你的名字,email 地址或者其他标识符。接着必须有一行注释,解释要做什么。

Example:

# TODO(kl@gmail.com): Use a "*" here for string repetition.
# TODO(Zeke) Change this to use relations.

如果你的 TODO 是「将来做某事」的形式,那么请确保你包含了一个指定的日期或者一个特定的时间。

导入格式

每个导入应该独占一行。导入总应该放在文件顶部,位于模块注释和文档字符串之后,模块全局变量和常量之前,导入应该按照从最通用到最不通用的顺序分组:

  1. 标准库导入
  2. 第三方库导入
  3. 应用程序制定导入

每种分组中,应该根据每个模块的完整包路径按字典顺序排序,忽略大小写。

访问控制

字啊 Python 中,对于琐碎又不太重要的访问函数,你应该直接使用公有变量来取代它们,这样可以避免额外的函数调用开销。当添加更多的功能时,你可以用属性(property)来保持语法的一致性。

另一方面,如果访问更复杂,或者变量的访问开销很显著,那么你应该使用像 get_foo()set_foo() 这样的函数调用。如果之前的代码行为允许通过属性(property)访问,那么久不要井新的访问函数与属性绑定。这样,任何试图通过老方法访问变量的代码就没法运行,使用者也就会意识到复杂性发生了变化。

命名

Python 之父 Guido 推荐的规范:

2016/08/08 阅读笔记

Quora: How can I study for 10+ hours a day without getting depressed and how can I make myself get used it?

Such a stupid question.

  1. Don't study more than 7.5 hours a day. You can't learn when you are exhausted.
  2. Take a day off per week. Do something you like on that day.
  3. Make a plan.What knowledge is most critical in each of the subjects? First, concentrate only on that.
  4. Study from low to high resolution.
  5. Nap. A lot.
  6. Study one topic for 2.5 hours. Then switch to another. Continue.
  7. Read. Then put down the book. Then summarize what you have read. Don't look at what you were reading when you summarize.

  1. Avoid time specific plans unless you're a monk or living in an extremely isolated environment.
  2. Create a simple mechanism for notes and plans. You don't need any complex thing. The aim here is to work efficiently so these tools should be tools, not the whole story.
  3. Create tasks as atomic as possible. Task should not be long. Best practice is that the task can be done after 1-2 hours of efficient work. If the task is big, you divide it into subtasks. In other words, avoid context switching as much as possible.
  4. Get rid of all external distraction.
  5. Use Pomodoro Technique.
  6. If you have a long-run work and you can't complete it at the end of the day, take a note for tomorrow's yourself in order to remember the basic of teh task. And tomorrow, you start the day by glancing over the connected tasks.

If studying makes you depressed, it's because your mind considers it a chore that you're forced to do. Doing any such chore for long periods of time is awful and depressing. On the other hand, doing somehting you love is fun, even when it's physically or mentally exhausting. So what you need to do is make the experience more valuable and enjoyable.

  1. Knowing WHY you're learning something.
  2. Really understanding what you're learning.

  1. Learn and practise proven efficient and effective study tools like SQ5R and Cornell Notes.
    • SQ5R reading/satudying strategy: [Survey, Question, Read, Record, Recite, Review, Reflect],

The Python Tutorial Reading Notes

Data Structures

More on Lists

  • list.append(x): Equivalent to a[len(a):] = [x]
  • list.extend(L): Extend the list by appending all the items in the given list. Equivalent to a[len(a):] = L
  • list.insert(i, x): The first argument is the index of the element before which to insert. a.insert(len(a), x) == a.append(x).
  • list.remove(x): Remove the first item form the list whose value is x.
  • list.pop([i]): Remove the item at the given position in the list, and return it. If no index is specified, a.pop() removes and returns the last item in the list.(parameters with square brackets are optional)
  • list.clear(): Remove all item from the list. Equivalent to del a[:]
  • list.index(x): Return the index in the list of the first item whose value is x.
  • list.count(x): Return the number of times x appears in the list.
  • list.sort(key=None, reverse=False): Sort the items of the list in place.
  • list.reverse(): Reverse the elements of the list in place.
  • list.copy(): Return a shallow copy of the list. Equivalent to a[:].

Using Lists as Stacks

Use append and pop.

Using Lists as Queues

Lists are not efficient for this purpose. While appends and pops from the end of list are fast, doing inserts or pops from beginning of a list is slow.

Better to use collections.deque.

List Comprehensions

  • x = [item for item in series]
  • x = [do_something(item) for item in series if expression]

Nested List Comprehensions

The initial expression in a list comprehension can be any arbitrary expression, including another list comprehension.

Example: [[row[i] for row in matrix] for i in range(4)].

The del statement

Remove an item from a list given its index. (Do not return a value) It can also remove slices from a list.

del can also be used to delete entire variables: del a.

Tuples and Sequences

Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking or indexing. List are mutable, and their element are usually homogeneous and are accessed by iterating over the list.

  • Empty tuples are constructed by and empty pair of parentheses: empty = ()
  • A tuple with one item is constructed by following a value with a comma: sigleton = 'hello',

The statement t = 1, 2, 'hello' is an example of tuple packing: the values are packed together in a tuple. The reverse operation is also possible: x, y, z = t.

Sets

{} or set() function can be used to create sets. Note: to create an empty set you have to use set(), not {}; the latter creates an empty dictionary.

Example:

a = set('abracadabra')
b = set('alacazam')
  • a - b: letters in a but not in b
  • a | b: letters in either a or b
  • a & b: letters in both a and b
  • a ^ b: letters in a or b but not both

Similaryly to list comprehensions, set comprehensions are also supported.

Dictionaries

Dictionaries are indexed by keys, which can be any immutable type; strings and numbers can slways be keys. Tuples can be used as keys if they contain only one kind of item. You can't use use lists as keys, since lists can be modified in place using index assignments, slice assignments, or method like append() and extend().

It is best to think of a dictionary as an unordered set of key: value pairs.

  • del can delete a key: value
  • list(d.keys()) on a dictionary returns a list of all the keys used in the dictionary, in arbitrary order (if you want it sorted, use sortted(d.keys()) instead).
  • To check whether a single key is in the dictionary, use the in keyword. (in or not in)
  • Dict comprehensions can be used to create dictionaries from arbitrary key and value expressions: {x: x**2 for x in range(10)}
  • When the keys are simple strings, it is sometimes easier to specify pairs using keyword arguments: dic(sape=1, guido=2, jack=3) => {'sape': 1, 'jack': 3, 'guido': 2}

Looping Techniques

When looping through dictionaries, the key and corresponding value can be retrieved at the same time using the items() method.

knights = {'gallahad': 'the pure', 'robin': 'the brave'}
for k, v in knights.items():
    print(k, v)

When looping through a sequence, the position index and correspoding value can be retrieved at the same time using the enumerate() function.

for i, v in enumerate(['tic', 'tac', 'toe']):
    print(i, v)

To loop over two or more sequences at the same time, the entries can be paired with the zip() function.

numbers = [1, 2, 3]
names = ['one', 'two', 'three']
for number, name in zip(numbers, names):
    print('Number: {0}, Name: {1}'.format(number, name))

To loop over a sequence in sorted order, use the sorted() function which return a new sorted list while leaving the source unaltered. for item in soted(list)

It is sometimes tempting to change a list while you are looping over it; however, it is often simple and safer to create a new list instead.

More on Conditions

  • in and not in: check whether a value occurs (or not) in a sequence.
  • is and is not: compare whether two objects are really the same object; this only matters for mutable objcts like lists.
  • Comparisons can be chained. a < b == c
  • and and or are short-circuit operators

Comparing Sequences and Other Types

The comparison uses lexicographical ordering.

Modules

A module is a file containing Python definitions and statements. The file name is the module name with the suffix .py appended. Within a module, the module's name (as a string) is available as the value of the global variable __name__.

More on Modules

Note that in general the practice of importing * from a module is frowned upon, since it often causes poorly readable code. (It ok to use in interactive sessions.)

It's one module you want to test interactively, use importlib.reload().

import importlib
importlib.reload(modulename)

Executing modules as scripts

if __name__ == "__main__":
    code

This is often used either to provide a convenient user interface to a module, or for testing purposes (running the module as a script executes a test suite).

The Module Search Path

When a module named spam is imported, the interpreter first searches for a built-in module with that name. If not found, it then searches for a file named spam.py in a list of directories given by the variable sys.path, it is initialized from these locations:

  • The directory containing the input script (or the current directory when no file is specified).
  • PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
  • The installation-dependent default.

After initialization, Python programs can modify sys.path. The directory containing the script being run is placed at the beginning of the search path, ahead of the standard library path. This means that scripts in that directory will be loaded instead of modules of the same name in the library directory.

"Compiled" Python files

Python caches the compiled version of each module in the __pycache__ directory. It generally contains the Python version number. This naming convention allows compiled modules from dirrerent release and different version of Python to coexist. (Example: __pycache__/fib.python-27.pyc)

Python check the modification date of the source against the compiled version to see if it's out of date and needs to be recompiled.

Standard Modules

>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!

The variable sys.path is a list of strings that determines the interpreter's search path for modules. You can modify it using standard list operations.

The dir() Function

The built-in function dir() is used to find out which names a module defines.

Without arguments, dir() lists the names you have defined currently.

It list all types of names: variable, modules, functions, etc.

Input and Output

Methods of File Objects

It is good practice to use the with keyword when dealing with file objects. This has the advantage that the file is properly closed after its suite finishes, even if an exceptiohn is raissed on the way. It is also much shorter thatn writing equivalent try-finally blocks:

with open('workfile', 'r') as f:
    read_data = f.read()

2016/07/02 阅读笔记

How I became (and stayed) a successful programmer

How do I take a new skill like programming, grow it, shape it, and tune it over time so I can achieve longevity in the industry?

1. I surround myself with programmers who are way better than me

The best way to improve (at anything) is to learn from people better than you.

I remind myself to talk less and to listen more.

2. I occasionally leave my comfort zone

Leaving comfort zone helps me think differently by challenging a bunch of established ideas I already have.

Maybe you don't want to do this constantly because it can be hard to get into a rhythm with your normal area of work. But in moderation it can really open your mind to new ways of thinking.

Find a programming task that takes you out of your comfort zone and make it your next project. Then watch it pay off in spades.

3. I value being independent

What't most inportant is how you choose to find the answers to your questions.

This means I'll try to do most things myself first, and only when I really get stuck, I'll ask for help.

Benefits:

  • You learn how to be resourceful.
  • You earn respect by being courteous of other people's time and work.
  • You start developing your creativity.

Please don't quit -- every expert was once a beginner

Great developments never come from within your comfort zone

It's always hard to push yourself to the next level, because it requires greater effort than the usual. It requires more energy, which may tire you physically and mentally.But don't think about that. Think about the result. The achievement.

Every time you take a step toward expertise, no matter how small, it is still a step.

Learn in diffuse mode

Since it may be hard to get a concept the first time you read about it, you can do something that will make it better for you and help you get it quickly.

Read the full text with total focus (first time, even if you don't understand), okay? Now, read it again. After the second time, google the concept and try to read about it in different articles. That will help you see it from different angles, which will help you better understand it.

Next time is to read the text a third time. Your understanding of the concept is much better now than it was the first time you read it, even though in all likelihood you're still early on in your quest to truly understand that concept.

This medhod is called Diffuse mode. You can apply this type of learning toward applies to videos and tutorials, as well.

Every expert was once a beginner.

"You never fail until you stop trying." - Albert Einstein

You should know that reaching a high level takes time.

"I'm convinced that about half of what separates the successful Entrepreneurs from the non-successful ones is pure perseverance." - Steve Jobs

Some tips:

  • Don't work alone.
  • Don't wait for inspiration. Discipline is reliable.
  • Real work usually isn't fun.
  • Practice, practice, and practice.
  • Tutorials fish on your behalf, so you don't need to learn how to fish yourself. You need to read books.
  • Always try to understand a new concept using different resources.
  • Don't just read. Build. Try, try, and keep trying.
  • Frustration, boredom, tiredness, exhaustion -- these are all normal.
  • Ask.

"Long-term consistency trumps short-term intensity." - Bruce Lee

2016 Week 21 阅读笔记

怎样花两年时间去面试一个人

The great software developers, indeed, the best people in every field, are quite simply never on the market.j

The average great software developer will apply for, total,maybe, four jobs in their entire career.

—— Joel Spolsky

  1. 最好的人也许不投简历,就决定去哪里了。所以要在他们做决定之前找到他们。
  2. 比较差的会投很多次简历,找不到工作的时间约多,投的简历越多,给整个 pool 带来很多噪音,top 10% 的简历也许根本不算全部人的 top 10%。

—— 邹欣

Joel Spolsky 写了一本书,专门讲了公司招聘的心得和体会,《Smart and Gets Things Done》

现在绝大多数应届生简历而言,也许最具信息量的部分不是「精通 xxx,熟悉 yyy,掌握 zzz」,不是「在 uuu 实习过」,也不是这个项目那个作业,反倒是越来越被认为不重要的一项:毕业学校。原因是简历上的其他条目的信息量太小了。

很多时候,是否好好看完一本好书,对一个人的提升往往能达到质的区别。就算不好好看完一本书,马马虎虎看完,只要书是真的函数,也肯定会有很大提高。

好书和坏书的差别,从本质上,就是学习效率和大方向的差别。读烂书浪费时间,但读好书却节省时间。

「书单计划」的优点:

  1. 清晰、明确。完全可度量。
  2. 防伪:读没读过,随便一问便知。而正因为应聘者也知道这事不像实习经验可以忽悠,所以也不敢乱往简历上捅词。
  3. 不在乎是否「泄题」:书单是完全公开的,无所谓,本来就是要你去读的。
  4. 管你用心不用心读,只要读了,读完了,就有区别。(笔者注:根据经验,没有用心读完可能真的没有什么卵用)
  5. 不存在「怎么做」的障碍:所有人都知道怎么读书——一页一页读。
  6. 不需要招聘者投入精力:书单在此,就这么简单。
  7. 评估的负担很大程度上转移到了应聘者身上:是不是认真看完了,有没有心得体会。

「书单计划」的背后是另一个悲剧的现实,如果不是因为这个现实,这个计划也完全没有必要,那就是,中国 IT 大学教育当中要求学的书,和企业真正需要你读的书相比,不是完全不够用,就是写得不够好,或者更悲剧的就是根本用不上,所以在这个大背景下出来的牛人都是自己淘书自己学的。

第一份工作的月薪 = 大学四年买过的技术书籍价格的总和

—— 熊力

GitHub

有经验的面试者只要稍稍扫两眼一个人的 GitHub 历史,跳出几个 check-in 历史看一看,便完全能够迅速判断这个人是否满足他的要求。不再需要费劲心机地去想题目,去观察,去揣测,去花费大量的时间的同时还只能采样到几个极为有限的点。

书单 + GitHub,就相当于一个两年左右的面试。

没有哪个行业像 IT 行业这样特殊:没有什么东西不能够(应该)在互联网上学到的。


一些书单

  1. 《编码的奥秘》
  2. 《深入理解计算机系统》
  3. 《Windows 核心编程》
  4. 《程序员的自我修养》
  5. 《代码大全》
  6. 《程序员修炼之道》
  7. 《编程珠玑》
  8. 《编程之美》
  9. 《The C Programming Language》
  10. 《The C++ Programming Language》
  11. 《Programming: Principles and Practice Using C++》
  12. 《Accelerated C++》
  13. 《计算机程序的构造和解释》
  14. 《Clean Code》
  15. 《Implementation Patterns》
  16. 《Design Patterns》
  17. 《Agile Software Development, Principles, Patterns, and Practices》
  18. 《Refactoring》
  19. 《C++ 编程思想》
  20. 《Effective C++》
  21. 《深度探索 C++ 对象模型》
  22. 《C++ 语言的设计和演化》
  23. 《C 专家编程》
  24. 《C 陷阱与缺陷》
  25. 《C 语言接口与实现》
  26. 《Lua 程序设计》
  27. 《Linkers and Loaders》
  28. 《COM 本质论》
  29. 《深入理解 Windows 操作系统》
  30. 《Unix 编程艺术》
  31. 《代码优化:有效使用内存》
  32. 《深入理解 Linux 内核》
  33. 《TCP/IP 详解》
  34. 《软件随想录》
  35. 《黑客与画家》
  36. 《编程人生》
  37. 《人月神话》
  38. 《算法导论》
  39. 《快速软件开发——有效控制与完成进度计划》
  40. 《IT 项目管理那些事》
  41. 《最后期限》
  42. 《走出软件作坊》
  43. 《你的灯亮着吗》
  44. 《Algorithms》(by Sanjoy Dasgupta, Christos Papadimitriou and Umesh Vazirani)
  45. 《Data Structures and Algorithms》
  46. 《The Design of the UNIX Operating System》
  47. 《Compilers》(龙书)
  48. 《Computer Architecture: A Quantitative Approach》
  49. 《Flow》
  50. 《Outliers》(Why hard work and luck are both important)

一直以来伴随我的一些学习习惯(三):阅读方法

  1. 乘着对一件事有热情的时候,一股脑把万事那个最难的阶段熬过去。
  2. 根据主题来查阅资料,而不是根据资料来查阅主题。按照主题来阅读,你会发现读的时候不再是老老实实地一本书看完再看另一本,而是非常频繁地从一本书跳到另一本书,从一处资料跳到另一处资料,从而来获得多个不同的人对同一个主题是如何讲解的。因为即便是经典的书,你也不能指望它对其中每一个主题的介绍都是尽善尽美的,有些书对某个主题(知识点)的介绍比较到位,有些书则对另一些知识点介绍得比较到位。而有时候一篇紧凑的 paper 比一本书上讲得还要好。我硬盘里面的书按照主题分类,每个主题下面都有一堆书,当我需要学习某个主题的知识时,我会把里面涉及这个主题的书都翻开来,索引到相关章节,然后挑讲得好的看。
  3. 好资料,坏资料。好资料的特点:从问题出发;重点介绍方法背后的理念,注重直观解释,而不是方法的技术细节;按照方法被发明的时间流程来介绍(先是遇到什么问题,然后怎样分析,推理,最后发现目前所使用的方法)。坏资料的特点是好资料的反面:上来就讲方法细节,仿佛某方法是从天上掉下来的。根本不讲为什么要用这个方法,人们最初是因为面对什么问题才想到这个方法的,其间又是怎样才想出这个方法的,方法背后的直观思想又是什么。
  4. 学习一个东西之前,首先在大脑中积累充分的「疑惑感」。即弄清面临的问题到底是什么,在浏览方法本身之前,最好先使劲问问自己能想到什么方法。一个公认的事实是,你对问题的疑惑越大,在之前做的自己的思考越多,当看到借到之后的印象就越深刻。
  5. 有选择地阅读。这里的选择体现在两个地方,一是选择一本书中感兴趣的章节优先阅读,二是对一本书中技术性较弱或信息密度较低的部分快速地略读。一般来说,除了技术性非常强的书之外,大多数书的信息密度很低,有很多废话。一般来说在阅读的时候应该这样来切分内容:1. 问题是什么?2. 方案是什么?3. 例子是什么?如果是需要解释一个现象的,那么1. 现象是什么?2. 解释是什么?3. 之城这个解释的理由是什么? 4. 例子是什么。
  6. 为什么看不懂?1. 你看得不够使劲。对于这类情况,仔仔细细地再多读两遍,多试着去理解两遍,往往会恍然大悟。2. 其中涉及到了你不懂得概念,这是技术性的不理解,这种情况就需要 Cross Reference。如果一句话中用到了你不懂得概念,那就去查。奇怪的是很多人看不懂也不分析一下为什么不懂,就直接放弃了。正如解决问题一样,问题卡住解决不了了,第一时间要做的就是分析为什么解决不了,而不是直接求救。3. 作者讲述的顺序不对,你接着往下看,也许看到后面就明白前面的了。
  7. 如何搜寻到好书。1. 同作者的著作。2. Amazon 相关推荐和主题相关的书列。3. 一本好的著作(或一份好的资料——不管是书还是网页)在参考资料里面重点提到的其他著作。4. 有时对于一个主题,可以搜索到好心人总结的参考资源导引,那是最好不过的。

What every computer science major should know

  • What should every student know to get a good job?
  • Waht should every student know to maintain lifelong employment?
  • What should every student know to enter graduate school?
  • What should every student know to benefit society?

Portfolio versus resume

Every computer science major should build a portfolio.

A portfolio could be as simple as a personal blog, with a post for each project or accomplishment. A better portfolio would include per-project pages, and publicly browsable code (hosted perhaps on github or Google code).

Contributions to open source shold be linked and documented.

A code portflolio allows employers to direcctly judge ablility. GPAs and resumes do not.

Technical communication

I would recommend that students master a presentation tool like PowerPoint or (my favorite) Keynote.

For producing beautiful mathematival documentation, \(LaTex\) has not equal. All written assignments in techical courses should be submitted in LaTex.

An engineering core

The Unix philosophy

The Unix philosophy is one that emphassizes linguistic abstraction and composition in order to effect computation.

Systems administration

Programming Language

Discrete mathematics

Data structures and algorithms

Theory

Architecture

Operating System

Networking

Security

Swift学习笔记:控制流

Repeat-While

repeat-whilewhile 判断循环条件之前,先执行一次循环的代码块,和其他语言中的 do-while 是类似的。

repeat {
    statements
} while condition

Switch

switch 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch语句会执行对应的代码。当有可能的情况较多时,通常用 switch 语句替换 if 语句。

switch 语句必须是完备的,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值得情况下,你可以使用默认(default)分支满足该要求,这个默认分支必须在 switch 语句的最后面。

不存在隐式的贯穿(No Implicit Fallthrouth)

和其他语言的 Switch 不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。所以,不需要再 case 分支中显式地使用 break 语句。

如果想要贯穿到特定的 case 分支中,请使用 fallthrough 语句。

区间匹配

case 分支的模式也可以是一个值得区间。

元组

可以只用元组在同一个 switch 语句中测试多个值,元组中的元素可以是值,也可以是区间,使用下划线 _ 来匹配所有可能的值。

值绑定

case 分支的模式允许将匹配的值绑定到一个临时的常量或变量,这些常量或变量在该 case 分支里就可以被引用了。

case 分支的模式可以使用 where 语句来判断额外的条件。

控制转移语句

continue

continue 语句告诉一个循环体立刻停止本次循环迭代,重现开始下次循环迭代。

break

break 语句会立即结束整个控制流的执行。

带标签的语句

在 Swift 中,你可以在循环体和 switch 代码块中嵌套循环体和 switch 代码块来创造复杂的控制流结构。然而,循环体和 switch 代码块亮着都可以使用 break 语句来提前结束整个方法。因此,显式地指明 break 语句想要终止是哪个循环体或者 switch 代码块,会很有用。

为了实现这个目的,可以使用标签来标记一个循环体或者 switch 代码块,当使用 break 或者 continue 时,带上这个标签,可以控制该标签代表对象的中断或者执行。

语法:

label name: while condition { statements }

提前退出

if 语句一样, guard 的执行取决于一个表达式的布尔值。我们可以使用 guard 语句来要求条件必须为真时,以执行 guard 语句后的代码。不同于 if 语句,一个 guard 语句总是有一个 else 分句,如果条件不为真则执行 else 分句中的代码。

Swift 学习笔记:字符串和字符

字符串是值类型

使用字符串会进行值拷贝。

在实际的编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着将字符串作为值类型的同时可以获得极高的性能。

Working with Characters

可以使用 for-in 循环来遍历字符串中的 characters 属性来获取每一个字符的值。

字符串可以通过传递一个值类型为 Character 的数组作为自变量来初始化。

连接字符串和字符

字符串可以通过加法运算符相加在一起创建一个新的字符串。

append() 方法可以将一个字符附加到一个字符串变量的尾部。

Unicode

Unicode Scalars(Unicode 标量)

Swift 的 String 类型是基于 Unicode 标量建立的。Unicode 标量是对应字符或者修饰符的唯一的 21 位数字。

Unicode 码位(code poing)的范围是 U+0000U+1F425 或者 U+E000U+10FFFF。Unicode 标量不包括 Unicode 代理项(surrogate pair)码位,其码位范围是 U+D800U+DFFF

注意,并不是所有的 21 位 Unicode 标量都表示一个字符,因为有一些标量是留作未来分配的。

字符串字面量的特殊字符(Special Character in String Literals)

可扩展的字形群集(Extended Grapheme Clusters)

每一个 Swift 的 Character 类型代表一个可扩展的字形群。一个可扩展的字形群是一个或多个可生成人类可读的字符 Unicode 标量的有序排列。

计算字符数量

如果想要获得一个字符串中的 Character 值得数量,可以使用字符串的 characters 属性的 count 属性。

注意在 Swift 中,使用可拓展的字符群集作为 Character 值来连接或改变字符串时,并不一定会更改字符串的字符数量。

访问和修改字符串

字符串索引

使用 startIndex 属性可以获取一个 String 的第一个 Character 的索引。使用 endIndex 属性可以获取最后一个 Character 的后一个位置的索引,所以 endIndex 属性不能作为一个字符串的有效下标。如果 String 是空串,两者是相等的。

String.Index.predecessor() 获得前一个索引,successor() 获得后一个索引。

Characters 属性的 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单个字符。

插入和删除

  • insert(_:atIndex:):在一个字符串的制定索引插入一个字符
  • insertContentsOf(_:at:):在一个字符串的指定索引插入一个字符串
  • `removeAtIndex(_:):在一个字符串的指定索引删除一个字符
  • removeRange(_:):在一个字符串的指定索引删除一个子字符串

比较字符串

字符串/字符相等

使用 ==!= 来比较两个字符/字符串是否相等/不等。

前缀/后缀相等

  • hasPrefix(_:)
  • hasSuffix(_:)

字符串的 Unicode 表示形式(Unicode Representations of Strings)

当一个 Unicode 字符串被写进文本文件或其他存储时,字符串中的 Unicode 标量会用 Unicode 定义的几种编码格式(encoding forms)编码。每一个字符串中的小块编码都被称为 code units。这些包括 UTF-8,UTF-16,UTF-32 编码格式。

Swift 学习笔记:属性

属性将值跟特定的类、结构或枚举关联。存储常量或变量作为实例的一部分,而计算属性(不是存储)一个值。

  • 计算属性:类、结构体、枚举
  • 存储属性:类、结构体

存储属性

延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标记一个延迟存储属性。

必须将延迟存储属性声明称变量,因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明称延迟属性。

延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值得外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。

计算属性

计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

便捷 setter 声明

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue

只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置新的值都会调用属性观察器,即使新值和当前值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。你不必为非重写的计算属性添加属性观察器,因为可以通过它的 setter 直接监控和响应值得变化。

  • willSet 在新的值被设置之前调用
  • didSet 在新的值被设置之后立即调用

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。

didSet 观察器会将旧的属性值作为参数传入,可以为该参数名或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

父类的属性在子类的构造器中被赋值时,它在父类中的 willSetdidSet 观察器会被调用,随后才会调用子类的观察器。在父类初始化方法调用之前,子类给属性赋值时,观察器不会被调用。

如果将属性通过 in-out 方式传入函数, willSetdidSet 也会被调用。这是因为 in-out 参数采用了拷入拷出模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。

全局变量和局部变量

计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。

全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记 lazy 修饰符。

局部范围的常量或变量从不延迟计算。

类型属性

存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。

Swift 学习笔记:集合类型

Swift 语言提供 Arrays、Sets 和 Dictionaries 三种基本的集合类型来存储数据合集。数组(Array)是有序数据的集;集合(Sets)是无序无重复数据的集;字典(Dictionaries)是无序的键值对的集。

集合的可变性

在我们不需要改变集合的时候创建不可变集合是很好的实践(使用 let),如此 Swift 编译器可以优化我们创建的集合。

数组

创建一个带有默认值的数组

Swift 中的 Array 类型提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(count)和适当类型的初始值(repeatedValue)传入数组构造函数:

var threeDoubles = [Double](count: 3, repeatedValue: 0.0)

通过两个数组相加创建一个数组

我们可以使用加法操作符来组合两种已经存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来。

访问和修改数组

isEmpty 检查数组是否为空。

使用加法赋值运算符(+=)可以直接在数组后面添加一个或多个拥有相同类型的数据项。

调用数组的 insert(_:atIndex:)方法来在某个具体的索引值之前添加数据项。

数组的遍历

如果我们同事需要每个数据项的值和索引值,可以使用 enumerate() 方法来进行数组遍历。enumerate() 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历。

集合(Sets)

集合用来存储相同类型且没有确定顺序的值。

Swift 中的 Set 类型被桥接到 Foundation 中的 NSset 类。

集合类型的哈希值

一个类型为了存储在集合中,该类型必须是可哈希化得——也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int 类型的,相等的对象哈希值必须相同,比如a=b,因此必须是a.hashValue = b.hashValue

Swift 的所有基本类型默认都是可哈希化的,可以作为集合的值类型或者字典的键类型。没有关联值的枚举成员值默认也是可哈希化的。

你可以使用你自定义的类型作为集合的值类型或者是字典的键的类型,但你需要使你的自定义类型符合 Swift 标准库中的 Hashable 协议。符合 Hashable 协议的类型需要提供一个类型为 Int 的可读属性 hashValue。由类型的 hashValue 属性返回的值不需要再同一程序的不同执行周期或者不同程序之间保持相同。

因为 hashValue 协议符合 Equatable 协议,所以符合该协的类型也必须停工一个 == 运算符的实现。这个 Equatable 协议要求任何符合 == 实现的实例间都是一种相等的关系,也就是说,对于 a, b, c 三个值来说,必须满足下面三种情况:

  • 自反性:a == a
  • 对称性:a == b, b == a
  • 传递性:a == b && b == c, a == c

遍历一个集合

Swift 中的 Set 类型没有确定的顺序,为了按照特定顺序来遍历一个 Set 中的值可以使用 sort() 方法,它将根据提供的序列返回一个有序集合。

for genre in favoriteGenres.sort() {...}

集合操作

字典

字典中的数据项并没有具体顺序。

Swift 中 Dictionary 类型被桥接到 Foundation 中的 NSDictionary 类。

字典遍历

  1. 元组方式
  2. dic.keys
  3. dic.values

如果我们只是需要某个字典的键值集合来作为某个接受 Array 实例的 API 的参数,可以直接使用 keys 或 values 属性构造一个新数组。

let airportCodes = [String](airports.keys)
let airportNames = [String](airports.values)

Swift 的字典类型书无序集合类型。为了以特定的顺序便利字典的键或值,可以对字典的 keys 或 values 属性使用 sort() 方法。

Swift 学习笔记:基本运算符

空合运算符(NIl Coalescing Operator)

空合运算符(a ?? b)将对可选类型 a 进行空判断,如果 a 包含一个值就进行解封,否则就返回一个默认值 b。表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致。

空合运算符是对以下代码的简短表达方法:

a != nil ? a! : b

如果 a 为非空值(non-nil),那么 b 值将不会被计算,这也就是所谓的短路求值。

2016 Week 19 阅读笔记

准备好迎接 3.0 API 变化

在即将发布的 Swift 3 将在 Cocoa 和 CocoaTouch 上做出重大改变。

某些 API 的名字变得更加简洁了。

PROGRAMMING BY POKING: WHY MIT STOPPED TEACHING SICP

关于为什么 MIT 停止了赫赫有名的 6.001,Gerry Sussman 的回答:

  1. 教了太多年,不想教了。
  2. 这门课放到今天已经没有那么重要了。在 80 年代和 90 年代,工程师们通过把简单和易于理解的部分组成复杂的系统,SICP 就是为了提供这种抽象的语言而存在的。

He said that programming today is “More like science. You grab this piece of library and you poke at it. You write programs that poke it and see what it does. And you say, ‘Can I tweak it to do the thing I want?'”. The “analysis-by-synthesis” view of SICP — where you build a larger system out of smaller, simple parts — became irrelevant. Nowadays, we do programming by poking.

为什么现在改用 Python 了?

Python has a ton of libraries that make it applicable to many types of projects that instructors might want to assign (like writing software to control a robot.)

Python 库多 -_-|||

另外,Sussman 还说道 SICP 确实比现在使用 Python 教学更加 coherent。

什么是 Git

Git 采用分散式架构,是分散式版本管理 DVCS(Distributed Version Control System)的代表。

性能

不同于某些版本管理软件,Git 在决定代码修改历史以及保存形式的时候不会被文件名的变化所愚弄,Git 关注的文件内容本身。在实际的操作中,Git 使用一种混合了差分编码(delta encoding,仅保存代码修改的差分),压缩,直接保存,以及版本元数据(version metadata objects)的管理方式。

安全性

Git 将保持所管理代码的整合性作为首要要务。所有的文件内容,文件相互关系,以及文件目录结构,版本,标签以及修改,都经过加密哈希校验算法(SHA1)的保护。这可以防止各种意外的代码修改食物,或者是第三者的恶意修改,使得代码修改历史完全有迹可循。

Swift 学习笔记:高级运算符

与 C 语言中的算术运算符不同,Swift 中的算数运算符默认是不会溢出的。所有溢出行为都会被捕获并报告错误。

如果想让系统允许溢出行为,可以选择使用 Swift 中另一套默认支持溢出的运算符,所有的这些溢出运算符都是以&开头的。

在 Swift 中可以自由地定义中缀、前缀、后缀和赋值运算符,以及相应的优先级与结合性。这些运算符在代码中可以像预定义的运算符一样使用,我们甚至可以扩展已有的类型以支持自定义的运算符。

位运算符

位运算符可以操作数据结构中每个独立的比特位。它们通常被用在底层开发中,比如图形编程和创建设备驱动。位运算符在处理外部资源的原始数据时也十分有用,比如对自定义通信协议传输的数据进行编码和解码。

按位取反运算符

按位取反运算符 ~ 可以对一个数值的全部比特位进行取反:

按位取反运算符是一个前缀运算符,需要直接放在运算的数之前,并且它们之间不能添加任何空格。

按位与运算符

按位与运算符 & 可以对两个数的比特位进行合并。

按位或运算符

按位或运算符 | 可以对两个数的比特位进行比较。

按位异或运算符

按位异或运算符 ^ 可以对两个数的比特位进行比较。

按位左移、右移运算符

按位左移运算符 << 和按位右移运算符 >> 可以对一个数的所有位进行制定位数的左移和右移,但是需要遵守下面定义的规则。

对一个数进行按位左移或按位右移,相当于对这个数进行乘以2或除以2的运算。

无符号整数的移位运算

规则:

  1. 已经存在的位按制定的位数进行左移和右移
  2. 任何因移位而超出整型存储范围的位都会被丢弃
  3. 0 来填充移位后产生的空白位

这种方法称为逻辑移位

有符号整数的移位运算

对比无符号整数,有符号整数的移位运算相对复杂得多,这种复杂性源于有符号整数的二进制表现形式。(为了简单起见,以下的示例都是基于 8 比特位的有符号整数的,但是其中的原理对于任何位数的有符号整数都是通用的。)

有符号整数使用第 1 个比特位(通常被称为符号位)来表示这个数的正负。符号位为 0 代表正数,为 1 代表负数。

其余的比特位(通常被称为数值位)存储了实际的值。有符号正整数和无符号数的存储方式是一样的,都是从 0 开始算起。

负数的存储方式略有不同。它存储的值得绝对值等于 2n 次方减去它的实际值(也就是数值位表示的值),这里的 n 是数值位的比特位。一个 8 比特位的数有 7 个比特位是数值位,所以是 27 次方,即 128

负数的表示通常被称为二进制的补码表示。用这种表示方法来表示负数乍看起来有点奇怪,但它有几个优点。

首先,如果想对 -1-4 进行加法运算,我们只需要将这两个数的全部 8 个比特位进行相加,并将计算结果中超出 8 位的数值丢弃:

其次,使用二进制补码可以使负数的按位左移和右移运算得到跟正数相同的结果,即每向左移一位就将自身的数值乘以 2,每向右移一位就将自身除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:

这个行为可以确保有符号整数的符号位不会因为右移运算而改变,这通常被称为算数移位

溢出运算符

在默认情况下,当向一个整数赋予超过它容量的值时,Swift 默认会报错,而不是生成一个无效的数。这个行为为我们在运算过大或者过小的数的时候提供了额外的安全性。

当然,也可以选择让系统在数值溢出的时候采取截断处理,而非报错。可以使用 Swift 提供的三个溢出运算符来让系统支持整数溢出运算。这些运算符都是以 & 开头的:

  • 溢出加法 &+
  • 溢出减法 &-
  • 溢出乘法 &*

数值溢出

数值有可能出现上溢或者下溢。

溢出也会发生在有符号整型数值上。在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也要参与计算,正如按位移位运算符所描述的。

对于无符号与有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数编程最小的数。同样的,当发生下溢时,它们会从所能容纳的最小数编程最大的数。

优先级和结合性

运算符的优先级使得一些运算符优先于其他运算符,高优先级的运算符会先被计算。

结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边结合为一组,还是与右边结合为一组。

运算符函数

类和结构体可以为现有的运算符提供自定义的实现,这通常被称为运算符重载。

前缀和后缀运算符

类与结构体也能提供标准单目运算符的实现。单目运算符只运算一个值。当运算符出现在值之前时,它就是前缀的,而当它出现在值之后时,它就是后缀的。

要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func 关键字之前制定 prefix 或者 postfix 修饰符。

符合赋值运算符

符合赋值运算符将赋值运算符 = 与其他运算符进行结合。在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内被直接修改。

还可以将赋值与 prefixpostfix 修饰符结合起来,例如实现自增运算符 ++

注:不能对默认的赋值运算符 = 进行重载。只有组合赋值运算符可以被重载。同样地,也无法对三目条件运算符 a ? b : c 进行重载。

等价运算符

自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为相等运算符 == 与不等运算符 !=。对于自定义类型,Swift 无法判断其是否相等,因为「相等」的含义取决于这些自定义类型在你的代码中所扮演的角色。

为了使等价运算符能对自定义的类型进行判等运算,需要为其提供自定义的实现,实现的方法与其他中缀运算符一样。

自定义运算符

除了实现标准运算符,在 Swift 中还可以声明和实现自定义运算符。

新的运算符要使用 operato 关键字在全局作用域内进行定义,同时还要指定 prefiinfix或者postfix修饰符:

prefix operator +++ {}

自定义中缀运算符的优先级和结合性

自定义的中缀运算符也可以指定优先级和结合性。

结合性可取的值有 leftrightnone。当做结合运算符跟其他相同优先级的左结合运算符写在一起时,会跟左边的值进行结合。同理,当右结合运算符跟其他相同优先级的右优先级运算符写在一起时,会跟右边的值进行结合。而非结合运算符不能跟其他相同优先级的运算符写在一起。

结合性的默认值是 none,优先级的默认值是 100

下面的例子定义了一个新的中缀运算符 +-,此运算符的结合性为 left,并且它的优先级为 140

infix operator +- { associativity left precedence 140 }

注:当定义前缀与后缀运算符的时候,我们并没有制定优先级。然而,如果对同一个值同时使用前缀和后缀运算符,则后缀运算符会先参与运算。

Swift 学习笔记:构造过程

存储属性的初始赋值

类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储属性的值不能处于一个未知的状态。

当你为存储属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者(property observers)。

默认属性值

如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的效果是一样的,只不过使用默认值让属性的初始化和声明结合得更加紧密。

参数的内部名称和外部名称

跟函数和方法参数相同,构造参数也拥有一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。

因为构造器并不像函数和方法那样在括号前有一个可辨别的名字。因此在调用构造器时,主要通过构造器中的参数名和类型来确定应该被调用的构造器。如果你在定义构造器时没有提供参数的外部名字,Swift 会为构造器的每个参数自动生成一个跟内部名字相同的外部名。

不带外部名的构造器参数

如果你不希望构造器的某个参数提供外部名字,你可以使用下划线(_)来显式描述它的外部名。

可选属性类型

可选类型的属性将自动化初始为 nil,表示这个属性是有意在初始化时设置为空的。

构造过程中常量属性的修改

你可以在构造过程中的任意时间点给常量属性指定一个值,只要在构造过程结束时是一个确定的值。一旦常量属性被赋值,它将永远不可更改。

默认构造器

如果结构体或类的所有属性都有默认值,同时没有定义的构造器,那么 Swift 将会给这些结构体或类提供一个默认构造器。这个默认构造器将简单创建一个所有属性都设置为默认值得实例。

结构体的逐一成员构造器

如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器,即使结构体的存储属性没有默认值。

注:类类型没有逐一成员构造器。

值类型的构造器代理

构造器可以通过调用其他构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重用。

构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其他构造器。类则不同,它可以继承自其他类,这意味着类有责任保证其所有继承的存储属性在构造时也能正确的初始化。

对于值类型,可以使用 self.init 在自定义的构造器中引用类型中的其他构造器。并且你只能在构造器内部调用 self.init

如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器。

加入你希望默认构造器、逐一成员构造器以及你自己的定义的构造器都能用来创建实例,可以将自定义的构造器写到 extension 中,而不是写在值类型的原始定义中。

类的继承和构造过程

类里面的所有存储属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。

制定构造器和便利构造器

制定构造器(designated initializers)是类中最主要的构造器。一个制定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。

便利构造器(convenience initializers)是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并未其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值得实例。

制定构造器和便利构造器的语法

类的制定构造器的写法和值类型的简单构造器一样。

便利构造器也采用相同样式的写法,但需要在 init 关键字之前放置 convenience 关键字。

类的构造器代理规则

为了简化制定构造器和便利构造器之间的调用关系,Swift 采用以下三条规则来限制构造器之间的代理调用:

  1. 指定构造器必须调用其直接分类的指定构造器。
  2. 便利构造器必须调用同一类中定义的其他构造器。
  3. 便利构造器必须最终导致一个制定构造器被调用。

制定构造器必须总是向上代理
便利构造器必须总是横向代理

两段式构造过程

Swift 中类的构造过程包含两个阶段。

  1. 每个存储属被引用它们类指定的一个初始值;
  2. 当每个存储属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储属性。

两段式构造过程让构造器更加安全,同时在整个类层级结构中给与了每个类完全的灵活性。两段式构造可以防止属性在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

Swift 编译器将执行4中有效的安全检查,以确保两段式构造能不错地完成:

  1. 制定构造器必须保证它坐在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
  2. 制定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没有这么做,制定构造器赋予的新值将被父类中的构造器覆盖。
  3. 便利构造器必须先代理调用同一类中的其他构造器,然后再为任意属性赋新值。如果没有这么做,便利构造器赋予的新值将被同一类中的其他制定构造器所覆盖。
  4. 构造器在第一阶段完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

构造器的继承和重写

Swift 中的子类默认情况下不会继承父类的构造器,这种机制可以防止一个父类的简单构造器被一个更专业的类继承,并被错误地用来创建子类实例。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符。

当你重写一个父类的指定构造器时,你总是需要写 override 修饰符,即使你的子类将父类的制定构造器重写为便利构造器。

相反,如果你编写了一个和父类便利构造器想匹配的子类构造器,由于子类不能直接调用父类的便利构造器,因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中「重写」一个父类便利构造器时,不需要加 override 前缀。

子类可以在初始化时修改继承来的变量属性,但是不能修改继承来的常量属性。

构造器的自动继承

如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。在实践中,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

可失败构造器

如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。

为了妥善处理这种构造过程中可能会失败的情况,你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器,其语法为在 init 关键字后面添加问好 init?

可失败构造器的参数名和参数类型,不能与其他非可失败构造器的参数名、及参数类型相同。

可失败构造器会创建一个类型为自身类型的可选类型对象。你通过 return nil 语句来表明可失败构造器在何种情况下应该失败。

注:严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 return nil 表明可失败构造器失败,而不要用关键字 return 来表明构造器成功。

枚举类型的可失败构造器

你可以通过一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的参数无法匹配任何枚举成员,则构造失败。

带原始值的枚举类型的可失败构造器

带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:),该可失败构造器有一个名为 rawValue 的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能和某个枚举成员的原始值匹配,则该构造器会构成相应的枚举成员,否则构造失败。

构造失败的传递

类、结构体、枚举的可失败构造器可以横向代理到乐熊中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

重写一个可失败构造器

如果其他的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器。

在重写父类中必要的指定构造器时,不需要添加 override 修饰符。

通过闭包或函数设置属性的默认值

如果某个存储属性的默认值需要一些定制或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被创建时,对应的闭包或函数就会被调用,而它们的返回值会当做默认值赋值给这个属性。

这种类型的闭包或函数通常会创建一个跟属性相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。

class SomeClass {
    let someProperty: SomeType = {
        // 在这个闭包中给 someProperty 创建一个默认值
        // someValue 必须和 SomeType 类型相同
        return someValue
    }()
}

闭包结尾的大括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包,如果忽略了这对括号,相当于闭包本身作为值赋给了属性,而不是将闭包的返回值赋值给属性。

注:如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其他部分还没有初始化。这意味着你不能再闭包里访问其他属性,即使这些属性有默认值,同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。

Swift 学习笔记:访问控制

访问控制可以限定其他源文件或者模块中的代码对你的代码的访问级别。这个特性可以让我们隐藏代码的一些实现细节,并且可以为其他人可以访问和使用的代码提供接口。

模块和源文件

Swift 中的访问控制模型基于模块和源文件这两个概念。

模块指的是独立的代码单元,框架或应用程序会作为一个独立的模块来构建和发布。一个模块可以使用 import 关键字导入另一个模块。

在 Swift 中,Xcode 的每个 target(例如框架或应用程序)都被当做独立的模块处理。如果你是为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成独立的框架,这个框架就是 Swift 中的一个模块。

源文件就是 Swift 中的代码源文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

  • public:可以访问同一模块源文件中的任何实体,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,框架中的某个接口可以被任何人使用时,你可以将其设置为 public 级别。
  • internal:可以访问同一模块源文件中的任何实体,但是不能从模块外访问该模块源文件的实体。通常情况下,某个接口只在应用程序或框架内部使用时,你可以将其设置为 internal 级别。
  • private:限制实体只能在所在的源文件内部使用。使用 private 级别可以隐藏某些功能的实现细节。

Swift 中的 private 访问级别不同于其他语言,它的范围限于源文件,而不是声明范围内。这就意味着,一个类型可以访问其所在源文件中的所有 private 实体,但是如果它的扩展定义在其他源文件中,那么它的扩展就不能访问它在这个源文件中定义的 private 实体。

访问级别的基本原则

Swift 中的访问级别遵循一个基本原则:不可以在某个实体定义访问级别更高的实体。

默认访问级别

不显式制定,默认为 internal 级别,有一些例外。

单 target 应用程序的访问级别

当你编写一个单 target 应用程序时,应用的所有功能都是为该应用服务,而不需要提供给其他应用或者模块使用,所以我们不需要明确设置访问级别,使用默认的访问级别 internal 即可。但是,你也可以使用 private 级别,用于隐藏一些功能的实现细节。

框架的访问级别

当你开发框架时,就需要把一些对外的接口定义为 public 级别,以便使用者导入该框架后可以正常使用其功能。这些被你定义为 public 的接口,就是这个框架的 API。

单元测试 target 的访问级别

当你的应用程序包含单元测试 target 时,为了测试,测试模块需要访问应用程序模块中的代码。默认情况下只有 public 级别的实体才可以被其他模块访问。然而,如果在导入应用程序模块的语句前使用 @testable 特性,然后在允许测试的编译设置(Build Options -> Enable Testability)下编译这个应用程序模块,单元测试 target 就可以访问应用程序模块中所有 internal 级别的实体。

元组类型

元组的访问级别将由元组中访问级别最严格的类型来决定。例如,如果你构建了一个包含两种不同类型的元组,其中一个类型为 private 类型,那么这个元组的访问级别为 private

函数类型

函数的访问级别根据访问级别最严格的参数类型或返回值类型的访问级别来决定。

枚举类型

枚举成员的访问级别和该枚举类型相同,你不能为枚举成员单独指定不同的访问级别。

枚举定义中的任何原始值或关联值得类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个 internal 访问级别的枚举中定义 private 级别的原始值类型。

子类

子类的访问级别不得高于父类的访问级别。例如,父类的访问级别是 internal,子类的访问级别就不能是 public

可以通过重写为继承来的类成员提供更高的访问级别。

Getter 和 Setter

常量、变量、属性、下标的 GettersSetters 的访问级别和它们所属类型的访问级别相同。

Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 varsubscript 关键字之前,你可以通过 privat(set)internal(set) 为它们的写入权限制定更低的访问级别。

构造器

自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一例外的是必要构造器,它的访问级别必须和所属类型的访问级别相同。

如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。

默认构造器

默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别是 public。如果一个类型被指定为 public 级别,那么默认构造器的访问级别将为 internal。如果你希望一个 public 级别的参数类型也能在其他模块中使用这种无参数的默认构造器,你只能自己提供一个 public 访问级别的无参构造器。

结构体默认的成员逐一构造器

如果结构体中任意存储型的访问级别为 private,那么该结构体默认的成员逐一构造器的访问级别就是 private。否则,这种构造器的访问级别依然是 internal

协议

协议中的每一个要求都具有和该协议相同的访问级别。你不能将协议中的要求设置为其他访问级别。这样才能确保该协议的所有要求对于任意采纳者都将可用。

如果你定义了一个 public 访问级别的协议,那么该协议的所有实现也会是 public 访问级别。这一点不同于其他类型,例如,当类类型是 public 访问级别时,其成员的访问级别却只是 internal

协议继承

如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相同。

Swift 学习笔记:错误处理

表示并抛出错误

Swift 中的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。

抛出一个错误可以让你表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用 throws 关键字。

处理错误

当某个错误被抛出时,附近的某部分代码必须负责处理这个错误,例如纠正这个问题、尝试另外一种方式、或是向用户报告错误。

当一个函数抛出一个错误时,你的程序流程会发生改变,所以重要的是你能迅速识别代码中会抛出错误的地方。为了标识出这些地方,在调用一个能抛出错误的函数、方法或者构造器之前,加上 try 关键字,或者 try?try! 这种变体。

和其他语言中的异常处理不同的是,Swift 中的错误处理并不涉及解除调用栈,这是一个计算代价高昂的过程。就此而言,throw 语句的性能特性是可以和 return 语句媲美的。

用 throwing 函数传递错误

为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数列表后面加上 throws 关键字。一个标有 throws 关键字的函数被称作 throwing 函数。

一个 throwing 函数可以在其 nebula 抛出错误,并将错误传递到函数被调用时的作用域。

用 Do-Catch 处理错误

可以使用一个 do-catch 语句运行一段闭包代码来处理错误。如果在 do 子句中的代码抛出了一个错误,这个错误会与 catch 子句做匹配,从而决定哪条子句能处理它。

将错误转换成可选值

可以使用 try? 通过将错误转换成一个可选值来处理错误。如果在评估 try? 表达式时一个错误被抛出,那么表达式的值就是 nil

禁用错误传递

有时候你知道某个 throwing 函数实际上在运行的时候是不会抛出错误的,在这种情况下,你可以在表达式前面写 try! 来禁用错误传递,这会把调用包装在一个断言不会有错误抛出的运行时断言中。如果实际上抛出了错误,你会得到一个运行时错误。

指定清理操作

可以使用 defer 语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,还是由于诸如 return 或者 break 的语句。

例如,你可以用 defer 语句来确保文件描述符得以关闭,以及手动分配的内存得以释放。

defer 语句将代码的执行延迟到当前的作用域退出之前。该语句由 defer 关键字和要被延迟执行的语句组成。延迟执行的语句不能包含任何控制转移语句,例如 break 或是 return 语句,或是抛出一个错误。

延迟执行的操作会按照它们被指定时的顺序的相反顺序执行。

Swift 学习笔记:泛型

泛型代码让你能够根据自定义的需求,编写出使用与任意类型、灵活可重用的函数及类型。它能让你避免代码的重复,用一种清晰和抽闲的方式来表达代码的意图。

类型约束

类型约束可以指定一个类型参数必须继承指定类,或者符合一个特定的协议或协议组合。

当你创建自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。抽象概念,例如可哈希的,描述的是类型在概念上的特征,而不是它们的显式类型。

类型约束语法

你可以在一个类型参数名后面防止一个类名或者协议名,并用冒号进行分隔,来定义类型约束,它们将成为类型参数列表的一部分。对泛型函数添加类型约束的基本语法如下所示(作用于泛型类型时的语法与之相同):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

上面这个函数有两个类型参数。第一个类型参数 T,有一个要求是 T 必须是 SomeClass 子类的类型约束,同理,第二个必须符合 SomeProtocol 协议的类型约束。

关联类型

定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占3位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。你可以通过 associatedtype 关键字来指定关联类型。

Swift 学习笔记:协议

协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。

协议语法

拥有父类的类在采纳协议时,应该将父类名放在协议名之前,以逗号分隔。

属性要求

协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。此外,协议还指定属性是可读还是可读可写的。

如果要求属性是可读可写的,那么该属性不能是常量属性或只读的计算属性;如果协议要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

协议总是用 var 关键字来声明变量属性,在类型后面加上 {set get} 来表示属性是可读可写的,可读属性则用 { get } 来表示。

在协议中定义类型属性时,总是使用 static 关键字作为前缀。

方法要求

在协议中定义类方法的时候,总是使用 static 关键字作为前缀。

Mutating 方法要求

有时需要在方法中改变方法所属的实例。将 mutating 关键字作为方法的前缀,写在 func 关键字之前,表示可以在该方法中修改它所属性的实例以及实例的任意属性的值。

实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。

构造器要求

协议可以要求采纳协议的类型实现制定的构造器。你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但是不需要写花括号和构造器实体。

构造器要求在类中的实现

你可以在采纳协议的类中实现构造器,无论是作为制定构造器,还是作为便利构造器,你都必须为构造器实现表上 required 修饰符。

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 requiredoverride 修饰符。

可失败构造器要求

协议作为类型

尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中得元素类型

委托(代理)模式

委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。

委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保采纳协议的类型能提供这些类型。委托模式可以用来响应特定的动作,或者接受外部数据源提供的数据,而无需关心外部数据源的类型。

通过扩展添加协议一致性

即便无法修改源代码,依然可以通过扩展令已有类型采纳并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。

通过扩展采纳并符合协议,和在原始定义中采纳并符合协议的效果完全相同。

通过扩展采纳协议

当一个类型已经符合了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空扩展体的扩展来采纳该协议。

协议类型的集合

协议类型可以在数组或者字典这样的集合中使用。

协议的继承

协议能够继承一个或者多个其他协议,可以在继承的协议的基础上正价新的要求。

类类型专属协议

你可以在洗衣的继承列表中,通过添加 class 关键字来限制协议只能被类类型采纳,而结构体或枚举不能采纳该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前。

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 这里是类类型专属协议的定义部分
}

协议合成

有时候需要同时采纳多个协议,你可以将多个协议采用 protocol<SomeProtocol, AnotherProtocol> 这样的格式进行组合,称为协议合成(protocol composition)。你可以在<>张罗列任意多个你想要采纳的协议,以逗号分隔。

协议合成并不会生成新的、永久的协议类型,而是将多个协议中的要求合成到一个旨在局部作用域中的有效临时协议中。

检查协议一致性

你可以使用类型转换中描述的 isas 操作符来检查协议一致性,即是否符合某协议,并且可以转换到制定的协议类型。

  • is 用来检查视力是否符合某个协议,符合返回 true
  • as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,会引发运行时错误。

可选的协议要求

协议可以定义可选要求,采纳协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

可选的协议要求只能用在标记 @objc 特性的协议中。该特性表示协议将暴露给 Objective-C 代码。即使你不打算和 OC 有什么交互,如果你想要指定可选的协议要求,都那么还是要为协议加上 @obj 特性。

协议扩展

协议可以通过扩展来为采纳协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需再每个采纳协议的类型中都重复同样的实现,也无需使用全局函数。

提供默认实现

可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果采纳协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中默认实现被使用。

为协议扩展添加限制条件

在扩展协议的时候,可以指定一些限制条件,只有采纳协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述。