Kaito's Blog

致力成为一枚silver bullet.

0%

如果你想对计算机的组成和系统结构有个系统的了解,推荐你看一本书,叫做《深入理解计算机系统》。这本书也是计算机界的经典书籍,如果能吃透这本书里的内容,你会对计算机系统和软件开发有一个全新的认识。

由于这本书不太容易学习,有很多程序员买了这本书,但可能读了一两个章节发现太过枯燥理解不了,便放下珍藏了。

我和你一样,每次拿起这本书都刻意让自己学下去,但都败下阵来。

后来在知乎发现有人指导如何阅读这本书时,有网友推荐了国人写的教材,与《深入理解计算机系统》内容非常类似,但更易读。

这本书的作者叫袁春风,她是南京大学计算机系的教授,写过2本书《计算机组成与系统结构》(2010年出版)、《计算机系统基础》(2014年出版),两者内容差不多,第二本更像是第一本的升级版。

在阅读这2本书时,发现还是比较容易理解的,这个系列整理出了相关知识点,加深记忆以备忘。

计算机的功能和特性

我们现在使用计算机能做的事情非常多,能够看网页、听音乐、看视频、社交聊天,这背后其实都是对信息的处理。

计算机背后是如何工作的呢?我们先来看计算机的一个简单定义:

计算机是一种能自动化对数字信息进行运算和逻辑处理的高速处理装置,计算机处理的对象都是数字化信息。

计算机的基本功能包括以下3个方面:

  • 数据处理:计算机最基本的功能,可以计算加减乘除等运算,也可以处理文字、数字、音乐、视频信息。
  • 数据存储:计算机能够自动工作的最基本保证,数据和程序事先被存储好,在需要时被取出自动执行。
  • 数据传送:计算机内部各功能部件之间的数据传输,以达到信息交换,计算机外部连接使得信息得以传输到另一台计算机,从而构建出了计算机网络。

有了这些基本功能,计算机中肯定需要有对数据处理、数据存储、数据传送相对应的功能部件,它们分别是:算术逻辑运算器、存储器、总线和IO接口

计算机主要的核心部件都采用高速电子元器件制造,处理速度极快,这就为运算速度提供了基础保证。

计算机可以处理各种信息,应用也极其广泛,只要现实世界中某个问题能找到相应的算法,就能编写成程序通过计算机执行算出结果,这为计算机的准确性和智能化提供了重要的基础。

阅读全文 »

只要你是做软件开发的,就肯定听说过ASCII、Unicode、UTF-8、GBK这些字符编码,而且字符编码时刻与我们开发相关联。

它们之间到底有什么区别?为什么会有这么多字符编码?这篇文章我们来看一下它们之间的的关系以及区别。

概念

在开始之前,作为程序员,我希望大家能够先理清楚两个概念:字符集、字符编码

很多人搞不清楚这两者的关系与区别,以至于对于字符编码方面的知识了解不深,甚至混乱。

字符集

平时我们生活中使用的文字、标点符号、图形符号、数字,这些可以统称为字符

由非常多个字符组合后产生的集合,这个集合称为字符集

也就是说,我们可以人为的根据某个规则归纳一些我们使用的文字、符号,这些文字、符号的集合就称为一个字符集,并且可以根据不同的规则划分不同的字符集。

字符编码

那字符编码是什么?

我们知道计算机内部使用的是二进制运算,如果要想让计算机识别我们人类的文字、符号、数字,就需要一个转换规则,把我们人类使用的这些字符转换成计算机认识的二进制,也就是0和1。

这个转换的规则就称为字符编码

阅读全文 »

在 Python 开发中,yield 关键字的使用其实较为频繁,例如大集合的生成,简化代码结构、协程与并发都会用到它。

但是,你是否真正了解 yield 的运行过程呢?

这篇文章,我们就来看一下 yield 的运行流程,以及在开发中哪些场景适合使用 yield

生成器

如果在一个方法内,包含了 yield 关键字,那么这个函数就是一个「生成器」。

生成器其实就是一个特殊的迭代器,它可以像迭代器那样,迭代输出方法内的每个元素。

如果你还不清楚「迭代器」是什么,可以参考我写的这篇文章:Python进阶——迭代器和可迭代对象有什么区别?

我们来看一个包含 yield 关键字的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# coding: utf8

