# 概述

Python 是一种解释型的、面向对象的、动态数据类型的、通用的高级程序设计语言。下面我们可以来详细了解一下这门语言的特性。

# Python 是一门高级语言

Python 是一门人与计算机之间交流的语言,这类语言被称为计算机语言,计算机语言主要是我们用来和计算机沟通的语言,它和我们日常生活中人与人之间交流的语言有共通之处但又有所不同。

小知识:我们这个世界上时至今天大概有 6000 多种语言用于人与人之间沟通,当然,充满活力的语言并不多,也许你只知道汉语、英语、日语,或者当我们学习历史后会了解到联合国六种正式语言 - 汉、英、法、俄、西班牙、阿拉伯,甚至其他的语言我们都不知道叫什么,但事实是这些语言是现实而又存在的,据统计,全球每两周就会有一门语言消失,而令我们自豪的是汉语是世界上使用人数最多的语言(据统计,母语使用人数超过五千万的语言只有 23 种,汉语是使用人数最多的,英语是使用最广泛的)。

计算机语言总的来说可以分成机器语言,汇编语言,高级语言三大类。机器语言比较低级,就是我们常见的 0 和 1,大家可能在大学学习过数电,里面有高电平和低电平,正好对应这里的二进制的 1 和 0,当然,除了数电课程外,在计算机组成原理也有对补码、移码等相对底层的计算机逻辑运算的描述,其本身都是基于二进制 0 和 1 来计算的。

机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。不同的计算机都有各自的机器语言,即指令系统。机器语言是最底层的语言,主要是基于二进制 0 和 1 实现的,二进制是计算机的语言的基础,这种计算机能够认识的语言,就是机器语言。计算机系统最大特征是指令通过一种语言传达给机器。为了使电子计算机进行各种工作,就需要有一套用以编写计算机程序的数字、字符和语法规划,由这些字符和语法规则组成计算机各种指令(或各种语句)。

计算机的硬件作为一种电路元件,它的输出和输入只能是有电或者没电,也就是所说的高电平和低电平,所以计算机传递的数据是由 “0” 和 “1” 组成的二进制数,所以说二进制的语言是计算机语言的本质。计算机发明之初,人们为了去控制计算机完成自己的任务或者项目,只能去编写 “0”、“ 1” 这样的二进制数字串去控制电脑,其实就是控制计算机硬件的高低电平或通路开路,这种语言就是机器语言。直观上看,机器语言十分晦涩难懂,其中的含义往往要通过查表或者手册才能理解, 使用的时候非常痛苦,尤其当你需要修改已经完成的程序时,这种看起来无序的机器语言会让你无从下手,也很难找到程序的错误。而且,不同计算机的运行环境不同,指令方式操作方式也不尽相同,所以当你在这种机器语言就有了特定性,只能在特定的计算机上执行,而一旦换了机器就需要重新编程,这极大的降低了程序的使用和推广效率。但由于机器语言具有特定性,完美适配特定型号的计算机,故而运行效率远远高过其他语言。机器语言,也就是第一代编程语言。

不难看出机器语言作为一种编程语言, 灵活性较差可阅读性也很差,为了减轻机器语言带给软件工程师的不适应,人们对机器语言进行了升级和改进:用一些容易理解和记忆的字母,单词来代替一个特定的指令。通过这种方法,人们很容易去阅读 已经完成的程序或者理解程序正在执行的功能,对现有程序的 bug 修复以及运营维护都变得更加简单方便,这种语言就是我们所说的汇编语言, 即第二代计算机语言。

汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,也称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。汇编语言又称第二代计算机语言,用一些容易理解和记忆的字母,单词来代替一个特定的指令,比如:用 “ADD” 代表数字逻辑上的加减,“ MOV” 代表数据传递等等,这时候就需要一个专门的程序把这些字符变成计算机能够识别的二进制数,所以平台相关,移植很难。

比起机器语言,汇编语言具有更高的机器相关性,更加便于记忆和书写,但又同时保留了机器语言高速度和高效率的特点。汇编语言仍是面向机器的语言,很难从其代码上理解程序设计意图,设计出来的程序不易被移植,故不像其他大多数的高级计算机语言一样被广泛应用。所以在高级语言高度发展的今天,它通常被用在底层,通常是程序优化或硬件操作的场合。

