HTTP缓存

缓存是某个资源(或文档)的副本。对于私有缓存,可以服务同一个客户端的对同一个资源多次的请求,比如浏览器缓存。对于共享缓存(或代理缓存),可以服务多个客户端对于同一个资源的请求,比如CDN。为什么需要缓存,缓存有哪些优点:

  • 减少网络延迟,更快的加载内容,提高用户体验
  • 减少冗余数据传输,降低网络负载
  • 降低服务器负载,服务器可以更快的处理请求

不同类型的缓存

  • 私有缓存 - 服务于单个用户
  • 共享缓存 -存储响应被很多用户复用

其他缓存,gateway cache, CDN, reverse proxy cache 和 部署在服务器上的负载均衡器,以获得更好的可靠性和性能.

HTTP Cache TypeHTTP Cache Type

私有浏览器缓存(Private browser cache)

私有缓存服务于单个用户.

共享代理缓存(Shared proxy caches)

共享缓存服务于多个用户.比如ISP或者企业可以配置一个web代理,作为本地网络基础架构,服务于多个用户.热点资源可以被多个用户复用,减少网络流量和延迟. 

常用的缓存条目的形式:

  • 成功的检索请求结果 - 200 OK响应,比如HTML 文档,图片或者文件
  • 永久重定向 - 301(Moved Permanently)响应
  • 错误响应 - 404(Not Found)结果页
  • 未完成的结果 - 206(Partial Content)响应
  • 不仅仅是GET请求的响应 - 定义一个合适的cache key

缓存处理步骤

对于一个HTTP GET请求报文,基本缓存处理包含7个步骤:

  1. 接收-缓存从网络中读取请求报文
  2. 解析-缓存对报文进行解析,提取HTTP首部信息
  3. 查询-缓存查询是否命中,如果没有,则从源服务器获取,并缓存到本地
  4. 新鲜度检测-检查副本是否新鲜,如果不新鲜,则与源服务器进行验证
  5. 响应-缓存用新的首部和缓存主体创建响应报文
  6. 发送-缓存通过网络将响应发送给客户端
  7. 日志-缓存记录这次请求的日志

缓存处理流程图:
HTTP_CACHE_GET_FLOW_CHARTHTTP_CACHE_GET_FLOW_CHART

缓存控制

服务器可以通过HTTP定义文档过期之前可以将其缓存多长时间。

  • Cache-Control: no-store
  • Cache-Control: no-cache
  • Cache-Control: must-revalidate
  • Cache-Control: max-age
  • Expires

no-store与no-cache

HTTP/1.1提供了几种限制对象缓存方式。 no-store和no-cache首部可以防止缓存未经证实的已缓存对象:

1
2
3
Pragma: no-cache
Cache-Control: no-store
Cache-Control: no-cache

标识为no-store的响应回禁止缓存对响应进行复制。标识为no-cache的响应可以在本地缓存中,只是在与服务器进行新鲜度再验证之前,缓存不能提供给客户端使用。

Cache-Control: max-age=3600表示收到服务器响应文档处于新鲜状态的秒数。max-age=0表示不缓存文档。

Expires: Fri, 05 Jul 2017, 12:00:00 GMT表示文档绝对过期时间。不推荐使用Expires,HTTP设计者后来任务,由于服务器之间的时间不同步或不正确,会导致文档新鲜度计算错误。

如果源服务器希望缓存严格遵守过期时间,可以在加Cache-Control: must-revalidate的HTTP首部。Cache-Control: must-revalidate响应告诉缓存,在事先没有跟源服务器再验证之前,不能提供这个对象的过期副本。缓存仍然可以提供新鲜的副本。如果缓存进行must-revalidate新鲜度是,源服务器不可用,缓存必须返回一条Gateway Timeout从错误。

缓存命中

缓存命中、未命中和再验证:
HTTP Cache HitHTTP Cache Hit

缓存命中率

由缓存缓存提供服务的请求所占的比例成为称为缓存命中率(cache hit rate)。命中率在0到1之间,0表示缓存全部未命中,1表示缓存全部命中。缓存服务提供者希望缓存的命中是100%,而实际的缓存命中率与缓存大小,缓存内容变化,请求者兴趣相似度等因素相关。

缓存新鲜度

文档过期

