本章使用的 Python 版本是 3.8。
Python 对代码的书写格式制定了各种规范,它们被收录在了 Python Enhancement Proposals ( PEP ) 中。不过,随着学习的进行,你自然会适应并遵守这些书写格式,因此这里不再赘述。在 PyCharm 当中,你可以使用 Ctrl + Alt + L 快速规范代码书写。
基础数据类型
数值型
这里仅需简单地将数值分为三种类型:整型 int
,浮点数 float
,布尔值 bool
,复数 complex
。其中,浮点数不区分单精度和双精度。Python 是一个动态类型语言,所有的变量都是动态确定类型的。可以使用 type()
函数确定一个变量当前的类型。如:
#
x = 1.00
# python 只内置 print 进行控制台输出,默认情况下自带回车。
print(type(x))
#
x = 1
print(type(x))
# bool: True, False
#
x = True
print(type(x))
#
x = 3 + 2j
print(type(x))
在这个例子中,打印了四次变量 x
的数据类型,且每一次 x
的类型都不同。可以通过 :type
的方式主动声明变量的数据类型,但事实上并不会影响脚本的执行。
x: int = 10
x = "hello"
print(x, end="\n")
Python 会自动处理数值计算的精度转换。比如说:
print(1/2)
程序输出的结果将是 0.5
,而非 0
。然而,Python 提供了 int()
,float()
,str()
,complex()
等类型转换函数,可以实现强制类型转换的效果。下面的输出将是 0
:
print(int(1/2))
字符串
Python 的字符串类型为 str
。无论是使用 ''
或者 ""
包括的文本都可以被认为是字符串。如:
h = "hello" # str
w = 'world' # str
print(h, w)
可以使用三引号的形式表示一段文本块 ( 仍然属于 str
类型 ),它的另一个用法是做脚本内的长文注释。如:
"""
2022/6/17
author: Me
This is the first python script written by myself.
you can use text block as the code description.
"""
print("hello world")
Python 的 str
有两个实用的运算符重载。其中,+
操作符可以简单地将两个字符串拼接起来,而 *
操作符可以令字符串自身重复拼接。
x = "hello"
print(x + "world") # helloworld
print(x * 2) # hellohello
注,字符串在 Python 中可被视作由单个字符组成的字符列表 list。后文在列表中介绍的所有操作同样适用于字符串。
Python 有另外一种嵌入拼接的字符串模板写法,如:
age = 18
name = "me"
info = f"""
studentInfo:{age}
name: {name}
"""
字符串前面的 f
代表 format。Python 会将 age
和 name
两个变量的值嵌入到 info
字符串内部。
复合数据类型
列表 list 与区间 range
列表 list
是最常用的线性数据结构,使用 []
声明。Python 不要求一个列表下的所有元素都保持同一类型。比如:
xs = [1, "2", 3, 4.00, 5]
# len() 是 Python 的内置函数,可以打印列表的长度。
print(len(xs))
可以通过列表嵌套的形式生成高维列表。不过,我们更倾向于使用 numpy
库去生成高维数组 ( 或称矩阵 ),后者在数值运算中的性能更高。
xxs = [[1, 2, 3], [3, 4, 5]]
print(xxs)
在 Python 中,可以使用 0
起始的非负下标 n
表示列表中从左到右数的第 n + 1
个位置,以 -1
起始的负数下标 -m
表示列表中从右到左的第 m
个位置。比如:
xs = [1, "2", 3, 4.00, 5]
p1 = xs[-2] # 4.00
p2 = xs[2] # 3
在 Python 中,这种
x[0]
下标访问的底层指向__getitem__()
方法,它本质上是操作符重载的一种。
列表内的元素引用是可更改的。比如:
xs = [1, 2, 3]
xs[2] = 4
print(xs) # [1, 2, 4]
列表可以像字符串那样使用 +
操作符拼接,或者是使用 *
操作符重复。
xs = [1, 2, 3, 4] * 2
ys = [1, 2, 3, 4] + [5, 6, 7, 8]
print(xs) # [1, 2, 3, 4, 1, 2, 3, 4]
print(ys) # [1, 2, 3, 4, 5, 6, 7, 8]
利用这个特性可以快速生成一个元素初值为 i
,长度为 n
的列表。如:
i = 0
n = 10
xs = [i] * n
print(*xs)
遍历列表是最常见的程序逻辑。在 Python 中可表示为:
for x in xs:
print(x)
如果 xs
是一个对象列表,则在每次迭代中,Python 会以 引用拷贝 的形式将列表元素提取给临时变量 x
。换句话说,如果在循环体内修改了 x
的引用,那么后续对它的状态修改将不会传递到原列表,因为引用共享关系被破坏掉了。比如:
# 这个对象有一个值 v
class Foo:
def __init__(self, v_):
self.v = v_
xs = [Foo(1)]
for x in xs:
# 破坏引用共享
x = Foo(2)
x.v = 3
# 1, not 2 or 3
print(xs[0].v)
在不破坏共享引用的情况下,对 x
的内部状态的修改会传递到原列表。比如:
# 这个对象有一个值 v
class Foo:
def __init__(self, v_):
self.v = v_
xs = [Foo(1)]
for x in xs:
x.v = 2
# 2.
print(xs[0].v)
在后文介绍的切片中也会有类似的现象。与之相对的是,数值类型 ( 包括 str
) 都是 不可变 的。此时对 x
做何修改都不会传递到原列表。
xs = [1, 2, 3, 4, 5]
# 试图通过这种方式将 xs 内的数值 x 全部映射成 2x
for x in xs:
x = x * 2
# 仍然打印 [1, 2, 3, 4, 5]
print(*xs)
如果要以简明的形式实现 list → list 的映射,可以参考后文的推导式来完成,而不是绞尽脑汁思考如何复现
for(i=0;i 由于 0 下标的存在,数组的最后一个下标是其长度 -1.
# stop: -1 -> 遍历到 -1 下标之前,即 0 号下标。
# step: -1 -> 每次迭代下标 -1.
for x in range(len(xs) - 1, -1, -1):
print(xs[x])
Python 内置了一个返回 逆序迭代器 的函数:
recersed()
。sx = reversed("hello") # 字符串也是列表的一种 s = "".join([x for x in sx]) # 见后文的生成式 # 切片形式的最简化版本: sx = "hello"[::-1]
区间
range
和列表list
是两个不同的类型,可以通过type
函数检查出区别。range
可以被视作一种 抽象的不可变列表,因此它也可以被迭代,但是range
类型不提供下标索引方式的访问。如:xs = range(10) xs[1] = -1 # don't do this
如果想利用区间生成列表,可以使用
list()
函数进行转换。切片
切片是基于列表 ( 或区间 ) 截取出的子序列 ( 或子区间 ),并不是一个独立的数据类型。比如,下面的代码表示从
xs
的[2,4)
下标位置截取出切片:xs = [1, 2, 3, 4, 5] ss = xs[2:4] # [3,4]
切片同样可以
[start:stop:step]
的顺序指定步长。其中start 0
,则表示从左到右的顺序切片,默认值start = 0
,stop = len(rs)
。如果 step < 0
,则表示从右到左的顺序切片,默认值start = -1
,stop = -len(rs)-1
。因此,切片有非常灵活的声明方式,以下写法均成立:
rs = range(1, 10) # [1, 2,..., 9] print(*rs[:2]) # [1, 2] print(*rs[4:]) # [5, 6..., 9] == xs[4::] print(*rs[::]) # [1, 2,..., 9] == xs print(*rs[::2]) # [1, 3, 5, 7, 9] != xs[:2] print(*rs[4::2]) # [5, 7, 9] print(*rs[4::]) # [5, 6,..., 9] == xs[4:]
其中,可以特别记忆切片
rs[::-1]
的写法,它相当于rs
的逆序排列,对于字符串同样适用。Python 是通过 引用拷贝 截取对象元素的。换句话说,对切片内元素状态的更改会发生传递。
class VV: def __init__(self, v_): self.v = v_ x = [VV(1)] y = x[:] y[0].v = 2 # 2 2 print(x[0].v, y[0].v)
想要避免这种耦合性,可以使用新的实例引用进行赋值,从而破坏掉引用共享。
class VV: def __init__(self, v_): self.v = v_ x = [VV(1)] y = x[:] y[0] = VV(2) # 1 2 print(x[0].v, y[0].v)
对于数值型的列表则不会有这样的问题,因为这里不涉及引用拷贝。
a = [1] b = a[:] b[0] = 2 # [1] [2] print(a, b)
元组 tuple
元组可被视作一个轻量的 引用不可变 数据容器,标准的写法是使用
()
声明。比如:t = (1, 2, 3) # 可以用下标索引的方式访问元素,但不可修改。 e = t[1] print(e)
基于元组可以引申出相当多的特性。比如,可以利用元组进行多重赋值,或者 理解成是元组的提取式。对于丢弃不用的元素,可以使用
_
符号简单地忽略掉。(x, y, _) = (1, 2, 3) print(x, y) # x = 1, y = 2
Python 函数也可以返回元组,或者理解成是像 Go 语言的函数一样返回了多个值。如:
def swap(x, y): return (y, x) (x,y) = swap(1,2) print(x, y) # x = 2, y = 1
Python 的元组可以省略
()
,多个元素间仅以,
相隔。上面的代码还可以简写成:def swap(x, y): return y, x a, b = swap(1, 2) print(a, b)
特殊地,如果要把单个元素视作元组,则在元素后加上
,
,比如如a,
。*集合 set
关于集合和字典部分,我们事实上是在讨论更深刻的话题:Python 对象的相等性。
集合
set
类型和列表的重要区别是:集合内的元素不会发生重复,使用{}
声明。首先,可以直接放入集合内的元素有数值,字符串,元组。比如:sets = {1, 1, 2} # len(sets) == 2, 说明重复的 1 被筛除掉了。 print(len(sets))
我们再来讨论保存对象的集合是什么样的。首先是一段代码示例:
class Foo: def __init__(self, v_): self.v = v_ ref = Foo(1) sets = {ref, ref} # len(sets) == 1 print(len(sets))
Python 内部以 计算哈希值 的方式判断元素是否重复。在默认情况下,Python 会使用对象的引用计算哈希值。显然,相同的引用必然会发生哈希碰撞。如果能理解这一点,下面的运行结果就很好解释了:两个
Foo(1)
是不同的引用,因此它们可以在同一个集合下共存。sets = {Foo(1), Foo(1)} # len(sets) == 2 print(len(sets))
然而我们更希望能构建一个值不重复的对象集合。一个有效的方案是根据实例的所有状态 ( 或称属性 ) 计算哈希值。显然易见的是:如果两个对象的状态全都相等,那它们的哈希值也必然相等,从而进一步推导出两者重复。
为此,在类定义中需要同时重写
__eq__()
和__hash__()
两个方法。class Foo: def __init__(self, v_): self.v = v_ # 官方推荐的做法是将实例的属性全部混入到一个元组中,使用元组计算哈希值。 def __hash__(self): return hash(self.v,) def __eq__(self, other): return self.v == other.v st = {Foo(1),Foo(2),Foo(1)} print(len(st)) # 集合的实际元素只有 2 个。
后文简称这样的类是可计算哈希的 hashable type。Python 规定仅重写
__eq__()
但未重写__hash__()
的类是 unhashable type,它们无法作为元素放入集合,也不能作为后文字典的key
。值得一提的是,__eq__()
函数本身还是==
操作符的重载。Python 内置了大量
__XX__
命名的内置方法或函数,它们又被称之为 "魔法" 函数。Python 依赖这些函数生成语法糖或者执行内部机制。Python 提供各种操作符进行集合基本的交并补计算,如:
A = {1, 2, 3, 4, 5} B = {3, 4, 5, 6, 7} print(A - B) # {1, 2} print(B - A) # {6, 7} print(A ^ B) # {1, 2, 6, 7}, 对称差 print(A | B) # {1, 2, 3, 4, 5, 6, 7} 并集 print(A & B) # {3, 4, 5} 交集
*字典 dictionary
字典是一个特殊的集合,它的内部存放以
key : value
表示的键值对,同样使用{}
声明,记作dict
类型。同一个字典内部,key
不会发生重复。可以充当key
的有 数值,字符串,元组,以及 hashable type,理由同集合。dictionary = {"a": "abandon", "b": "banana", "c": "clap"}
Python 提供两种方式来从
dict
字典中获取value
值:以下标索引的方式访问。不过,字典中在查询不到指定 key
时会抛出异常。调用 dict
的get
方法。当搜索key
失败时以第二个参数提供的默认值进行代替,是更加安全的访问方式。dictionary = {"a": "abandon", "b": "banana", "c": "clap"} maybeKey = dictionary["d"] # error """ 这里有一个特殊的细节。不要这么做: dictionary.get("d", default="Nil") 这里的 get 方法是 C 语言层面实现的 (为了更高的性能), 而它不兼容 default=xxx 这样的传参方式。 """ getNilIfNull = dictionary.get("d","Nil")
可以使用
in
关键字查询字典内是否有某个key
值,这个关键字后文还会给出进一步说明。dictionary = {"a": "abandon", "b": "banana", "c": "clap"} boolean = "d" in dictionary print(boolean) # False
可以使用
del
关键字删除字典内的指定键值对。如果这个key
并不存在,则会抛出异常。dictionary = {"a": "abandon", "b": "banana", "c": "clap"} del_key = "c" if del_key in dictionary: del dictionary["c"] print(dictionary)
可以用另一个
dict
更新当前的字典。原字典中已存在的键值对会被覆盖,不存在的键值对将会添加。如:dictionary = {"a": "abandon", "b": "banana", "c": "clap"} dictionary.update({"a": "abuse", "d": "desk"}) # {'a': 'abuse', 'b': 'banana', 'c': 'clap', 'd': 'desk'} print(dictionary)
字典可以使用
for
循环遍历。在下面的例子中,字典中每个键 key 被提取到了临时变量k
。比如:dictionary = {"a": "abandon", "b": "banana", "c": "clap"} for k in dictionary: print(dictionary[k], end=",")
*推导式
推导式是 Python 独特的,用于对复合数据类型进行映射的有效方式之一。比如:
xs = [1, 2, 3, 4, 5] """ xs.map{x => 2*x} """ x2s = [2*x for x in xs] print(x2s)
上述的代码相当于对
xs
做了一步映射 ( map ) 操作:首先通过for
表达式提取出xs
的每一个值,经变换后收集到一个新的列表中去。再进一步,如果在推导式内安插
if
守卫,这个推导式还将可以实现对xs
的过滤 ( fitler ) 操作。如:xs = [1, 2, 3, 4, 5] """ xs.filter{_ % 2 == 0}.map{_ * 2} """ x2s = [2*i for i in xs if i % 2 == 0] print(x2s)
推导式适用于列表,区间,元组,集合,字典。比如,生成一连串的键值对:
words = ["hello", "world", "python"] # {'h': 'hello', 'w': 'world', 'p': 'python'} k = {w[0]: w for w in words} print(k)
列表推导式可以嵌套表达。比如:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # seq2d -> row -> num def flatten(seq2d): return [num for row in seq2d for num in row] # 1 2 3 ... 8 9 xs = flatten(matrix) print(*xs)
作者:花花子
来源:稀土掘金