在编程语言经历了机器语言,汇编语言等更新之后,人们发现了限制程序推广的关键因素 —— 程序的可移植性。需要设计一个能够不依赖于计算机硬件,能够在不同机器上运行的程序。这样可以免去很多编程的重复过程,提高效率,同时这种语言又要接近于数学语言或人的自然语言。在计算机还很稀缺的 50 年代,诞生了第一个高级编程语言。当时计算机的造价不菲,但是每天的计算量又有限,如何有效的利用计算机有限的计算能力成为了当时人们面对的问题。同时,因为资源的稀缺, 计算机的运行效率也成为了那个年代工程师追寻的目标。为了更高效的使用计算机,人们设计出了高级编程语言,来满足人们对于高效简洁的编程语言的追求。

计算机不能理解高级语言,毕竟高级语言已经离计算机底层并与机器语言相去甚远。

# Python 是一门解释性语言

Python 是一门解释性语言 (英语是 interpreted),解释性语言的执行方式类似于我们日常生活中的 “同声翻译”,应用程序源代码一边由相应语言的解释器 “翻译” 成目标代码 (机器语言),一边执行,因此效率比较低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器,但是也有好处,好处就是这种方式比较灵活,可以动态地调整、修改应用程序。类似的语言还有 Ruby 等。

小知识:还有另一种高级语言,也就是编译性语言,区别于 “同声传译”,这个更像是将一本英文书籍预先翻译为中文书籍,能够是我们在阅读时增加效率。比如我们常见的 java,就是需要通过 jvm 将.java 文件预先编译为.class 字节码文件后才能够被 jvm 逐行解释执行,这里同样存在解释过程,不过因为 Jit 技术的广泛使用,使得可以直接将 java 语言编译为机器指令执行,以提高其性能,此时其解释性概念也被弱化了,甚至可以视 java 语言为纯编译性的语言。编译其实就是指在应用程序执行之前需要将源代码编译为目标代码,也就是翻译成机器可以理解的机器语言,不需要在运行时重新翻译,所以程序可以脱离语言环境执行,这种语言使用比较方便,执行效率也很高。但是应用程序的修改必须依赖于对源代码的重新编译,所以修改上会很不方便。但是因为其执行效率以及在编译期间的诸多优势,又使其成为当今计算机高级语言中比较受欢迎的语言之一,换句话说,当今大多数的编程语言都是编译型的。比如 VB,C/C++,Java,GO,Pascal 等。

其实也有人说 java 是解释性语言,这个其实每个人的理解不同,侧重不同,所以不用太过于纠结,毕竟语言发展到今天,并且由于 Jit 技术的广泛应用,很多语言都集成了 Jit 编译器,并且在此基础上推陈出新,也许说不定将来某一天 Python 也会是一门优秀的编译性语言呢?

当我们编写 python 代码时,通常会产生一个以.py 结尾的文件,如果里面有 main 方法,我们就可以直接执行它,但是我们执行它之前,首先就需要有一个 python 的解释器,否则,这个文件就无法被执行。

目前市场上流行的 python 解释器有很多,比较主流的就是 CPython,这也是 python 的官方版本的解释器,因为这个解释器是用 C 语言开发的,所以叫 CPython,因为是官方的、开源的,所以使用最广泛。

还有一种解释器叫 IPython,它的功能和 CPython 基本相同,也是开源的,但它更注重交互,就像名字里的 I 代表 interactive 一样,IPython 提供了一个增强的 Python 脚本,也叫 Python Shell,可以方便我们直接和解释器交互,用来测试或者做并行计算都很不错,如果想体验一下的话,可以直接通过 pip 安装 ipython,然后输入 ipython 进入交互模式即可,或者使用 conda 进行安装也可以。

PyPy 也是一种 python 解释器,官方主页上描述为速度快,也就是快速的解释器。以最新的 PyPy3.7 为例,官方表明其速度相较于 CPython 大概快上 4 倍以上,PyPy 另一个主打的 feature 就是 JIT 即时编译,对于内存使用方便也有做到一些优化。

JPython 也是一种 100% 通过 Java 语言实现的 python 解释器,可以看成是 Java 的补充,它可以被 jvm 编译为 java 字节码执行,Java 程序员可以将 Jython 库 (其实就是 1 个 jar 包) 添加到系统中,以允许最终用户编写简单或复杂的脚本来为应用程序添加功能。