就像牛奶过期一样,文档也有过期时间。

1
2
3
HTTP/1.1 200 OK
Content-Type: text/plain
Cache-Control: max-age=484200
1
2
3
HTTP/1.1 200 OK
Content-Type: text/plain
Expires: Fri, 05 Jul 2017, 12:00:00 GMT

Cache-Control: max-age=484200是一个相对过期时间,max-age定义了文档的最大使用期,从第一次生成文档到文档不再新鲜为止,以秒为单位。

Expires:Fri, 05 Jul 2017, 12:00:00 GMT是一个绝对过期时间,如果过期时间已经过了,则文档不再新鲜。该首部要时钟同步

服务器再验证

缓存文档过期并不意味着该副本与服务器文档不一致,只是意味着要与源服务器进行再验证。

  • 如果再验证内容发生了变化,缓存获取新的副本,替换过期副本
  • 如果再验证内容没有发生变化,缓存只需要获取新的首部,对缓存的副本的首部进行更新

条件再验证HTTP首部:

Header Description
If-Modify-Since: 如果从指定日期之后文档被修改过了,就执行该请求。可以与Last-Modified服务器响应首部配合使用,只有在内容被修改后,才去获取新的内容
If-None-Match: 服务器可以为文档提供特殊的标签,而不是将其与最近修改时间相匹配,这些标签就像序列号一样

If-Modify-Since: Date再验证:

  • 如果自指定日期后,文档被修改了,If-Modify-Since条件为真,源服务器会返回成功的响应,包含新的过期首部和新文档实体
  • 如果自指定日期后,文档未被修改,If-Modify-Since条件为假,源服务器会返回一个304 Not Modified的响应,不包含文档实体内容

img

If-None-Match: Tags

有些情况下,仅使用最后修改时间是不够的。

  • 文档被周期性的重写,最后修改时间发生变化,而内容未改变
  • 服务器无法判定最后修改时间

HTTP允许用户对实体打标签,进行标识。

img

什么时候使用最后修改时间和标签验证?

如果服务器返回了ETag首部,客户端必须使用标签验证。如果服务器只返回了Last-Modified首部,客户端可以使用最后修改时间验证。

新鲜度计算算法

为了分辨文档是否新鲜,需要计算两个值,文档的使用期(age)和文档的新鲜生存期(freshness lifetime)。如果文档使用期小于文档新鲜生存期,则文档是新鲜的。

1
is_fresh_enough = True if age < freshness_lifetime

使用期

使用期包含了网络传输时间和文档在缓存的停留时间。

1
2
3
4
5
6
7
apparent_time = time_got_response - date_header_value
corrent_apparent_time = max(0, apparent_time)
age_when_document_arrived_at_our_cache = corrent_apparent_time

how_long_copy_has_been_in_our_cache = current_time - got_response_time

age = age_when_document_arrived_at_our_cache + how_long_copy_has_been_in_our_cache

基于Date首部计算apparent使用期

apparen时间:

apparent时间等于获得响应时间减去服务器发送文档时间:

1
apparent_time = time_got_response - date_header_value

为了防止由于服务器时间不同步导致apparent_time为负,进行时间修正:

1
2
apparent_time = max(0, apparent_time)
age_when_document_arrived_at_our_cache = apparent_time

对网络时延对补偿:

如果文档在网络或服务器中阻塞了很长时间,相对使用期的计算可能会极大的低估文档使用期。缓存知道文档请求时间,以及文档到达时间。HTTP/1.1会在这些网络延迟上加上整个网络时延,一遍对其进行保守校正。这个从缓存到服务器到缓存高估了服务器到缓存延迟,它是保守的。如果出错,只会使文档看起来比实际使用期要老,并会引发不必要的验证。

1
2
response_delay_estimate = time_got_response - time_issued_request
age_when_document_arrived_at_our_cache = apparent_time + response_delay_estimate

Note:该时延补偿会导致最后计算文档使用期大于实际的文档使用期。apparent_time是包含网络时延的,对网络时延补偿是否必要?在服务器负载较高,对服务器的处理时间进行补偿倒是很有必要。

缓存停留时间:

1
how_long_copy_has_been_in_our_cache = current_time - got_response_time

img