# 生成器
def gen(n):
for i in range(n):
yield i

g = gen(5) # 创建一个生成器
print(g) # <generator object gen at 0x10bb46f50>
print(type(g)) # <type 'generator'>

# 迭代生成器中的数据
for i in g:
print(i)

# Output:
# 0 1 2 3 4

注意,在这个例子中,当我们执行 g = gen(5) 时,gen 中的代码其实并没有执行,此时我们只是创建了一个「生成器对象」,它的类型是 generator

然后,当我们执行 for i in g,每执行一次循环,就会执行到 yield 处,返回一次 yield 后面的值。

这个迭代过程是和迭代器最大的区别。

换句话说,如果我们想输出 5 个元素,在创建生成器时,这个 5 个元素其实还并没有产生,什么时候产生呢?只有在执行 for 循环遇到 yield 时,才会依次生成每个元素。

此外,生成器除了和迭代器一样实现迭代数据之外,还包含了其他方法:

  • generator.__next__():执行 for 时调用此方法,每次执行到 yield 就会停止,然后返回 yield 后面的值,如果没有数据可迭代,抛出 StopIterator 异常,for 循环结束
  • generator.send(value):外部传入一个值到生成器内部,改变 yield 前面的值
  • generator.throw(type[, value[, traceback]]):外部向生成器抛出一个异常
  • generator.close():关闭生成器

通过使用生成器的这些方法,我们可以完成很多有意思的功能。

阅读全文 »

为了提高程序的运行效率,Python与其他语言一样,提供了多进程和多线程的开发方式,这篇文章我们来讲Python的多进程和多线程开发。

进程

Python提供了mutilprocessing模块,为多进程编程提供了友好的API,并且提供了多进程之间信息同步和通信的相关组件,如QueueEventPoolLockPipeSemaphoreCondition等模块。

函数当做进程

Python中创建多进程的方式有2种:

  • 函数当做进程
  • 类当做进程

逻辑简单的任务一般使用函数当做进程,逻辑较多或代码结构复杂的建议使用类当做进程。

首先来看函数当做进程的例子:

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

import os
import time
import random
from multiprocessing import Process

def task(name):
s = random.randint(1, 10)
print 'pid: %s, name: %s, sleep %s ...' % (os.getpid(), name, s)
time.sleep(s)

if __name__ == '__main__':
# 创建5个子进程执行
ps = []
for i in range(5):
p = Process(target=task, args=('p%s' % i, ))
ps.append(p)
p.start()

# 主进程等待子进程结束
for p in ps:
p.join()

# Output:
# pid: 52361, name: p0, sleep 8 ...
# pid: 52362, name: p1, sleep 7 ...
# pid: 52363, name: p2, sleep 8 ...
# pid: 52364, name: p3, sleep 3 ...
# pid: 52365, name: p4, sleep 2 ...

使用p = Process(target=func, args=(arg1, arg2...))即可创建一个进程,调用p.start()启动一个进程,p.join()使得主进程等待子进程执行结束后才退出。

当这个程序执行时,你可以ps查看一下进程,会发现一共有6个进程在执行,其中包括1个主进程,5个子进程。

阅读全文 »

做 Python 开发时,想必你肯定听过 GIL,它经常被 Python 程序员吐槽,说 Python 的多线程非常鸡肋,因为 GIL 的存在,Python 无法利用多线程提高性能。

但事实真的如此吗?

这篇文章,我们就来看一下 Python 的 GIL 到底是什么?以及它的存在,究竟对我们的程序有哪些影响。

GIL是什么?

查阅官方文档,GIL 全称 Global Interpreter Lock,即全局解释器锁,它的官方解释如下:

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

翻译成中文就是:

在 CPython 解释器中,全局解释锁 GIL 是在于执行 Python 字节码时,为了保护访问 Python 对象而阻止多个线程执行的一把互斥锁。这把锁的存在主要是因为 CPython 解释器的内存管理不是线程安全的。然而直到今天 GIL 依旧存在,现在的很多功能已经习惯于依赖它作为执行的保证。

我们从这个定义中,可以看到几个重点:

  1. GIL 是存在于 CPython 解释器中的,属于解释器层级,而并非属于 Python 的语言特性。也就是说,如果你自己有能力实现一个 Python 解释器,完全可以不使用 GIL
  2. GIL 是为了让解释器在执行 Python 代码时,同一时刻只有一个线程在运行,以此保证内存管理是安全的
  3. 历史原因,现在很多 Python 项目已经习惯依赖 GIL(开发者认为 Python 就是线程安全的,写代码时对共享资源的访问不会加锁)