import org.python.util.PythonInterpreter;

public class JythonHelloWorld {
  public static void main(String[] args) {
    try(PythonInterpreter pyInterp = new PythonInterpreter()) {
      pyInterp.exec("print('Hello Python World!')");
    }
  }
}

另外还有一些其他的 Python 解释器,但是都比较小众化了,所以暂时不需要了解,等以后有机会使用的时候可以再深入研究。

# Python 是面向对象的(OOM-Object-OrientedModel)

什么是面向对象呢?相信有不少初入计算机行业的小朋友都会开玩笑地将其理解为女盆友,比如我大学时候好像是写 C 语言时,每次创建一个变量后,编辑器的右下角就会提醒你创建了一个对象,那时候觉得很好玩,写程序不愁找对象,因为万物皆对象(好吧,把 Java 编程思想里的至理名言班门弄斧一下)。

面向对象是一种编程范式,也是 python 支持的一种编程范式之一。面向对象的核心是抽象,将世界万物抽象为各个对象,每个对象拥有其自身的属性,也会有其相应的行为。比如将人比作一个对象,那么人拥有五官、有肢体,这些可以作为人的属性,同时人又可以走路、跳舞、吃饭,这些都是人的行为,所以可以理解为 对象=属性+行为 ,这也体现了对象封装的特性。并且由于人与人之间有一些继承关系,比如父子间的遗传,或者最直白的就是财产继承等等。面向对象还有一种特性就是多态,也就是同一个行为可以具有多个不同表现形式,在程序里可以体现为同一个接口的不同实现方式,在现实中比如打印机可以打印黑白图片,也可以打印彩色图片等等。

Python 符合面向对象范式,所以也遵循面向对象的基本原则:单一功能、开闭原则、里氏替换、迪米特(最少知识)、接口隔离以及依赖反转(或依赖倒置),具体可以了解一下 23 种设计模式,这些设计模式也是遵循这些准则来实践的。python 一个简单的类大概如下:

class Person:
    """这里可以写一些注释帮助大家理解这个类做些什么"""
    public_count = 0
    __private_count = 0  # 私有属性,外部不可访问
    def __init__(self, name, age, height, weight):
        self.name = name
        self.age = age
        self.height = height
        self.weight = weight
        Person.public_count += 1
        Person.__private_count += 1
    def display_name(self):
        print('Name is %s.' % self.name)
    def display_age(self):
        print('Age is %s.' % self.age)
    def bmi(self):
        """ 体重BMI指数(Body Mass Index, 单位kg/m2) """
        bmi = self.weight / pow(self.height, 2)
        if bmi < 18.5:
            print('BMI指数:%.2f,体重过低' % bmi)
        elif 18.5 <= bmi <= 23.9:
            print('BMI指数:%.2f,体重正常' % bmi)
        elif 24 <= bmi <= 27.9:
            print('BMI指数:%.2f,超重' % bmi)
        else:
            print('BMI指数:%.2f,肥胖' % bmi)
    @staticmethod
    def __secret():  # 类私有方法,外部不可访问
        print("It's a private method.")
jalen = Person('Jalen', 29, 1.78, 77)
jalen.display_name()
jalen.display_age()
jalen.bmi()
print(jalen.public_count)
print(jalen.__doc__)

# Python 拥有动态的数据类型(dynamically-typed )

动态类型语言是运行期间才做数据类型检查的语言,即动态类型语言编程时,永远不用给任何变量指定数据类型。 该语言会在第一次赋值给变量时,在内部将数据类型记录下来。python 是典型的动态数据类型的语言。其他比如 ruby 也是动态类型的,这种动态类型的语言有利有弊,优点是书写和阅读方便,不用显式声明变量的具体类型,隐藏了一些编程细节,缺点就是命名的不规范可能导致后期代码维护上的困难,我们经常遇到的一个问题就是,很多其他同事写的代码拿给另外一个同事看,很难确定数据的类型是什么,甚至有一些自己早期写的代码,对于一些函数的参数类型都不敢确定具体是什么,而如果变量的命名不够规范,不仅对自己,对其他维护人员也是一场灾难。