新鲜生存期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def calculate_server_freshness_limit(**kwargs):
heuristic = False
server_freshness_limit = default_cache_min_lifetime
if max_age_value_set:
server_freshness_limit = max_age_value_set
elif expires_value_set:
server_freshness_limit = expires_value_set - date_value
elif last_modified_value_set:
time_since_last_modify = max(0, date_value - last_modified_value)
server_freshness_limit = int(time_since_last_modify*lm_factor)
heuristic = True
else:
server_freshness_limit = default_cache_min_lifetime
heuristic = True

if heuristic:
if server_freshness_limit > default_cache_max_lifetime:
server_freshness_limit = default_cache_max_lifetime
if server_freshness_limit < default_cache_min_lifetime:
server_freshness_limit = default_cache_min_lifetime

return server_freshness_limit

LM-factor算法计算新鲜周期

  • 如果已缓存文档最后一次修改发生在很久以前,它可能是一份稳定的文档,不会突然发生变化,因此将其汲取保存在缓存中比较安全
  • 如果已缓存的文档最近被修改过,就说明它很可能会频繁发生变化,因此在与服务器再验证之前,只应该将其缓存很短一段时间
1
2
time_since_modify = max(0, date_value - server_last_modified)
server_freshness_limit = time_since_modify * lm_factor

img

其他

缓存相关的HTTP头部:

Header Description
Cache-Control 缓存控制
Expires 过期绝对时间
If-Modify-Since 从某个时间开始文档是否发生改变
If-None-Match 文档的标签是否发生改变
Last-Modified 最后修改时间
ETag 文档标签

参考

OAuth

OAuth客户端使用一个访问令牌(access token)来访问受保护的资源,其中访问令牌包含了特殊作用域(specific scope), 生命周期,和其他的访问属性。访问令牌由授权服务器在资源拥有者授权之后颁发(issue)。客户端通过访问令牌资源服务器上受保护资源。

比如,一个终端用户(resource-owner)可以授权打印服务器(client)访问他的存储在图片分享服务(resource server)上的受保护的图片,但不用与打印服务器分享他的用户名和密码。他可以授权图片分享服务(authorization server)颁发一个特殊的证书(access token).

Introduction

Roles

Name Description
resource owner 一个能授权访问受保护资源的实体
resource server 托管受保护资源的服务器,能处理通过访问令牌访问受保护资源的请求
client 代表资源拥有者发起访问受保护资源的请求的应用程序
authorization server 在资源拥有者认证、授权之后,能够颁发访问令牌给客户端的服务器

Protocol Flow

Authorization Grant

授权grant是一个证书表示资源拥有者已经授权,客户端可以用授权grant获取访问令牌。该规范定义4种授权类型:

  • authorization code
  • implicit
  • resource owner password credentials
  • client credentials

Access Token

访问令牌是一种证书用来访问受保护的资源。一个访问令牌就是认证服务器颁发给客户端的字符串。令牌包含了特殊作用域,过期时间,授权的资源拥有者等信息。

Refresh Token

刷新令牌也是一个证书用来获取访问令牌。刷新令牌是认证服务器发放给客户端的,在当访问令牌不可用或过期时,用来获取新的访问令牌。

刷新令牌是资源资源拥有者授权给客户端认证码。与访问令牌不同,刷新令牌只能与认证服务器通信,不能从资源服务器获取资源。

Refreshing an Expired Access TokenRefreshing an Expired Access Token

Client Registration

在使用OAuth协议之前,客户端需要在认证服务器注册应用。

客户端注册是让认证服务器能够识别、信任客户端,其中包括客户端类型,重定向URI等。

Client Types

OAuth定义了两种客户端类型

  • confidential - 客户端能够管理证书的机密性
  • public - 客户端不能管理证书的机密性

Client Identifier

认证服务器给已注册的客户端颁发一个客户端标识。对于认证服务器,客户端标识是唯一的。

Client Authentication

如果客户端类型是confidential, 客户端和认证服务器建立一个客户端认证方法。Confidential客户端会向认证服务器声明它支持证书,比如密码或公私钥对。

Obtaining Authorization

在请求访问令牌之前,客户端先要获取资源拥有者的授权。OAuth定义了4种授权类型: authorization code, implicit, resource owner password credential, 和client credentials.

Authorization Code Grant

授权码授权类型用来获取访问令牌和刷新令牌。