在这里我想强调的是,因为 Python 默认的解释器是 CPython,GIL 是存在于 CPython 解释器中的,我们平时说到 GIL 就认为它是 Python 语言的问题,其实这个表述是不准确的。

其实除了 CPython 解释器,常见的 Python 解释器还有如下几种:

  • CPython:C 语言开发的解释器,官方默认使用,目前使用也最为广泛,存在 GIL
  • IPython:基于 CPython 开发的交互式解释器,只是增强了交互功能,执行过程与 CPython 完全一样
  • PyPy:目标是加快执行速度,采用 JIT 技术,对 Python 代码进行动态编译(不是解释),可以显著提高代码的执行速度,但执行结果可能与 CPython 不同,存在 GIL
  • Jython:运行在 Java 平台的 Python 解释器,可以把 Python 代码编译成 Java 字节码,依赖 Java 平台,不存在 GIL
  • IronPython:和 Jython 类似,运行在微软的 .Net 平台下的 Python 解释器,可以把 Python 代码编译成 .Net 字节码,不存在 GIL

虽然有这么多 Python 解释器,但使用最广泛的依旧是官方提供的 CPython,它默认是有 GIL 的。

那么 GIL 会带来什么问题呢?为什么开发者总是抱怨 Python 多线程无法提高程序效率?

阅读全文 »

如果你看过比较优秀的 Python 开源框架,肯定见到过元类的身影。例如,在一个类中定义了类属性 __metaclass__,这就说明这个类使用了元类来创建。

那元类的实现原理究竟是怎样的?使用元类能帮我们在开发中解决什么样的问题?

这篇文章,我们就来看一下 Python 元类的来龙去脉。

什么是元类?

我们都知道,定义一个类,然后调用它的构造方法,就可以初始化出一个实例出来,就像下面这样:

1
2
3
4
5
6
class Person(object)

def __init__(name):
self.name = name

p = Person('zhangsan')

那你有没有想过,我们平时定义的类,它是如何创建出来的?

别着急,我们先来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> a = 1               # 创建a的类是int a是int的实例
>>> a.__class__
<type 'int'>

>>> b = 'abc' # 创建b的类是str b是str的实例
>>> b.__class__
<type 'str'>

>>> def c(): # 创建c的类是function 方法c是function的实例
... pass
>>> c.__class__
<type 'function'>

>>> class D(object): # 创建d的类是D d是D的实例
... pass
>>> d.__class__
<class '__main__.D'>

在这个例子中,我们定义了 intstrfunctionclass,然后分别调用了它们的__class__ 方法,这个 __class__ 方法可以返回实例是如何创建出来的。

从方法返回的结果我们可以看到:

  • 创建整数 a 的类是 int,也就是说 aint 的一个实例
  • 创建字符串 b 的类是 str,也就是说 bstr 的一个实例
  • 创建函数 c 的类是 function,也就是说 cfunction 的一个实例
  • 创建实例 d 的类是 class,也就是说 dclass 的一个实例

除了这些之外,我们在开发中使用到的例如 listdict 也类似,你可以测试观察一下结果。

现在我们已经得知,创建这些实例的类是 intstrfunctionclass,那进一步思考一下,这些类又是怎么创建出来的呢?

同样地,我们也调用这些类的 __class__ 方法,观察结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> a = 1
>>> a.__class__.__class__
<type 'type'>
>>>
>>> b = 'abc'
>>> b.__class__.__class__
<type 'type'>
>>>
>>> def c():
... pass
>>> c.__class__.__class__
<type 'type'>
>>>
>>> class D(object):
... pass
>>> d = D()
>>> d.__class__.__class__
<type 'type'>

从结果我们可以看到,创建这些类的类,都是 type,所以 type 就是创建所有类的「元类」。也就是说,元类的作用就是用来创建类的

你可以这样理解:

  1. 元类 -> 类
  2. 类 -> 实例

用伪代码表示,就是下面这样:

1
2
klass = MetaClass()     # 元类创建类
obj = klass() # 类创建实例

是不是很有意思?