区别于动态类型,还有一种静态类型语言,最流行的 Java 就是静态类型语言,这类语言在编译期检查变量的数据类型,所以在写程序时候需要明确指定你定义的变量是哪种类型,并且这些类型一旦定义便是固定的,而且这种类型语言在声明变量的初期就必须指定类型,否则会产生编译错误。动态类型和静态类型的区别就是看它是否在编译时确定。举例如下:

if __name__ == '__main__':
    a = 'a'
    b = 10
    c = [1, 2, 3]
    d = {'name': 'Jalen', 'age': 12}
    e = (1, 2, 3)
    print('End!')
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Test {
    public static void main(String[] args) {
        Character a = 'a';
        Integer b = 10;
        ArrayList<Integer> c = new ArrayList<>(3);
        c.add(1);
        c.add(2);
        c.add(3);
        Map<String, Object> d = new HashMap<>();
        d.put("name", "Jalen");
        d.put("age", 12);
    }
}

编程语言还有一种区分:强类型、弱类型,强类型定义语言(Explicit type conversion),强制数据类型定义语言,类型安全的语言,一旦变量被指定某个数据类型,如果不经强制转换,即永远是此数据类型。弱类型定义语言(Implicit type conversion,类型不安全的语言),数据类型可以被忽略的语言。它与强类型定义语言相反,一个变量可以赋不同数据类型的值。从这种区分上看,python 是强类型语言,举个例子:

#include <stdio.h>
int main(void) {
    char a='a';
    int b=10;
    int c=a+b;  // 这里不会报错
	return 0;
}
if __name__ == '__main__':
    a = 'a'
    b = 10
    c = a + b  # 报:TypeError: must be str, not int
    print('End!')

# Python 拥有非常棒的可读性(code readability)

Python 具有高可读性,这也是其设计初衷,Python 有相对完整的代码风格,我们可以称之为 “Pythonic”,比如 python 里对于参数的设计:

  1. 位置参数 func (x, y)、
  2. 关键字参数 func (x, y, z=None)、
  3. 任意参数 func (x, y, *args)
  4. 任意关键字参数 func (**kwargs)

还有针对变量的命名以及函数返回值的设计,包括对于循环语句(for i in list),Unpacking 解包和创建忽略变量(_)等等。

PEP8 是 Python 事实上的代码风格指南,比如 Intellij 的 Pycharm 便集成了该风格校验,当你写代码时,如果有某些不符合该风格的地方,编辑器右侧会产生一个黄色的警告,提醒你应该怎样去修改,并将修改方法提供给你,或者你也可以直接通过快捷键让编辑器帮你修改(比如 windows 可以默认敲击 Alt + Enter 直接修改)。假如你没有使用 IDE,你也可以使用 pip 安装一些代码检测工具,比如 pycodestyle、autopep8 或者 flake8,通过它们执行一些检测命令,然后根据输出的一些不合规的结果进一步去调整你的代码。这和 Java 里的一些工具很类似,比如 java 里有 Checkstyle,FindBugs 等插件,也可以通过简单执行去检测代码的不合规之处。

下面列一下 python 之禅,它包含了影响 Python 编程语言设计的 19 条软件编写原则,在最初及后来的一些版本中,一共包含 20 条,其中第 20 条是「这一条留空(...)请 Guido 来填写。(这留空的一条从未公布也可能并不存在)

Tim Peters: Timsort 算法作者,该算法广泛应用于 python2.3+ 和其他语言中,他同时 python 语言的贡献者之一,参与早期 CPython 的实现,Python 之禅作为 Python Enhancement Proposal 20 被纳入官方 Python 文献,并作为复活节彩蛋被纳入 Python 解释器。他为 Python Cookbook 贡献了关于算法的章节。

Guido van Rossum:Python 之父,生于荷兰,阿姆斯特丹大学数学和计算机科学硕士学位。帮助开发过 ABC 语言,1989 开始 python 开发。python 语言有很多设计之处借鉴了 ABC 语言。

>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Python之禅 by Tim Peters
优美胜于丑陋(Python以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)
不要包容所有错误,除非您确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)
当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为您不是 Python 之父(这里的 Dutch 是指 Guido )
做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)
如果您无法向人描述您的方案,那肯定不是一个好方案;反之亦然(方案测评标准)
命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

# Python 的 GC 也很好(garbage-collected)

提到垃圾收集,首先要了解的就是内存管理,垃圾收集都是基于当前内存分布和内存现状对于无用的变量或引用做出的进一步的收集处理工作。