认证码获取流程:

Authorization Request

Content-Type: application/x-www-form-urlencoded

Parameter Description
response_type REQUIRED该值必须设置为”code”
client_id REQUIRED注册的应用的id
redirect_uri OPTIONAL重定向URI,成功获取授权码后重定向到oauth客户端的URI
scope OPTIONAL作用域
state RECOMMENDED客户端用来管理请求、回调状态的值。认证服务器的重定向到UA时,会包含这个值。这个参数应该用来防止扩展请求伪造
1
2
3
4
5
6
7
8
GET /o/authorize/?response_type=code&client_id=wt6Pvm2s3vbb8RPE7nlPlugwaMnj58UhFpk8bCPp&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth%2Fauthorized%2F HTTP/1.0
Host: localhost:8000
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8

认证服务器会验证这个请求,保证请求中的所有参数都是有效的。如果请求是有效的,认证服务器需要资源拥有者授权。如果资源拥有者授权成功,授权服务器根据redirect_uri返回给UA一个重定向响应。

Authorization Response

Content-Type: application/x-www-form-urlencoded

Parameter Description
code REQUIRED认证服务器生成的授权码,为了防止泄露的危险,授权码必须在很短的时间过期。授权码生存期推荐为10min.客户端只能使用一次授权码。授权码与客户端id,重定向URI绑定
state REQUIRED如果客户端请求中包含state,授权服务器必须要返回该值
1
2
3
4
5
6
7
8
HTTP/1.0 302 Found
Date: Tue, 18 Jul 2017 04:12:00 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Location: http://localhost:8000/oauth/authorized/?code=x1yxsYhAJ23pXNMXej4tB13dvMFTov
Vary: Cookie

Error Response

Conent-Type: application/x-www-form-urlencoded

Parameter Description
error REQUIRED
error_description OPTIONAL
error_uri OPTIONAL
state REQUIRED
1
2
HTTP/1.1 302 Found
Location: https://client.example.com/cb?error=access_denied&state=xyz

Access Token Request

Conent-Type: application/x-www-form-urlencoded

Parameter Description
grant_type REQUIRED该值必须设置“authorization_code”
code REQUIRED授权服务器返回的code
redirect_uri REQUIRED重定向URI
client_id REQUIRED注册应用的ID
1
2
3
4
5
6
7
8
9
10
POST /o/token/ HTTP/1.0
Host: localhost:8000
Connection: close
Content-Length: 183
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.1
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=x1yxsYhAJ23pXNMXej4tB13dvMFTov&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth%2Fauthorized%2F&client_id=wt6Pvm2s3vbb8RPE7nlPlugwaMnj58UhFpk8bCPp

认证服务器必须要验证客户端的请求

  • 对于confidential客户端,需要客户端认证
  • 如果客户端包含认证信息,认证客户端
  • 验证授权码是否有效
  • 验证redirect_uri是否证券

Access Token Response

如果获取访问令牌的请求是有效的而且认证通过,认证服务器需要发放访问令牌和可选的刷新令牌。如果请求是无效的或认证失败,需要返回一个错误响应。

1
2
3
4
5
6
7
8
9
HTTP/1.0 200 OK
Date: Tue, 18 Jul 2017 04:12:00 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Pragma: no-cache
Cache-Control: no-store

{"access_token": "4C1gRp2TfYKN0tO45fqa6BkFZxTJNU", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "oZX6XtV2OGSVRnvyOoa7qerWddOPKw", "scope": "read write"}

Refreshing an Access Token

如果认证服务器向客户端颁发了刷新令牌,客户端可以使用刷新令牌更新访问令牌。

Content-Type: application/x-www-form-urlencoded

Parameter Description
grant_type REQUIRED该值必须设置为”refresh_token”
refresh_token REQUIRED认证服务器颁发的刷新令牌
scope OPTIONAL

刷新令牌是长期存在证书用来更新访问令牌,刷新令牌必须要与客户端id绑定。此外,客户端必须向认证服务器提供认证信息。