在这里,你也可以感受一下这句话的含义:Python 中一切皆对象!

无论是普通类型、方法、实例,还是类,都可以统一看作对象,它们的起源就是元类。

其实,在 Python 中,使用 type 方法,我们可就以创建出一个类,type 方法的语法如下:

1
type(class_name, (base_class, ...), {attr_key: attr_value, ...})

例如,像下面这样,我们使用 type 方法创建 MyClass 类,并且让它继承 object

1
2
3
4
5
>>> A = type('MyClass', (object, ), {}) # type创建一个类,继承object
>>> A
<class '__main__.MyClass'>
>>> A()
<__main__.MyClass object at 0x10d905950>

我们还可以使用 type 创建一个包含属性和方法的类:

1
2
3
4
5
6
7
8
9
10
11
>>> def foo(self):
... return 'foo'
...
>>> name = 'zhangsan'
>>>
# type 创建类B 继承object 包含 name 属性和 foo 方法
>>> B = type('MyClass', (object, ), {'name': name, 'foo': foo})
>>> B.name # 打印 name 属性
'zhangsan'
>>> print B().foo() # 调用 foo 方法
foo

通过 type 方法创建的类,和我们自己定义一个类,在使用上没有任何区别。

其实,除了使用 type 方法创建一个类之外,我们还可以使用类属性 __metaclass__ 创建一个类,这就是下面要讲的「自定义元类」。

阅读全文 »

在 Python 开发中,我们经常听到有关「容器」、「迭代器」、「可迭代对象」、「生成器」的概念。

我们经常把这些概念搞混淆,它们之间有哪些联系和区别呢?

这篇文章,我们就来看一下它们之间的关系。

容器

首先,我们先来看一下容器是如何定义的?

简单来说,容器就是存储某些元素的统称,它最大的特性就是判断一个元素是否在这个容器内。

怎么理解这句话?

很简单,在 Python 中,我们通常使用 innot in 来判断一个元素存在/不存在于一个容器内。

例如下面这个例子:

1
2
3
4
5
6
print('x' in 'xyz')  # True
print('a' not in 'xyz') # True
print(1 in [1, 2, 3]) # True
print(2 not in (1, 2, 3)) # False
print('x' not in {'a', 'b', 'c'}) # True
print('a' in {'a': 1, 'b': 2}) # True

在这个例子中,我们可以看到 strlisttuplesetdict 都可以通过 innot in 来判断一个元素是否在存在/不存在这个实例中,所以这些类型我们都可以称作「容器」。

那为什么这些「容器」可以使用 innot in 来判断呢?

这是因为它们都实现了 __contains__ 方法。

如果我们也想自定义一个容器,只需像下面这样,在类中定义 __contains__ 方法就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
class A:

def __init__(self):
self.items = [1, 2]

def __contains__(self, item):
return item in self.items

a = A()
print(1 in a) # True
print(2 in a) # True
print(3 in a) # False

在这个例子中,类 A 定义了 __contains__ 方法,所以我们就可以使用 1 in a 的方式去判断这个元素是否在 A 这个容器内。

换句话说,一个类只要实现了 __contains__ 方法,那么它就是一个「容器」。

我们在开发时,除了使用 in 判断元素是否在容器内之外,另外一个常用的功能是:输出容器内的所有元素。

例如执行 for x in [1, 2, 3],就可以迭代出容器内的所有元素。

那使用这种方式输出元素,是如何实现的?这就跟「迭代器」有关了。

阅读全文 »

在 Python 开发中,我们经常会使用到 with 语法块,例如在读写文件时,保证文件描述符的正确关闭,避免资源泄露问题。

你有没有思考过, with 背后是如何实现的?我们常常听到的上下文管理器究竟是什么?

这篇文章我们就来学习一下 Python 上下文管理器,以及 with 的运行原理。

with语法块

在讲解 with 语法之前,我们先来看一下不使用 with 的代码如何写?

我们在操作一个文件时,代码可以这么写:

1
2
3
4
5
6
7
# 打开文件
f = open('file.txt')
for line in f:
# 读取文件内容 执行其他操作
# do_something...
# 关闭文件
f.close()

这个例子非常简单,就是打开一个文件,然后读取文件中的内容,最后关闭文件释放资源。

但是,代码这么写会有一个问题:在打开文件后,如果要对读取到的内容进行其他操作,在这操作期间发生了异常,这就会导致文件句柄无法被释放,进而导致资源的泄露。