什么是内存?内存就是内部存储器,是计算机组成的部件之一,用来暂时存放 cpu 中运行的数据,我们所有的程序,比如你经常玩的英雄联盟,或者经常玩的一些手机游戏,都要先通过磁盘加载到内存中才能运行,区别于外部存储器(也就是硬盘或者 USB 外设),CPU 从内存中加载数据的速度要比硬盘快,所以也可以把内存认为是一个缓冲区,可以视为为了提高程序的运行速度而设计的一个硬件设备,所以内存的运行也决定计算机整体运行程度的快慢。我们可以想象一个人正在读书,此时就可以把内存比作人的大脑,比如你恰好此时正在看这边文章,那么你的大脑此时此刻首先要做的就是将这篇文章逐行读到大脑中,当然不排除你可能一目十行,如果你能一目十行,那么恭喜你,你的大脑可能已经赶超了千千万万的人,或者说你的大脑数据交换快,看书速度快,所以也更聪明。而将文字记到大脑里就相当于是计算机从硬盘将数据加载到内存,下面你要做的就是去思考,可能认为我写的都是胡扯,也可能认为我写的还不错。计算机的思考主要是运算,内存就是它的脑容量,然而记忆总会模糊甚至遗忘,内存也是,我们可能不记得 3 天前的中午吃了什么菜,计算机也是如此,因为我们不需要它记得太多,内存有限,并非无穷大,总有些不太重要的数据从内存中匆匆而过,消失在曾经的记忆里。

经常遇到有些小伙伴有时候会把缓存和内存混淆,其实他们确实很相似,也许只是在使用方式上产生的概念的不同表达,通常我们程序里的缓存,诸如 Ehcache 还有 redis、memcache 等一些 NoSQL,它们存储的数据,都可以理解为内存中的一部分,我们希望能够提升程序的数据访问速度,而主动将数据 push 到内存中,以提高用户交互能力,尽可能减少磁盘 IO、网络 IO 等行为带来的数据获取上的延迟。除了应用中的缓存概念外,事实上还有一种物理上的缓存,叫高速缓存,它介于 CPU 和内存之间,可以理解为用来给内存提速的小帮手,因为现实中 cpu 的执行速度非常非常的快,导致内存中数据的交换速度仍然无法满足 cpu 的需求,高速缓存的出现主要就是为了解决 CPU 运算速度与内存读写速度不匹配的矛盾。高速缓存目前有一级缓存、二级缓存,部分高端 cpu 还配有三级缓存,这种缓存一般容量很小,而且非常昂贵,因为成本也非常高。

上面说了这么多,其实就是想简单描述一下内存这个概念,但是又怕自己描述不清。很多小伙伴都知道,gc 是面向内存的,不仅仅是 python 的 gc,包括 java 对于历代 jvm 的 gc 收集器的设计,同样是针对内存方面的阶段性调整和优化,下面我们来简单了解下 python 的 gc。

python 主要使用的是一种比较简单的垃圾收集方式,我们称它为引用计数。所谓引用计数,理解起来就是当一个对象在内存中分配空间后,我们初始化一个计数器,初始值设为 0,这个值可以存放在对象头或者是维护一个引用列表,当有其他对象,比如可能是一个字典,或者是一个列表中引用了这个对象后,那对应这个地址的空间上的引用计数器自动 + 1,反之如果列表销毁后,那么这个对象的引用则会 - 1,直到计数器最终归 0,此时我们认为该对象不再被引用,或者说不再被程序里任何其他对象使用,此时,就可以被后台的 gc 线程清理掉这块内存上的数据,重新清理出的内存空间可以方便分配给下一个对象。

但是熟悉 java 的小伙伴会指出,我们 java 一开始也有过引用计数法的设计,但是因为对象的循环依赖问题,最后还是选择了可达性分析算法(通过 GC Roots 遍历可达节点),python 使用引用计数法不会有问题吗?这确实是一个问题,如果两个对象互相依赖,同时这两个对象在内存中都不再被其他对象或方法使用,始终孤立,那么它们的计数器始终不会 - 1,这种情况下很可能会导致隐藏的内存泄露风险,甚至严重些最终导致内存溢出,此时,就要对引用计数法提供一些额外的辅助。