1
2
3
4
5
6
7
8
9
10
11
POST /o/token/ HTTP/1.0
Host: localhost:8000
Connection: close
Content-Length: 69
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic d3Q2UHZtMnMzdmJiOFJQRTdubFBsdWd3YU1uajU4VWhGcGs4YkNQcDpWcUF3NDlwb3VXc0lUVU1OcWFrdzczQ0NSRVZzclMyRWd5SG9WeVJnekpWNWRlWnNKQUNBeVdkeEFJMXZRc2RaZk1JQmlCRlRFeUR5YUkyeTJJcU1vR3JEbkFNWE5ITWd4TFRJUDIxRXhIR0tkUmNTVlJ5RjRxOHA5STR5OWhqVQ==

grant_type=refresh_token&refresh_token=oZX6XtV2OGSVRnvyOoa7qerWddOPKw

Response:

1
2
3
4
5
6
7
8
9
HTTP/1.0 200 OK
Date: Tue, 18 Jul 2017 06:47:50 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Pragma: no-cache
Cache-Control: no-store

{"access_token": "gebkQX71vIwmksTsvleRKMzpdysLHY", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "CbpaNXAqyexTQOuCvQf1RUoEN2EZeX", "scope": "read write"}

Accessing Protected Resources

客户端可以同访问访问资源服务器受保护的资源。资源服务器必须要验证访问令牌,保证访问令牌没有过期,并且其作用域能覆盖此资源。

Access Token Types

“bearer”令牌类型,参考RFC6750

1
2
3
GET /resource/1 HTTP/1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM

“mac”令牌类型

1
2
3
4
5
GET /resource/1 HTTP/1.1
Host: example.com
Authorization: MAC id="h480djs93hd8",
nonce="274312:dj83hs9s",
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="

References

Python元类

[Metaclasses] are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why). The Python core developer Time Peters said.

元类是创建类的类。新风格(new-style)类是type类的实例。元类是type类的派生类,通过重载type类的__new____init__方法,重新定义类创建协议,来实现类创建的定制化。

元类模型

(对象)实例是通过类创建,类是通过type类创建,元类是type类的派生类。

  • 类型(自定义)是通过type类或type派生元类创建
  • 元类是type类的派生类
  • 用户自定义类是type类的实例
  • 用户自定义类可以生成自己的实例

类声明协议

Python解释器在类声明语句结束时,调用type类型创建,class = type(classname, superclasses, attributedict)

1
2
type.__new__(meta, classname, superclasses, attributedict)
type.__init__(cls, classname, superclasses, attributedict)

声明元类

Py3与Py2声明元类的方式不一样:

Py3声明元类

1
2
3
4
5
6
7
8
9
>>> class Metaclass(type):
... def __new__(meta, classname, superclasses, attributedict):
... return type(classname, superclasses, attributedict)
... def __init__(cls, classname, superclasses, attributedict):
... pass
...
>>> class Dummy(object, metaclass=Metaclass):
... pass
...

Py2声明元类

1
2
3
4
5
6
7
8
9
>>> class Metaclass(type):
... def __new__(meta, classname, superclasses, attributedict):
... return type(classname, superclasses, attributedict)
... def __init__(cls, classname, superclasses, attributedict):
... pass
...
>>> class Dummy(object):
... __metaclass__ = Metaclass
...

继承和实例

  • 元类继承于type类
  • 元类的声明可以被派生类继承
  • 元类的属性不能被类的实例继承
  • 元类的属性可以被类获取

元类继承于type类

元类重载type类的__new____init__方法,定制类的创建和初始化。

元类的声明可以被派生类继承

元类的属性不能被类实例继承

类是元类的实例,元类的行为可以被类访问,当类不能被类的实例访问。

元类的属性可以被类获取

继承

Python继承算法

  1. 对于一个实例,先搜索这个实例,再搜索实例的类,再搜索超类
    a. 先搜索实例的__dict__
    b. 再搜索该实例的类的__mro__对应类的__dict__
  2. 对于一个类,先搜索类,再搜索超类,再搜索元类
    a. 根据__mro__搜索类的__dict__
    b. 再搜索元类的__dict__
  3. 规则1和2中,再b阶段中,数据描述的优先级高
  4. 规则1和2中,对于内置的运算符,跳过a阶段

数据描述符继承算法

对于定义了__set__拦截赋值的描述符就是数据描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> class D(object):
... def __get__(self, ins, _type):
... print('call D.__get__')
... def __set__(self, ins, value):
... print('call D.__set__')
...
>>>
>>> class Dummy(object):
... d = D()
...
>>> ins = Dummy()
>>> ins.d
call D.__get__
>>> ins.d = 'spam'
call D.__set__

