Kaito's Blog

致力成为一枚silver bullet.

0%

我们知道计算机中数字都是使用的二进制表示和运算,对于我们熟悉的十进制数字,可以通过数值进制公式进行转换后计算和显示。

但除了这种编码方式之外,计算机有时为了显示和计算方便,对十进制还定义了新的编码方式:ASCII码字符表示、BCD码表示。这篇文章我们来了解一下这2种编码方式的特点。

ASCCI码字符表示

如果只是对十进制数进行打印或显示,那么可以把十进制数看成字符串,直接用ASCII码表示,09分别对应ASCII码的30H39H,这种表示方式,1位十进制用8位二进制数表示。

用ASCII码表示的十进制又分为前分隔数字串后嵌入数字串

前分隔数字串

前分隔数字串将符号位单独用一个字节表示,放在数字串之前。

正号(+)用ASCII码2B(H)表示,符号(-)用2D(H)表示。

例如十进制数+236用前分隔数字串表示为0010 1011 0011 0010 0011 0011 0011 0110,对应的十六进制为2B 32 33 36,在内存中占用4个字节。

十进制数-2369用前分隔数字串表示为0010 1101 0011 0010 0011 0011 0011 0110 0011 1001,对应的十六进制为2D 32 33 36 39,在内存中占用5个字节。

可见,十进制数每增加一位,就要多一个字节表示对应数字。

阅读全文 »

在上一篇文章中,我们主要介绍了在计算机中使用定点数表示数字的方式。

简单回顾一下,简单来说,用定点数表示数字时,会约定小数点的位置固定不变,整数部分和小数部分分别转换为二进制,就是定点数的结果。

但用定点数表示小数时,存在数值范围、精度范围有限的缺点,所以在计算机中,我们一般使用「浮点数」来表示小数。

这篇文章,我们就来详细看一下浮点数到底是如何表示小数的,以及浮点数的的范围和精度有多大。

什么是浮点数?

首先,我们需要理解什么是浮点数?

之前我们学习了定点数,其中「定点」指的是约定小数点位置固定不变。那浮点数的「浮点」就是指,其小数点的位置是可以是漂浮不定的。

这怎么理解呢?

其实,浮点数是采用科学计数法的方式来表示的,例如十进制小数 8.345,用科学计数法表示,可以有多种方式:

1
2
3
4
8.345 = 8.345 * 10^0
8.345 = 83.45 * 10^-1
8.345 = 834.5 * 10^-2
...

看到了吗?用这种科学计数法的方式表示小数时,小数点的位置就变得「漂浮不定」了,这就是相对于定点数,浮点数名字的由来。

使用同样的规则,对于二进制数,我们也可以用科学计数法表示,也就是说把基数 10 换成 2 即可。

阅读全文 »

在现实生活中,我们经常使用整数和小数,不知道你有没有思考过,这些数字在计算机中是如何存储的?

我们学习计算机知识时,经常听到「定点数」和「浮点数」,它们之间有什么区别?

平时说到整数,我们就说它是一个定点数,说到小数,就说它是一个浮点数,这种说法是正确的吗?

这篇文章,我们先来看一下,「定点数」究竟与整数和小数有什么关系。

什么是定点数?

要想理解什么是「定点数」,首先,我们需要理解「定点」究竟是什么意思?

我们都知道,数字既包括整数,又包括小数,而小数的精度范围要比整数大得多,所以如果我们想在计算机中,既能表示整数,也能表示小数,关键就在于这个小数点如何表示?

于是人们想出一种方法,即约定计算机中小数点的位置,且这个位置固定不变,小数点前、后的数字,分别用二进制表示,然后组合起来就可以把这个数字在计算机中存储起来,这种表示方式叫做「定点」表示法,用这种方法表示的数字叫做「定点数」。

也就是说「定」是指固定的意思,「点」是指小数点,小数点位置固定即定点数名字的由来。

定点数如何表示数字?

既然定点数只是表示数字的一种方式,那试想,它可以表示整数吗?可以表示小数吗?

答案是肯定的。

定点数如果要表示整数或小数,分为以下三种情况:

  1. 纯整数:例如整数100,小数点其实在最后一位,所以忽略不写
  2. 纯小数:例如:0.123,小数点固定在最高位
  3. 整数+小数:例如1.24、10.34,小数点在指定某个位置

对于前两种情况,纯整数和纯小数,因为小数点固定在最低位和最高位,所以它们用定点数表示时,原理是相同的,只需要把整数部分、小数部分,按照十进制转二进制的规则,分别转换即可。

而对于整数 + 小数的情况,用定点表示时,需要约定小数点的位置,才能在计算机中表示。

阅读全文 »

信息的二进制编码

在计算机内部,所有信息都是采用二进制进行编码和计算的,有没有想过,为什么采用这种方式?

其原因在于二进制只有两种状态,制造一个只有两个稳定状态的物理器件要比多个稳定状态的物理期间容易得多,使用高、低电位或脉冲有、无表示都很方便,能够很可靠的表示“0”和“1”。

如果都采用二进制计算,那么我们平时使用的数字、符号甚至图片和视频,都应该通过某种方式转换成二进制,
才能得以让计算机运算。这种转换方式称为“编码”。

我们人类角度看到的数据到计算机内部的数据转换如下:

这篇文章,我们先来分析数值型数据的进制与转换规则。

阅读全文 »

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

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

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

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

这本书的作者叫袁春风,她是南京大学计算机系的教授,写过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__ 创建一个类,这就是下面要讲的「自定义元类」。

阅读全文 »