python 采用 “标记 - 清除” 算法解决容器内循环引用的问题。

  1. 标记:遍历对象,如果对象可达,则标记为可达,说明对象还再用
  2. 清除:遍历对象,如果发现某些对象不可达,则回收

CPython 解释器维护了两个双端链表,指针分别指向前后两个容器对象,方便插入和删除操作,其中一个链表记录当前容器内对象,另一个记录有可能被回收的对象(初始为空)。gc 守护线程在遍历容器内对象时(也就是第一个双端链表)会将所有 gc 引用计数默认 - 1,然后筛选出第一个链表中引用归 0 的对象挪到第二个链表中,留待确认;然后第二次遍历,根据可达分析,当第一个链表中某个对象 gc 引用不为 0 并且指向了第二个链表中某个待回收对象时,此时会将该对象从第二个链表移回第一个链表,以此类推,最后第二个链表剩下的对象便是循环引用的对象,对于这些对象再执行清除操作。所以在标记的过程中会存在 stop the world 的情况,以防止程序的执行对容器内对象产生新的引用导致误差。

标记 - 清除会导致整个应用的停滞,基于此,python 设计了一种以空间换时间的方式来提高 gc 效率,这种方式就是 “分代回收”,分代回收存在一个年龄的概念,也就是对象的年龄会随着 gc 的次数增加而增加,如果某个对象熬过了第一轮 gc 而没有被清除掉,那么年龄 + 1,熬过了第二轮 gc,年龄 + 2,后续对于这种年龄大的对象,我们有理由认为它可能会伴随应用的运行一直存在,这时我们可以在上面的 “标记 - 清除” 算法中将这种年龄大的对象排除掉,减少扫描次数,此时只需要着重考虑和扫描那些年轻的对象,但是也并不是说年龄大的对象永远不去扫描,只是减少扫描频率,可能当年龄 + 2 的对象扫描了 1 次的时候,年龄为 0 的对象已经扫描了 10 次。这和 java 里的分代回收有相同之处,GC 的方式与其 minor gc/full gc 类似。

所以说,python 的 gc = 引用计数 + 标记清除 + 分代回收。

# Python 支持多种编程范式(Programming paradigm)

编程范式是指某种编程语言典型的编程风格或编程方式。编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种编程语言也可以适用多种编程范式。比如 python 支持包括结构化编程(structured,典型的是 Procedural 过程化编程),面向对象编程(object-oriented)和函数式编程(functional programming)

Python 由 Guido van Rossum 于 1989 年底发明,第一个公开发行版发行于 1991 年。Python 作为 ABC 编程语言的继承者,ABC 对 Python 语言的设计产生了重大影响(Guido 曾在 1980 年代中期在 ABC 系统上工作了几年)。现在我们还能够从 python 语言的设计风格上看到一些昔日 ABC 语言的影子,比如缩进风格,比如 for 循环等等

python 支持面向过程的结构化编程,我们可以在不使用 class 的情况下,仅仅通过 def 定义一些列函数实现一个复杂的 web app。当然,我们也可以使用 class 这种面向对象的编程范式来实现一个应用,这个完全看我们的个人喜好,我个人倒是很喜欢面向过程这种编程方式,尤其是结合 flask web 这种小巧的 web 框架一起使用。

python 支持函数式编程,并且对于一些常用的函数做了一些封装,同时支持 lambda 操作,允许把函数本身作为参数传入另一个函数,还允许返回一个函数,无需变量,只需处理固定的输入并且获取固定的输出即可。尤其是结合 pandas、numpy 做数据分析处理时,函数式使用起来更方便。

# Python 的 GIL 全局解释锁

GIL 是什么?GIL 的全称是 Global Interpreter Lock (全局解释器锁),来源是 python 设计之初为了数据安全所做的决定。参考 java 多线程对于临界资源的非常复杂的处理我们就能看到一些端倪。

我们都知道,每个 CPU 在同一时间只能执行一个线程,就像我们思考一样,在那一瞬间,我们脑海里只会驻留一个画面。计算机的 cpu 也是一样,我们目前使用的操作系统都是分时操作系统,也就是采用时间片轮转方式处理服务请求,每个程序在执行时需要先获取时间片,这保证了同一时间一个 cpu 只会做一件事情,我们日常上网中看似可以在听音乐的同时聊 qq(以单核计算机为例),可能会让我们产生一个 cpu 是一起做这些事情,其实不然,事实上是因为 cpu 执行时间分片,一瞬间产生了多次切换,就像我们虽然可以在烧水的同时嗑瓜子,但是我们其实真正做的永远只是当前这一件事,其他的事情会被阻塞住或者是被等待。