未定义__set__的描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class D(object):
... def __get__(self, ins, value):
... print('call D.__get__')
...
>>> class Dummy(object):
... d = D()
...
>>> ins = Dummy()
>>> ins.d
call D.__get__
>>> ins.d = 'spam'
>>> ins.d
'spam

Python 的继承算法

  1. 对于实例I,先搜索实例,再搜索类,再搜索超类
    a. 根据类的__mro__搜索超类的__dict__
    b. If 如果在a阶段发现了数据描述,调用该数据描述,完成后退出
    c. Else 返回该实例__dict__中的值
    d. Else 调用非数据描述符,并放回结果
  2. 对于类C,搜索类,再搜索超类,再搜索元类
    a. 搜索类的__mro__依次搜索类的__dict__
    b. If 如果在a阶段发现了数据描述符,调用该数据描述符,完成后退出
    c. Else 返回该类__dict__中值g
    d. Else 调用非数据描述符,返回结果

Note, 数据描述符的优先级 > 普通属性 > 非数据描述符

元类与类装饰器

TODO

示例

django ORM

ripozo API

参考

  • Learning Python 5th Edition

Python描述符

如果一个对象定义了以下任意方法,这个对象就是一个描述符。给描述符下个定义,描述符就是绑定了行为属性的对象。

object.__get__(self, instance, owner)

object.__set__(self, instance, value)

object.__delete__(self, instance)

属性访问的默认行为就是从一个对象字典中获取、设置和删除属性。比如,a.x首先会搜索a.__dict__['x'],其次type(a).__dict__['x'],最后所有type(a)的元类。如果要查找的值一个包含描述器方法的对象,Python会用调用描述器方法代替默认行为。

Note:只有new-style class会调用描述符的对象的方法。

描述符是一个强大的通用协议。Python内建的property, staticmethod, classmethod, super背后的实现机制都是描述符协议。

Descriptor Protocol

object.__get__(self, ins, _type=None)

object.__set__(self, ins, value)

object.__del__(self, ins)

如果一个对象包含上面任意一个方法,就可以被看作是一个描述符。如果一个对象定义了__get____set__两个方法,该对象可以被看作一个数据描述符,如果一个对象只定义了__get__,该对象就是non-data描述符。

数据描述符与非数据描述符的区别在于,描述符与对象实例entry调用优先级。如果一个实例的字典有一个entry和数据描述符的名字相同,数据描述符的调用的优先级高。如果一个实例有一个entry和非数据描述符的名字相同个,实例entry的调用的优先级高。

Invoking Descriptors

obj.d查找obj的字典是否包含d,如果d定义了__get__方法,d.__get__(obj, type(obj))就会被调用。

1
2
3
4
5
6
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v

super()返回的对象有一个定制化的__getattribute__方法,用于调用描述符。super(B, self).m先会搜索obj.__class__.__mro__查找基类A,如果是一个数据描述符,则会调用A.__dict__['m'].__get__(obj, B),如果是一个非数据描述符,返回结果不会改变。

Built-in Descriptors

Property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class C(object):
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")


class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)

def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)

def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

Staticmethod

1
2
3
4
5
6
7
8
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"

def __init__(self, f):
self.f = f

def __get__(self, obj, objtype=None):
return self.f

Classmethod

1
2
3
4
5
6
7
8
9
10
11
12
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"

def __init__(self, f):
self.f = f

def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc

References

Python上下文管理器

上下管理器是一个对象,定义了执行with语句时需要创建的上下文。context manager的__enter__()__exit__()方法分别在进入、退出with语句时被调用。

object.__enter__(self)

object.__exit__(self, exc_type, exc_value, traceback)

with statement

1
2
with_stmt ::=  "with" with_item ("," with_item)* ":" suite
with_item ::= expression ["as" target]

with语句执行数据流:

  1. 评估上下文表达式是否包含上下文管理器
  2. 加载上下文管理器的__exit__方法
  3. 调用上下文管理的__enter__方法
  4. 如果target包含在with语句中,将__enter__方法的返回值赋给target
  5. 执行suite
  6. 调用__exit__()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)

contextlib