如何解决这个问题?

也很简单,我们使用 try ... finally 来优化代码:

1
2
3
4
5
6
7
8
9
# 打开文件
f = open('file.txt')
try:
for line in f:
# 读取文件内容 执行其他操作
# do_something...
finally:
# 保证关闭文件
f.close()

这么写的好处是,在读取文件内容和操作期间,无论是否发生异常,都可以保证最后能释放文件资源。

但这么优化,代码结构会变得很繁琐,每次都要给代码逻辑增加 try ... finally 才可以,可读性变得很差。

针对这种情况,我们就可以使用 with 语法块来解决这个问题:

1
2
3
with open('file.txt') as f:
for line in f:
# do_something...

使用 with 语法块可以完成之前相同的功能,而且这么写的好处是,代码结构变得非常清晰,可读性也很好。

明白了 with 的作用,那么 with 究竟是如何运行的呢?

上下文管理器

首先,我们来看一下 with 的语法格式:

1
2
with context_expression [as target(s)]:
with-body

with 语法非常简单,我们只需要 with 一个表达式,然后就可以执行自定义的业务逻辑。

但是,with 后面的表达式是可以任意写的吗?

答案是否定的。要想使用 with 语法块,with 后面的的对象需要实现「上下文管理器协议」。

什么是「上下文管理器协议」?

一个类在 Python 中,只要实现以下方法,就实现了「上下文管理器协议」:

  • __enter__:在进入 with 语法块之前调用,返回值会赋值给 withtarget
  • __exit__:在退出 with 语法块时调用,一般用作异常处理
阅读全文 »

在 Python 开发中,你可能听说过「描述符」这个概念,由于我们很少直接使用它,所以大部分开发人员并不了解它的原理。

但作为熟练使用 Python,想要进阶的你,建议还是了解一下描述符的原理,这也便于你更深层次地理解 Python 的设计思想。

其实,在开发过程中,虽然我们没有直接使用到描述符,但是它在底层却无时不刻地被使用到,例如以下这些:

  • functionbound methodunbound method
  • 装饰器propertystaticmethodclassmethod

是不是都很熟悉?

这些都与描述符有着千丝万缕的关系,这篇文章我们就来看一下描述符背后的工作原理。

什么是描述符?

在解释什么是「描述符」之前,我们先来看一个简单的例子。

1
2
3
4
class A:
x = 10

print(A.x) # 10

这个例子非常简单,我们在类 A 中定义了一个类属性 x,然后打印它的值。

其实,除了直接定类属性之外,我们还可以这样定义一个类属性:

1
2
3
4
5
6
7
8
class Ten:
def __get__(self, obj, objtype=None):
return 10

class A:
x = Ten() # 属性换成了一个类

print(A.x) # 10

仔细看,这次类属性 x 不再是一个具体的值,而是一个类 TenTen 中定义了一个 __get__ 方法,返回具体的值。

在 Python 中,允许把一个类属性,托管给一个类,这个属性就是一个「描述符」。

换句话说,「描述符」是一个「绑定行为」的属性。

怎么理解这句话?

回忆一下,我们开发时,一般把「行为」叫做什么?是的,「行为」一般指的是一个方法。

所以我们也可以把「描述符」理解为:对象的属性不再是一个具体的值,而是交给了一个方法去定义。

可以想一下,如果我们用一个方法去定义一个属性,这么做的好处是什么?

有了方法,我们就可以在方法内实现自己的逻辑,最简单的,我们可以根据不同的条件,在方法内给属性赋予不同的值,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Age:
def __get__(self, obj, objtype=None):
if obj.name == 'zhangsan':
return 20
elif obj.name == 'lisi':
return 25
else:
return ValueError("unknow")

class Person:

age = Age()

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

p1 = Person('zhangsan')
print(p1.age) # 20

p2 = Person('lisi')
print(p2.age) # 25

p3 = Person('wangwu')
print(p3.age) # unknow

这个例子中,age 类属性被另一个类托管了,在这个类的 __get__ 中,它会根据 Person 类的属性 name,决定 age 是什么值。

这只是一个非常简单的例子,我们可以看到,通过描述符的使用,我们可以轻易地改变一个类属性的定义方式。

描述符协议