实际上这里也涉及到了并行 / 串行,并行 / 并发的一些概念,可以稍微简单解释一下。并行是指多个 cpu 同一时间做不同事情,突出同时进行;串行是指所有事情由一个 cpu 按顺序去做,强调按顺序执行,不会造成时间片切换;并发是指无论上一个开始执行的任务是否完成,当前任务都可以开始执行,强调事情可能在一个时间段内同时发生,而且由于多核的原因以及单核时间片的切换,会产生竞争,对于临界资源的访问需要增强保护,否则可能会产生意料之外的状况。

很多了解 python 的开发人员都会从 python 的 GIL 设计吐槽 python,毕竟 python 的 GIL 设计导致 python 在多线程方面感到有些鸡肋,一个进程里的某个线程想要执行,必须先拿到 GIL 锁,GIL 锁相当于该线程的通行证,如果没有锁,那对不起,等吧。等上一个线程释放锁后再去竞争 GIL 锁,只有获得锁才能进入 cpu。而锁的竞争、线程的切换、锁的释放等等,都会消耗进程和 cpu 的资源,python 里一个进程永远只能同时执行一个线程 (拿到 GIL 的线程才能执行),所以在多核 cpu 上多线程效率并不高。

我们日常书写的程序,要么是 cpu 密集型,要么是 io 密集型,cpu 密集型通常指的是最大化利用 cpu 的计算能力,这种程序对于 cpu 的要求比较严格,会长时间占用时间片,也导致 cpu 更加繁忙。而 io 密集型通常指我们在加载数据时产生一些 io 等待,此时 cpu 的时间片可以让出来,允许其他线程做一些事情,等数据加载后再将时间片切换回之前的线程,所以对 cpu 的占用率不是很高。(python2.x 和 python3.x CPython 对于 GIL 的实现有所有不同,由 ticks 计数过渡到计时器)

所以,多核下想要充分利用 python 的提高并行效率,比较通用的办法是使用多进程。比如 python 的 gunicorn 服务器,通常会和 flask web 应用相结合,作为其启动方式,而 gunicorn 启动前又需要事先设置 workers 的数量,也就是应用的进程数量,这里其实就是一个多进程的使用的案例,通常我们建议 gunicorn 的 workers 数量设置为 (2 x $num_cores) + 1,也就是我们当前服务器的 cpu 数量的两倍再加 1 作为我们应用的进程数。

对于 Gunicorn 的 workers 描述:Gunicorn should only need 4-12 worker processes to handle hundreds or thousands of requests per second. Gunicorn relies on the operating system to provide all of the load balancing when handling requests. Generally we recommend (2 x $num_cores) + 1 as the number of workers to start off with.

# Python 对 unicode 的支持

python2.x 和 python3.x 对于编码有很大的差异,对于做过 python2 升级 python3 的程序员来说,unicode 编码这块可以说得上是非常痛苦的事情,如果是小项目还好,大的项目,光是处理编码问题就会耗费你的大量时间,甚至有些隐藏的编码问题不能及时发现还会导致产线异常。而且你的某个引入的第三方模块或许都会因为升级导致无法使用。

对于 python2 来讲,说不准什么时候就会因为引入了其他字符集导致 decode error,因为 python 发行要比 unicode 制定标准要早许多年,这也导致了 python 最初字符编码是使用 ASCII 实现的,而 ASCII 字符集锁包容的字符范围很窄,只有那 256 个字符,所以也就增加了后续处理其他字符集上的困难,对于 iso-8859-1 这些西欧字符集以及中文字符的支持都不友好,不过升到 python3 以后会感觉很舒适,毕竟短期内你不会遇到字符编码的问题了。