contextlib提供了快速定义支持上下文管理器的函数对象。

定义一个生成器函数,contextmanager装饰后就变成一个支持上下文管理器的函数对象。yield之前语句子在代码块之前被执行,yield之后语句在代码执行完之后被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from contextlib import contextmanager
>>>
>>> @contextmanager
... def tag(name):
... print('<%s>' % name)
... yield
... print('</%s>' % name)
...
>>> with tag('h1'):
... print('hotbaby')
...
<h1>
hotbaby
</h1>

context decorator

contextmanager是一个函数装饰器,装饰只包含一个yield语句的生成器函数,返回一个支持上下文管理器的函数对象。

1
2
3
4
5
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper

Note: 被装饰的生成器函数变成生成器作为参数传递到GeneratorContextManager对象中。

生成器上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class GeneratorContextManager(object):
"""Helper for @contextmanager decorator."""

def __init__(self, gen):
self.gen = gen

def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")

def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
# Need to force instantiation so we can reliably
# tell if we get the same exception back
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration, exc:
# Suppress the exception *unless* it's the same exception that
# was passed to throw(). This prevents a StopIteration
# raised inside the "with" statement from being suppressed
return exc is not value
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
if sys.exc_info()[1] is not value:
raise

references

Python Itertools

itertools.imap()

imap(func, *iterables) 和iter和map混合体。

工作原理

1
2
3
4
5
6
7
8
9
>>> def myimap(func, *iterables):
iterables = map(iter,iterables)
while True:
args = [next(it) for it in iterables]
yield func(*args)


>>> [item for item in myimap(pow, (2,2), (3,3))]
[8, 8]

itertools.chain()

chain(*iterables) 返回一个迭代器,该迭代器依次返回可迭代对象中没一个元素。

工作原理

1
2
3
4
5
6
7
>>> def mychain(*iterables):
for iter_ in iterables:
for item in iter_:
yield item

>>> [item for item in mychain('abc', 'def')]
['a', 'b', 'c', 'd', 'e', 'f']

References

Python PyPI

PyPI(Python Package Index)是Python软件仓库。

pip是Python包管理工具,默认使用pypi.python.org作为PyPI的镜像。

pip安装Python软件包经常会出现”链接pypi.python.org失败”。为了优化包的管理,考虑替换pypi.python.org,转而使用国内的PyPI镜像。

PyPI mirror list

Mirror Location
pypi.python.org San Francisco, California US
pypi.douban.com Beijing, Beijing CN
pypi.fcio.net Oberhausen, Nordrhein-Westfalen DE

Linux(Debian) 替换PyPI镜像

touch ~/.pip/pip.conf

1
2
3
[global]
index-url = https://pypi.douban.com/simple
format = columns

参考

Lisp

Lisp语言从诞生的时候就包含9种思想.其中一些我们今天已经习以为常,另一些则刚刚在其他高级语言中出现,至今还有2种是Lisp独有的.按照大众的接受程度,这9种思想依次如下排列:

  1. 条件结构.现在大家都觉得这是理所当然的,但是Fortran I就没有这个结构,它只有底层机器的goto结构.
  2. 函数也是一种数据类型.在Lisp语言中,函数与整数或字符串一样,也属于数据类型的一种.它有自己的字面表示形式(literal representation),能狗存储在变量中,也能当作参数传递.一种数据类型应该有的功能,它都有.
  3. 递归.Lisp是第一个支持递归函数的高级语言.
  4. 变量的动态类型.在Lisp语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有.复制变量就是相当于复制指针,而不是复制它们指向的数据.
  5. 垃圾回收机制.
  6. 程序由表达式组成.Lisp程序是一些表达树的集合,每个表达式都返回一个值.这与Fortran和大多数后来的语言都截然不同,他们的程序都由表达式和语句组成.区分表达式与语句在Fortran I中是自然的,因为它不支持语句嵌套.所以,如果你需要用数学式子计算一个值,那就只有表达式返回这个值,没有其他语法结构可用,否则就无法处理这个值.后来,新的编程语言支持块结构,这种限制当然就不存在了.但是为时已晚,表达式和语句的区分已经根深蒂固.它从Fortran扩散到它们两者的后继语言.
  7. 符号类型.符号实际上是一种指针,指向存储在散列表中字符串.所以,比较两个符号是否相等,只要看它们的指针是否一样就可以了,不用逐个字符比较.
  8. 代码使用符号和常量组成的树形表示法.
  9. 无论什么时候,整个语言都是可用的.Lisp并不真正区分读取,编译期和运行期.你可以在读取期编译或运行代码,也可以在编译期读取和运行代码,还可以在运行期读取或编译代码.在读取期运行代码,使得用户可以重新调整Lisp的语法,在编译期运行代码,则是Lisp宏的工作基础,在运行期编译代码,使得Lisp可以在Emacs这样的程序中充当扩展语言(extension language),在运行期读取代码,使得程序之间可以用S表达式通信,近来XML格式的出现使得这个概念被重新”发明”出来了.

