内置类
通过之前的交互式运行和脚本式运行教程,读者应该掌握了如何写hello world
、在哪里写hello world
以及写完之后怎么运行,接下来我们就介绍各种语法来丰富代码的内容。
篇幅限制我不会事无巨细,只能介绍最基础的内容和深入学习的方法。我希望读者看完本篇内容可以学会怎么学python。
学会使用dir和help函数
读者应该已经熟稔print
的用法,也大概能体会到何为函数、如何调用函数、如何传递参数,并且也应该注意到我们会用""
引号把字符包裹住传递到函数中。这足够让我们在一切开始之前先介绍两个非常有用的函数。
函数和参数
可能有读者会对这两个词感到疑惑,因为我们还未介绍函数和它的参数。
一个粗浅的理解是:函数是一个机器,输入一个东西,输出一个东西。输入的东西就叫参数,输出的东西叫返回值。
flowchart LR
参数 --> f{函数} --> 返回值
后面我们还会从类的角度给出另外一个理解。 学会寻求help
求救是每个人都应该学会的生存技能
如果不传入任何参数,直接运行help()
,那么我们会进入一个交互式命令行:
在这个命令行我们可以进行一些查询,按照提示可以输入任何的module
、keyword
或是topic
。
例如输入print
就会返回一下内容:
如果我们传递一些参数进去,就可以直接查询相关的内容:
通过这样一个函数,我们可以快速学习python的大多数内容。你可以忘记任何一个函数,但请不要忘记help
。
dir
是一把解剖钳
先用help
函数查一下dir
函数的用法:
返回的结果说的也很清楚,我这里做一个中文翻译:
如果不传递参数运行dir()
,那么就会返回当前命名空间(scope)的所有变量(names)。如果传递了一个对象进去,那么就会返回这个对象的属性(attributes)列表。
如果对象自定义了__dir__
方法(method),则会被调用。默认的__dir__
方法的逻辑是:
- 对于一个模块(module)对象,会返回模块的属性
- 对于一个类(class)对象,会返回它的属性并且递归到它所有的父类
- 对于其他的对象,则会返回它的属性、类的属性并且递归到所有的父类
总而言之,dir
函数可以帮助我们了解某个对象的属性和方法,还可以查询命名空间内的变量。
例如:
就返回了默认情况下,python内置的所有变量。
例如这个__builtins__
:
查询的结果包含了python所有的内置异常、内置函数、内置数据类型等等。
从类的视角来理解python
上面突然蹦出来了一大推的专有名词,我来挨个解释一下(下面可能会出现一些很高级的语法现象,读者可以暂时不管代码细节,重要的是文字解读):
内置(builtin)
所谓内置就是区别于自定义,内置是本来就有的,无需定义就可以使用的东西,而自定义是用户后续创建的。
类(class)
类是对象的抽象表达。说人话就是类(class)是一个模具,对象(object)则是模具生产出来的东西。
例如:
老师
老师就是一个类,而隔壁班的王老师就是一个对象。
由于老师们之所以都叫老师,是因为他们有共同的特质,所以我们把这些共同特质抽象出来做成了老师
这个类。而那些共同的特质我们就称之为类的属性(attribute)和方法(method)。
- 所谓的属性是指类的特征,例如每个老师都应该有年龄这个特征。
- 所谓的方法是在类上定义的函数,例如每个老师都可以在年末自然增长年龄。
再举一个数学上的例子,在抽象代数中这个思想无处在:
群(Group)
群\((G, \cdot)\)是由集合\(G\)和二元运算\(\cdot\)构成的,满足群公理的数学结构。
所谓的群公理指的是:
- 封闭性:二元运算\(\cdot\)在\(G\)上是封闭的
- 结合律:二元运算满足\((a\cdot b)\cdot c=a\cdot (b\cdot c)\)
- 单位元:\(\exists e \in G\)使得\(e\cdot a=a \quad \forall a \in G\)
- 逆元:\(\forall a \in G\)都\(\exists b \in G\)满足\(a\cdot b=e\)
这个定义是从诸多的数学对象中抽象出来的定义,比如整数和加法就是一个群,魔方某种意义上也是一个群。
- 这里的群公理就是群这个类的共同特质
- 这里的集合\(G\)就是群的属性
- 这里的运算\(\cdot\)就是群上定义的方法
python中几乎所有的东西都是类:函数是类,模块和包也是类;数据类型是类,错误和异常也是类……所以理解类这个概念对于理解python的逻辑至关重要。
类的定义方式是class
关键字,例如
创建老师类 | |
---|---|
注意到这里我们访问了定义在类里面的函数update_age
和属性age
,而访问的方式是object.attribute
,这种用.
来访问对象属性的语法风格和其他很多语言都是类似的。
了解一下其他语言的情况
R语言中.
是合法的变量名字符,没有任何的语法含义。
.
和python一样,都是访问对象属性和方法的语法结构。 C语言也和python类似:
最后,类之间是存在继承与被继承关系的。假如我们有一个人类
,而人
是一个很高级的类,老师
肯定有人的一切特质,所以如果我们可以让老师继承这个类。老师和人之间的关系就成为子类和父类。这种继承关系可以用.__mro__
(含义是Method resolution order,直译为方法的解释顺序)来查询,后续我会给出例子。
对象(object)
理解了何谓类,对象就好理解多了。对象就是类的一个实现,还是前面的例子:
- 王老师是老师类的一个对象,简言之王老师是老师
- 整数集和加法是群类的一个对象,简言之\((\mathbb{Z},+)\)是一个群
对象的生成方式是多种多样的,例如一些python内置的类可以用特定的语法来生成。例如字符串可以用" "
来生成,列表可以用[ ]
来生成。也可以用类的调用来生成,例如:
函数(function)
函数是一个特殊的类,他们的共同特质是可以被调用(callable)。具体来说函数是定义了__call__
方法的类,每一个具体的函数是函数类的一个对象。函数的调用方式读者应该很熟悉了,就是在后方加上()
,这和数学中的习惯是一致的。
函数的定义方式是def
关键字,之前在类的例子中我们已经见过了。
在不同的场合我们会把同一个东西强调为类或者是函数,比如list
- 在
list(range(10))
中,我们一般会把他叫作函数,功能是把range(10)
转化为一个列表。 - 而在
isinstance(1, list)
中,我们一般把他叫做类,代表着列表这个数据类型。
读者注意区分这些叫法。
变量(name)和命名空间(scope)
同样的,前文已经使用了变量的语法a = list('hello')
就创建了一个变量。
请注意变量只是对象的一个名字,但并不是对象本身。如果我们弃用了这个名字,对象本身不一定会消失。而如果我们把这个名字给别人,那么变量的数据类型就会改变,所以python被称为弱类型语言。一个名字只能指向一个对象,但是一个对象可以有多个名字。
变量是存在于命名空间内的,例如函数内部的空间和外部的空间就是两个不同的空间,变量是彼此独立的,类内部的空间和外部的空间也是不同的空间。
此外变量的命名是有一定的规范的:
变量命名规范
- 不能包含特殊字符例如
$
- 不能包含语法保留字符例如
#
,@
,+
,-
,.
等 - 不能用数字开头
常见的命名方式有两类;
- 下划线:
update_age
- 驼峰命名法:
UpdateAge
虽然理论上可以用中文、日文等字符作为变量名,但是不推荐。
变量的创建方式是=
:
此外提一句,函数用def
定义之后也是一个名字,实际上存在没有名字的函数lambda
函数。因此,函数的存在也是有命名空间的,前文所述的定义在类里面的函数
就是在强调函数的命名空间。
关键字(keyword)
关键字并非一个独立的概念,而是和前面的概念交叉的:
有些关键字承载着python的语法,例如def
、for
、while
、if
、arise
等,他们是逻辑结构的组成者。有特殊的地位,可以说是我们用类来理解python中为数不多的例外。因为def
等这些关键字都不是对象,如果你运行type(def)
会直接报错。这一部分内容我们下一篇文章会详细介绍。
有些关键字则是一个变量,例如True
、False
、Ellipsis
等,他们是内置的变量,无需定义声明即可使用,本质上都是对象。
还有些关键字是一个函数,例如print
、help
、dir
等,他们是内置的函数,无需定义声明即可使用,本质上也是对象。
内置数据类型(builtin type)
所谓的内置数据类型就是内置的一些类,这些类上往往实现了某种数据结构以及很多的方法。并且各自有着专用的表示方式:
- 字符串(string),例如
"hello"
- 布尔值(bool),例如
True
- 列表(list),例如
[1, 2, 3]
- 元组(tuple),例如
(1, 2, 3)
- 集合(set),例如
{1, 2, 3}
- 整数(int),例如
100
- 浮点数(float),例如
3.1415
- 复数(complex),例如
1j+2
- 字典(dict),例如
{1 : 'one', 2 : 'two'}
等等。
内置异常(builtin Exception)
异常(Exception)也是一个类,各种异常都继承自BaseException
,例如NameError
的继承关系为:
NameError.__mro__ | |
---|---|
例如上面的错误,是一个NameError
,错误的原因是<stdin>
的第一行有一个变量clear
未定义。
模块(module)和包(package)
module也是一个类,这个类背后是一个或多个python文件,这些python文件就被称为包,或者库。我们可以通过import
关键字,来导入一个module,然后就可以使用module里面的东西了。
例如math
就是一个module,我们现在来用一下math
里面的开方函数:
嵌套函数
上面的代码中包含了嵌套函数的写法,其实完全可以按照数学里的复合函数来理解。
print(math.sqrt(2))
就是先计算根号2,然后把计算结果传入print
函数再运行。
技术总结
讲完了上面这些概念,读者应该能体会到为什么这节的标题叫做从类的视角来理解python
。可以说,python内万物都是类和对象,甚至类本身也是抽象类的一个对象。而我们前面说过,dir
可以接受一个对象作为参数,所以任何一个东西你都可以拿过来dir()
一下看看它里面有什么东西。
上帝创造万物,那么上帝从哪里来
类是抽象类的对象 这句话我并没有深入考究过,甚至你如果问我那抽象类是对象嘛?我是回答不上来的,因为本质上用python来理解python是不太够的。最流行python的解释器是C语言来写的,也即是Cpython。不过也有用python来写的python解释器,叫PyPy,这方面我了解不多留给读者自行探索。
细说内置数据类型
理解了上面一节的内容,那么这一节基本就是在学习各种类以及类上实现的方法和属性。
str
python中用单双引号' '
和双双引号" "
以及三引号""" """
、''' '''
包裹起来的东西都是字符串,他们没什么大区别。
dir(str)
返回的结果为:
__xx__
这种带着双下划线的- 以及
count
这种普普通通的
前者我们称为魔法方法(magic method),是python类编程中的一个特殊机制,是为了实现特殊的语法而写的一些函数。
例如__add__
方法,就规定了字符串之间的加法:
__len__
方法,就规定了字符串的长度算法: 最后介绍__getitem__
方法,这大概是最复杂的那个了。 这个方法规定了取值的算法,在python中取值的行为用两个方括号来表示:object[]
hello
从左到右依次是0:h
,1:e
……,所以我们最终取出来的东西是e
。 另外这个取值函数还接受slice
类型的参数:
其中slice
是另外一个内置数据类型。
切片(slice)介绍
slice可以传入三个参数:slice(start, stop, step),这三个参数都是整数,并且都可以缺省
- 如果start缺省,默认从开头开始切,如果为负数则从右边开始数
- 如果stop缺省,默认切到最后,如果为负数则从右边开始数
- 如果step缺省,默认为1,如果为负数,则从stop切到start
- 切片的范围是从start到stop(如果setp为负数则反过来),每隔step取一个,并且不包含stop的那一个,但是包含start的那个,简单来说就是左闭右开
与之对应的是一个语法糖s[start:stop:stop]
,他们的功能完全是差不多的,前面的例子也给出了演示。以后我们更多地会使用语法糖,而不是s.__getitem__(slice())
的调用方式。
语法糖
所谓的语法糖就是可以减少代码量的一种简便写法。python中到处都是这种语法糖,所以增加了难度,但是学会之后写代码的速度也显著地高。
下面列举一些"语法糖":
- 取值
object.__getitem__()
object[]
object[]
就是object.__getitem__
的语法糖
- 切片
object[slice(1, 10, 2)]
object[1:10:2]
1:2:3
就是slice(1,2,3)
的语法糖
- 调用
print.__call__(1)
print(1)
()
就是__call__
的语法糖- 这个东西能不能称之为语法糖有待商榷,我写在这里是想让读者明白当我们给一个对象加上
()
实际上就是在调用他的__call__
方法
- 逻辑运算
1.__gt__(2)
1 > 2
>
就是__gt__
的语法糖
- 列表
[i for i in 'hello']
list('hello')
[object]
就是list(object)
的语法糖
之所以“语法糖”加了引号,是因为一般来说上面这些东西一般来说我们不称之为语法糖,而是视作python语法的一部分。
但是我想说,如果我们不学习这些语法,而是仅仅学习object.method()
这样的调用方式,也可以完成几乎全部的工作。如此一来,python的语法就变得简单起来,不再有各种复杂的语法现象,只有a.b
这一种调用方式。
这样的学习方式不能提高写代码的速度,但是可以加深我们对类的理解:python中几乎所有的操作都是在调用某个对象的某个方法。
总而言之,这个slice的效果就是切片,把对象的指定片段取出来。举几个例子来感受一下:
"hello"[::2]
就是从开头切到最后,间隔为2,所以他的结果为hlo
."hello"[::-1]
就可以把字符串倒过来,变成olleh
"hello"[:1]
按照左闭右开原则,结果为h
"hello"[1:-2]
按照左闭右开原则,结果为el
普普通通的函数例如upper
,我们可以用help
函数来看看他的用法:
help(str.upper) | |
---|---|
例如:
其他的函数我就不再介绍了,读者可以如法炮制,用help
函数来学习。
list
python中最常用的数据类型,没有之一。它的创建语法是:[]
,用方括号括起来的东西就成为列表的元素。
例如[1, 2, 3]
,再如[1, '2', False]
。
列表对元素没有任何的要求,他们可以重复,可以类型不一,甚至可以是指向同一个对象的两个变量。
列表也有诸多的方法,dir(list)
返回的结果为:
列表也支持切片操作,切片方法和字符串完全一致。
其他
常用的类型还有int
,float
,bool
,range
,dict
,tuple
,set
等等。他们本质都是类,里面的方法和属性大差不差,读者可以自行探索。
内置函数
函数虽然也是类,但是和数据类型的学习方法大相径庭。我们主要学习的内容不是函数类上定义的属性和方法,而是函数需要传入的参数、可以实现的功能以及最终返回的结果(这些内容只需要用help
函数就可以知道了)。
下面介绍几个我们之前遇到过的函数
print
找到前面我们help(print)
的返回值:
print
函数可以传入的参数: - value:要打印的东西,可以是多个
- sep:分隔符
- end:用什么结尾
- file:打印到哪里
- flush:是否强制flush
文档并未提及print
的返回值,实际上这个函数是没有返回值的。
知道了参数和涵义之后,我们就可以来运用这些参数达成我们想要的效果了:
python title= 1&2&3&
其他常用的函数
len
:反对一个对象的长度,例如len("hello")
,运行结果为5
type
:返回一个对象的类,例如type(1)
,运行结果为<class 'int'>
其他的我就不再列举了,如果你有兴趣可以把__builtins__
里面所有的东西全部用help
和dir
查一遍,就都了然于胸了。