需要额外关注的是,官方宣布,2020 年 1 月 1 日,停止 Python2 的更新。而 Python2.7 被确定为最后一个 Python 2.x 版本。所以还是推荐大家使用 python3,并且由于 python2 到 python3 的这种痛苦升级经历,不仅仅对于使用者,对于 python 的核心开发团队也是一种严酷的折磨,所以 Guido 曾经在访谈中就 python4 这个新版本是否会出现做出严肃的表述:“almost taboo to talk about a Python 4 in a serious sense”。提醒大家不要期待 python4,或许 python4 永远不会出现,毕竟 python 的核心开发团队在从 Python 2 过渡到 Python 3 的过程中吸取了痛苦而又深刻的经验教训。"I'm not thrilled about the idea of Python 4 and nobody in the core dev team really is – so probably there never will be a 4.0 and we'll just keep numbering until 3.33, at least,",所以大家可以放心的使用 python3,毕竟当前 python 的最新版本还是 3.10.1。

# Python 开源协议

像 Perl 语言一样,Python 源代码同样遵循 GPL (GNU General Public License) 协议。说起 GPL 开源协议,很多人会想到它的传染性,GPL 协议具有传染性源于其对自身的定义:只要你的项目中的某个部分是以 GPL 协议开源发布的,那么你的整个项目以及衍生的一些作品必须都要遵循 GPL 许可证,GPL 是第一个普遍使用的 Copyleft 许可证(Copyleft 条款更要求作者所授权的人对改动后的衍生作品要使用相同许可证,以保障其后续所有衍生作品都能被任何人自由使用。类似于一种保护伞机制,鼓励传播而不受限制,促进创作自由)。

除了 python,像 mysql 也是遵循 GPL,所以其被 oracle 从 Sun 公司收购后也还是一直保持这开源免费这一特性,虽然 mysql 近年来一直在走下坡路,这也可能和 postgresql 的兴起以及部分开源社区拥护 MariaDB 有很大关系。包括 Linus 在发布 Linux 的时候也是选择了 GPL,这也保证了我们人人都可以免费使用 linux 内核,并且后续衍生出很多 linux 发行版,如 debian 系列的 ubuntu,Redhat 系列中的 centos(虽然 CentOS Linux 发行版止步在了 CentOS8 版本,并且将在 2021 年 12 月 31 日停止维护,但是 CentOS Stream 的出现仍然诉说着 linux 的活力)等等,侧面促进了 linux 社区的磅礴发展。总而言之,任何的代码,只要置于 GPL 之下,就不再受作者或所有者控制了,无法被任何人或组织剥夺,意味着持续开源。

其实如果你想闭源,还有一种宽泛的 GPL 许可,被成为 LGPL(Lesser General Public License),这种许可证同样具有权威性,这是为了保证开源软件得到使用和推广,因为闭源软件中总会或多或少参杂有开源的成分,所以才诞生了 LGPL 许可证。毕竟从另外一面看,GPL 许可证实在是太严厉了。

世界上的开源许可证(Open Source License)大概有上百种,其他开源协议大致还有 BSD、MIT、Mozilla、Apache,每个协议都有其自身特点和肩负的责任,所以我们在做开源项目时或者在集成开源项目时一定要注意。当你在开发过程中使用了一个不兼容的软件许可证,可能会对后期的项目进行产生风险。

具体 python 相关协议许可可以直接到 github 上查看,查看地址为:https://github.com/python/cpython,可以切换不同 branch 查看不同版本对于 license 的描述细节,目前来看 python3 可能特意地避免使用 GPL code,所以可以不受限制的使用在专有项目上,This Python distribution contains no GNU General Public License (GPL) code, so it may be used in proprietary projects. There are interfaces to some GNU code but these are entirely optional.

# Python 的用途

python 自身很小巧、简单,非常适合初入软件行业的开发人员,据统计,同样的 java 业务代码使用 python 实现,可以减少接近一半的代码量。并且 python 还有丰富的三方库支持,比如数据科学中常用的用于数值处理和科学计算的 numpy(内部解除了 Python 的 GIL 全局解释锁)、scipy 等;还有我们经常用于数据处理的 pandas;用于数据展示的 matplotlib;用于机器学习的 tensforflow(谷歌)、pytorch、keras、scikit-learn 等等。Python 在数据分析方面具有很大的优势。

同时,python 目前在 web 开发,比如目前社区比较成熟的有 Django、Flask 等 web 框架;在大数据开发、人工智能、机器学习、后端服务和嵌入式的开发领域也有很重要的应用,相比于 java,python 的适用面更加广泛。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Jalen Chu 微信支付

微信支付

Jalen Chu 支付宝

支付宝

Jalen Chu 公众号

公众号