Lisp语言刚出现的时候,这些思想与其他编程语言大相径庭,后者的设计思想主要由50年代后期的硬件决定.随着时间流逝,流行的编程语言不断更新换代,语言设计思想逐渐向Lisp靠拢.思想(1)到思想(5)已经被广泛接受,思想(6)开始在主流编程语言中出现,思想(7)在Python语言中有所实现,不过似乎没有专用的语法.

思想(8)可能是最有意思的一点.它与思想(9)只是由于偶然的原因成为Lisp语言的一部分,因为它们不属于麦卡锡的原始构想,是由拉塞尔自行添加的.它们从此使得Lisp语言看上去很古怪,但也成为了这种语言最独一无二的特点.说Lisp语法古怪不是因为它的语法很古怪,而是因为它根本就没有语法,程序直接以解析树(parse tree)的形式表达出来.在其他语言中,这种形式只是经过解析在后台产生,但是Lisp直接采用它作为表达式形式.它由列表构成,而列表则是Lisp的基本数据结构.

用一种语言自己的数据结构来表达该语言是非常强大的功能.思想(8)和思想(9),意味着你可以写出一种能够自己编程的程序.

Django数据库路由

django ORM数据模型配置数据库.

django支持多个数据库,通过django ORM定义数据模型,比如class User(Model),无法通过class Meta配置管理该数据模型对应的数据库,只能使用默认数据库default

django ConnectionRouter解决数据模型与数据库映射.

实现DB router

db_router.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import logging
from django.conf import settings

_logger = logging.getLogger('django')

class DatabaseRouter(object):
"""
Database router to control the models for differrent db.
"""

DEFAULT_DB = 'default'

def _db(self, model, **hints):
db = getattr(model, '_database', None)
if not db:
return self.DEFAULT_DB

if db in settings.DATABASES.keys():
return db
else:
_logger.warn('%s not exist' % db)
return self.DEFAULT_DB

def db_for_read(self, model, **hints):
return self._db(model, **hints)

def db_for_write(self, model, **hints):
return self._db(model, **hints)

配置DB routers

DATABASE_ROUTERS = ['db_router.DatabaseRouter']

为Model制定数据库

1
2
3
4
5
class User(Model)
_database = 'user_db'
class Meta:
db_table = 'user'
...

实现原理

django通过ConnectionRouter管理数据库路由

django/db/utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ConnectionRouter(object):

@cached_property
def routers(self):
if self._routers is None:
self._routers = settings.DATABASE_ROUTERS
routers = []
for r in self._routers:
if isinstance(r, six.string_types):
router = import_string(r)()
else:
router = r
routers.append(router)
return routers

def _router_func(action):
def _route_db(self, model, **hints):
chosen_db = None
for router in self.routers:
try:
method = getattr(router, action)
except AttributeError:
# If the router doesn't have a method, skip to the next one.
pass
else:
chosen_db = method(model, **hints)
if chosen_db:
return chosen_db
instance = hints.get('instance')
if instance is not None and instance._state.db:
return instance._state.db
return DEFAULT_DB_ALIAS
return _route_db

db_for_read = _router_func('db_for_read')
db_for_write = _router_func('db_for_write')

router初始化

router = ConnectionRouter()

router引用

django/db/models/query.py

1
2
3
4
5
6
7
8
class QuerySet(object):

@property
def db(self):
"Return the database that will be used if this query is executed now"
if self._for_write:
return self._db or router.db_for_write(self.model, **self._hints)
return self._db or router.db_for_read(self.model, **self._hints)