Python 开发指南:基础、复合数据类型

2023年 7月 12日 29.4k 0

本章使用的 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 会将 agename 两个变量的值嵌入到 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 = 0stop = len(rs)

  • 如果 step < 0 ,则表示从右到左的顺序切片,默认值 start = -1stop = -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 时会抛出异常。
  • 调用 dictget 方法。当搜索 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)

    作者:花花子

    来源:稀土掘金

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论