了解了描述符的定义,现在我们把重点放到托管属性的类上。

其实,一个类属性想要托管给一个类,这个类内部实现的方法不能是随便定义的,它必须遵守「描述符协议」,也就是要实现以下几个方法:

  • __get__(self, obj, type=None) -> value
  • __set__(self, obj, value) -> None
  • __delete__(self, obj) -> None

只要是实现了以上几个方法的其中一个,那么这个类属性就可以称作描述符。

另外,描述符又可以分为「数据描述符」和「非数据描述符」:

  • 只定义了 __get___,叫做非数据描述符
  • 除了定义 __get__ 之外,还定义了 __set____delete__,叫做数据描述符

它们两者有什么区别,我会在下面详述。

现在我们来看一个包含 __get____set__ 方法的描述符例子:

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
# coding: utf8

class Age:

def __init__(self, value=20):
self.value = value

def __get__(self, obj, type=None):
print('call __get__: obj: %s type: %s' % (obj, type))
return self.value

def __set__(self, obj, value):
if value <= 0:
raise ValueError("age must be greater than 0")
print('call __set__: obj: %s value: %s' % (obj, value))
self.value = value

class Person:

age = Age()

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

p1 = Person('zhangsan')
print(p1.age)
# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'>
# 20

print(Person.age)
# call __get__: obj: None type: <class '__main__.Person'>
# 20

p1.age = 25
# call __set__: obj: <__main__.Person object at 0x1055509e8> value: 25

print(p1.age)
# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class '__main__.Person'>
# 25

p1.age = -1
# ValueError: age must be greater than 0

在这例子中,类属性 age 是一个描述符,它的值取决于 Age 类。

从输出结果来看,当我们获取或修改 age 属性时,调用了 Age__get____set__ 方法:

  • 当调用 p1.age 时,__get__ 被调用,参数 objPerson 实例,typetype(Person)
  • 当调用 Person.age 时,__get__ 被调用,参数 objNonetypetype(Person)
  • 当调用 p1.age = 25时,__set__ 被调用,参数 objPerson 实例,value 是25
  • 当调用 p1.age = -1时,__set__ 没有通过校验,抛出 ValueError

其中,调用 __set__ 传入的参数,我们比较容易理解,但是对于 __get__ 方法,通过类或实例调用,传入的参数是不同的,这是为什么?

这就需要我们了解一下描述符的工作原理。

描述符的工作原理

要解释描述符的工作原理,首先我们需要先从属性的访问说起。

在开发时,不知道你有没有想过这样一个问题:通常我们写这样的代码 a.b,其背后到底发生了什么?

这里的 ab 可能存在以下情况:

  1. a 可能是一个类,也可能是一个实例,我们这里统称为对象
  2. b 可能是一个属性,也可能是一个方法,方法其实也可以看做是类的属性

其实,无论是以上哪种情况,在 Python 中,都有一个统一的调用逻辑:

  1. 先调用 __getattribute__ 尝试获得结果
  2. 如果没有结果,调用 __getattr__

用代码表示就是下面这样:

1
2
3
4
5
6
7
def getattr_hook(obj, name):
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name)

我们这里需要重点关注一下 __getattribute__,因为它是所有属性查找的入口,它内部实现的属性查找顺序是这样的:

  1. 要查找的属性,在类中是否是一个描述符
  2. 如果是描述符,再检查它是否是一个数据描述符
  3. 如果是数据描述符,则调用数据描述符的 __get__
  4. 如果不是数据描述符,则从 __dict__ 中查找
  5. 如果 __dict__ 中查找不到,再看它是否是一个非数据描述符
  6. 如果是非数据描述符,则调用非数据描述符的 __get__
  7. 如果也不是一个非数据描述符,则从类属性中查找
  8. 如果类中也没有这个属性,抛出 AttributeError 异常

写成代码就是下面这样:

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
# 获取一个对象的属性
def __getattribute__(obj, name):
null = object()
# 对象的类型 也就是实例的类
objtype = type(obj)
# 从这个类中获取指定属性
cls_var = getattr(objtype, name, null)
# 如果这个类实现了描述符协议
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
# 优先从数据描述符中获取属性
return descr_get(cls_var, obj, objtype)
# 从实例中获取属性
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name]
# 从非数据描述符获取属性
if descr_get is not null:
return descr_get(cls_var, obj, objtype)
# 从类中获取属性
if cls_var is not null:
return cls_var
# 抛出 AttributeError 会触发调用 __getattr__
raise AttributeError(name)

如果不好理解,你最好写一个程序测试一下,观察各种情况下的属性的查找顺序。

到这里我们可以看到,在一个对象中查找一个属性,都是先从 __getattribute__ 开始的。

__getattribute__ 中,它会检查这个类属性是否是一个描述符,如果是一个描述符,那么就会调用它的 __get__ 方法。但具体的调用细节和传入的参数是下面这样的:

  • 如果 a 是一个实例,调用细节为:
1
type(a).__dict__['b'].__get__(a, type(a))
  • 如果 a 是一个,调用细节为:
1
a.__dict__['b'].__get__(None, a)

所以我们就能看到上面例子输出的结果。

数据描述符和非数据描述符

了解了描述符的工作原理,我们继续来看数据描述符和非数据描述符的区别。

从定义上来看,它们的区别是:

  • 只定义了 __get___,叫做非数据描述符
  • 除了定义 __get__ 之外,还定义了 __set____delete__,叫做数据描述符

此外,我们从上面描述符调用的顺序可以看到,在对象中查找属性时,数据描述符要优先于非数据描述符调用。

在之前的例子中,我们定义了 __get____set__,所以那些类属性都是数据描述符

我们再来看一个非数据描述符的例子:

1
2
3
4
5
6
7
8
9
class A:

def __init__(self):
self.foo = 'abc'

def foo(self):
return 'xyz'

print(A().foo) # 输出什么?

这段代码,我们定义了一个相同名字的属性和方法 foo,如果现在执行 A().foo,你觉得会输出什么结果?

答案是 abc

为什么打印的是实例属性 foo 的值,而不是方法 foo 呢?

这就和非数据描述符有关系了。

我们执行 dir(A.foo),观察结果:

1
2
print(dir(A.foo))
# [... '__get__', '__getattribute__', ...]

看到了吗?Afoo 方法其实实现了 __get__,我们在上面的分析已经得知:只定义 __get__ 方法的对象,它其实是一个非数据描述符,也就是说,我们在类中定义的方法,其实本身就是一个非数据描述符。

所以,在一个类中,如果存在相同名字的属性和方法,按照上面所讲的 __getattribute__ 中查找属性的顺序,这个属性就会优先从实例中获取,如果实例中不存在,才会从非数据描述符中获取,所以在这里优先查找的是实例属性 foo 的值。

到这里我们可以总结一下关于描述符的相关知识点:

  • 描述符必须是一个类属性
  • __getattribute__ 是查找一个属性(方法)的入口
  • __getattribute__ 定义了一个属性(方法)的查找顺序:数据描述符、实例属性、非数据描述符、类属性
  • 如果我们重写了 __getattribute__ 方法,会阻止描述符的调用
  • 所有方法其实都是一个非数据描述符,因为它定义了 __get__

描述符的使用场景

了解了描述符的工作原理,那描述符一般用在哪些业务场景中呢?

在这里我用描述符实现了一个属性校验器,你可以参考这个例子,在类似的场景中去使用它。

首先我们定义一个校验基类 Validator,在 __set__ 方法中先调用 validate 方法校验属性是否符合要求,然后再对属性进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Validator:

def __init__(self):
self.data = {}

def __get__(self, obj, objtype=None):
return self.data[obj]

def __set__(self, obj, value):
# 校验通过后再赋值
self.validate(value)
self.data[obj] = value

def validate(self, value):
pass

接下来,我们定义两个校验类,继承 Validator,然后实现自己的校验逻辑。

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

class Number(Validator):

def __init__(self, minvalue=None, maxvalue=None):
super(Number, self).__init__()
self.minvalue = minvalue
self.maxvalue = maxvalue

def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)

class String(Validator):

def __init__(self, minsize=None, maxsize=None):
super(String, self).__init__()
self.minsize = minsize
self.maxsize = maxsize

def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)

最后,我们使用这个校验类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person:

# 定义属性的校验规则 内部用描述符实现
name = String(minsize=3, maxsize=10)
age = Number(minvalue=1, maxvalue=120)

def __init__(self, name, age):
self.name = name
self.age = age

# 属性符合规则
p1 = Person('zhangsan', 20)
print(p1.name, p1.age)

# 属性不符合规则
p2 = person('a', 20)
# ValueError: Expected 'a' to be no smaller than 3
p3 = Person('zhangsan', -1)
# ValueError: Expected -1 to be at least 1

现在,当我们对 Person 实例进行初始化时,就可以校验这些属性是否符合预定义的规则了。

function与method

我们再来看一下,在开发时经常看到的 functionunbound methodbound method 它们之间到底有什么区别?

来看下面这段代码:

1
2
3
4
5
6
7
8
class A:

def foo(self):
return 'xyz'

print(A.__dict__['foo']) # <function foo at 0x10a790d70>
print(A.foo) # <unbound method A.foo>
print(A().foo) # <bound method A.foo of <__main__.A object at 0x10a793050>>

从结果我们可以看出它们的区别:

  • function 准确来说就是一个函数,并且它实现了 __get__ 方法,因此每一个 function 都是一个非数据描述符,而在类中会把 function 放到 __dict__ 中存储
  • function 被实例调用时,它是一个 bound method
  • function 被类调用时, 它是一个 unbound method

function 是一个非数据描述符,我们之前已经讲到了。

bound methodunbound method 的区别就在于调用方的类型是什么,如果是一个实例,那么这个 function 就是一个 bound method,否则它是一个 unbound method

property/staticmethod/classmethod

我们再来看 propertystaticmethodclassmethod

这些装饰器的实现,默认是 C 来实现的。

其实,我们也可以直接利用 Python 描述符的特性来实现这些装饰器,

property 的 Python 版实现:

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

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self.fget
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"
return self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "can't delete attribute"
return 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 的 Python 版实现:

1
2
3
4
5
6
7
class staticmethod:

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

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

classmethod 的 Python 版实现:

1
2
3
4
5
6
7
8
9
10
11
class classmethod:

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

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

除此之外,你还可以实现其他功能强大的装饰器。

由此可见,通过描述符我们可以实现强大而灵活的属性管理功能,对于一些要求属性控制比较复杂的场景,我们可以选择用描述符来实现。

总结

这篇文章我们主要讲了 Python 描述符的工作原理。

首先,我们从一个简单的例子了解到,一个类属性是可以托管给另外一个类的,这个类如果实现了描述符协议方法,那么这个类属性就是一个描述符。此外,描述符又可以分为数据描述符和非数据描述符。

之后我们又分析了获取一个属性的过程,一切的入口都在 __getattribute__ 中,这个方法定义了寻找属性的顺序,其中实例属性优先于数据描述符调用,数据描述符要优先于非数据描述符调用。

另外我们又了解到,方法其实就是一个非数据描述符,如果我们在类中定义了相同名字的实例属性和方法,按照 __getattribute__ 中的属性查找顺序,实例属性优先访问。

最后我们分析了 functionmethod 的区别,以及使用 Python 描述符也可以实现 propertystaticmethodclassmethod 装饰器。

Python 描述符提供了强大的属性访问控制功能,我们可以在需要对属性进行复杂控制的场景中去使用它。

之前做爬虫时,在公司设计开发了一个通用的垂直爬虫平台,后来在公司做了内部的技术分享,这篇文章把整个爬虫平台的设计思路整理了一下,分享给大家。

写一个爬虫很简单,写一个可持续稳定运行的爬虫也不难,但如何构建一个通用化的垂直爬虫平台?

这篇文章,我就来和你分享一下,一个通用垂直爬虫平台的构建思路。

爬虫简介

首先介绍一下,什么是爬虫?

搜索引擎是这样定义的:

网络爬虫(又被称为网页蜘蛛,网络机器人),是一种按照一定的规则,自动地抓取网页信息的程序或者脚本。

很简单,爬虫就是指定规则自动采集数据的程序脚本,目的在于拿到想要的数据。

而爬虫主要分为两大类:

  • 通用爬虫(搜索引擎)
  • 垂直爬虫(特定领域)

由于第一类的开发成本较高,所以只有搜索引擎公司在做,如谷歌、百度等。

而大多数企业在做的都是第二类,成本低、数据价值高。

例如一家做电商的公司只需要电商领域有价值的数据,那开发一个只采集电商领域数据的爬虫平台,意义较大。

我要和你分享的主要是针对第二类,垂直爬虫平台的设计思路。

阅读全文 »