zoukankan      html  css  js  c++  java
  • 《Fluent Python》- 02 序列构成的数组

    Guido曾为ABC语言贡献过代码。Python也从ABC继承了用统一的风格去处理序列数据这一点。它们都共用一套丰富的操作:迭代,切片,排序,还有拼接

    深入理解Python中的不同序列类型,不但能让我们避免重新发明轮子,它们的API还能帮助我们把自己定义的API设计得跟原生的序列一样,或者和未来可能出现的序列类型保持兼容

    内置序列类型概览

    容器序列:

        list,tuple,collections.deque

    扁平序列:

        str,bytes,bytearry,memoryview和array.array

    容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列存放的是值

    可变序列:

        list,bytearray,array.array,collections.deque,memoryview

    不可变序列:

        tuple,str,bytes

    列表推导和生成器表达式

    先看两个代码:

    symbols = 'test'
    codes = []
    for symbol in symbols:
        codes.append(ord(symbol))
    print(codes)
    symbols = 'test'
    codes = [ord(symbol) for symbol in symbols ]
    print(codes)

    提问,你觉得上面的容易理解还是下面的容易理解,如果你说下面的容易理解而且是在你还没接触过的情况下,那么我只想说,fnmdp

    对于任何学过一点点Python的人,都能看懂上面的那个,而且几乎不需要什么推导,而下面这个对于刚接触的我们来说,是要推导才能理解其中的意思。

    其实对于上面那个也是一样的,我们最初学循环的时候不也需要推导吗,只就是个习惯与理解的过程罢了。

    上下两个代码的区别在哪,其实上面是简单的逻辑推导,我们是通过代码的逻辑来理解其中的含义,下面是通过代码的名称和结构来理解其中的含义。这么说你可能有点晕,我举个例子,你定义了个方法叫a(),里面是实现两个数之和的,逻辑代码也很简单就是单纯的传两个参数a,b返回a+b。我定义了个方法叫sum(),除了名称之外和你一毛一样,但是用到你的方法的人需要去看a函数的逻辑来体会a的作用,而用到我方法的人是通过名称来得知我的方法的作用。大体上就是这个意思。当然这也没有硬性规则,有的时候for循环可能确实方便,宜读一些,这个度就要自己把握了。

    最后友情提示一下,不论在for循环里的symbol还是推导式里的那个symbol其实这个名称是能改成symbols也就是和列表重名,它不会报错的,但可能会影响你的代码逻辑,尽可能不同含义的东西不要用相同的名称。

    生成器表达式

    虽然我们可以通过列表推导的方式来初始化数组等序列,但是生成器表达式是更好的选择。因为它背后是遵循了迭代器协议,可以逐个产出元素,而不是先建一个完整的列表然后把这个列表传到某个构造函数里

    其实生成器表达式的语法和列表推导差不多,就是把方括号改成圆括号

    symbols = 'test'
    tuple(ord(symbol) for symbol in symbols )
    array.array('I', (ord(symbol) for symbol in symbols))

    后面会具体讲生成器的原理。现在知道怎么用就行了(这部分其实我看了,现在还没看怎么明白,最后两部分,控制流程和元编程还是比较抽象的)

    元祖不仅仅是不可变列表

    元祖可以理解为不可变列表,也可以理解为一些字段的集合,如果这样理解,那么数量和位置信息就特别重要了。

    下面是个简单的拆包应用,一行赋值完成。最好辨认的就是平行赋值就像下面这个

    city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) # 元祖拆包

    在拆包中用*可以替代掉一堆元素,但只能用一次

    city, *test, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) # test是一个[2003, 32450] 这样的数组

    切片

    为什么切片和区间会忽略最后一个元素?

    在切片和区间范围里,不包含最后一个元素,这样的好处是:

    当最有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)和my_list[:3] 都是返回3个元素。

    当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop-start)

    这样做也让我们可以利用任意一个下标来把序列分割成不重叠的的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了

    如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代的对象。即便只有单独一个值,也要把它转换成可迭代的序列

    对序列使用 + 和 * 

    l = [1, 2, 3]
    res = l*5
    print(res)  # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
    list = [5, 3, 2]
    list += [4, 6, 1]
    print(list)  # [5, 3, 2, 4, 6, 1]

    在拼接的过程中,两个被操作的序列都不会被修改,Python会创建一个包含同样类型数据的序列来作为拼接的结果

    建立由列表组成的列表

    有时候我们可能需要嵌套列表,构造如下

    board = [['_'] * 3 for i in range(3)]
    board[1][2] = '0'
    print(board)  # [['_', '_', '_'], ['_', '_', '0'], ['_', '_', '_']]
    new_board = [['_'] * 3] * 3  # 其实是三个指向同一列表的引用
    new_board[1][2] = '0'  # 直接打印可能看不出来区别,但是修改的时候就会同时修改三个
    print(new_board)  # [['_', '_', '0'], ['_', '_', '0'], ['_', '_', '0']]

    这里犯的错误就是追加的同一对象,而非新对象

    除了 + * 还有+=  *= 。随着目标序列的可变性,这两个运算符的结果也大相庭径

    关于 += 背后的特殊方法其实是__iadd__(用于就地加法)。*= 则是 __imul__(关于这两个后面还会提及)

    对于不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新对象里,然后再追加元素。但是str是一个例外,因为对于str来说+=实在是太普遍了,CPython对其做了优化,为str初始化时会留有额外可扩展空间。

    一个关于+=的问题

    t = (1, 2, [3, 4])
    t[2] += [50, 60]

    提问,会发生什么情况

    a.t变成(1, 2, [30, 40, 50, 60])

    b.因为tuple不支持对它的元素赋值,所以抛出TypeError异常

    c.以上两个都不是

    d.ab都对

    我刚开始看这个的时候认为的是a,因为tuple是不可变的,但是里面的元素的内容有可能是可变的。这里就是我忽略了+=的背后思想

    答案是d,你可能觉得很奇怪(没错,我第一开始也觉得奇怪,既然报错了,为什么又修改掉了)

    我们来实际跑一下结果

    t = (1, 2, [3, 4])
    t[2] += [50, 60]
    # TypeError: 'tuple' object does not support item assignment
    print(t)

    不会执行到print,确实报错了,tuple是不能变的。但是我说t也变了,这个要怎么弄,在命令行里可能好弄一些,在编译器里,我们就抓异常吧:

    t = (1, 2, [3, 4])
    try:
        t[2] += [50, 60]  # 这里会报错,但是t的值其实已经变了
    except TypeError:
        print(t)  # (1, 2, [3, 4, 50, 60])
    print(t) # (1, 2, [3, 4, 50, 60])

    尽管已经报错了,但是t的值却改变了,我用的是Python3.8,这个问题依然存在

    到此,我们需要获得的教训:

    不要把可变对象放在元祖里

    增量赋值不是一个原子操作,它抛异常了,但仍完成了操作

    我对元祖的一个理解:元祖内是不可变的,但是元祖内的对象没有保证必须不可变。

    list.sort 和 内置的sorted函数

    简单来说:list.sort 就地排序会改变原来序列,而sorted则是新生成一个序列,不会改变原来的序列

    bisect

    bisect(haystack, needle) 在haystack里搜索needle的位置

    # bisect(haystack, needle)  通俗来说,在列haystack中查找needle应该存在的位置,而非needle本身,
    #                           换句话说,如果你想插入needle那么应该插入的位置就是这个函数的返回值
    #                           因为采用的是二分的方式,前提是保证haytack是有序的
    
    index = bisect(sorted_list, 10)
    print(index)

    插入:

    sorted_list.insert(index, 10)
    print(sorted_list)  # 保证了有序性
    bisect.insort(sorted_list, 18)  # 可以直接实现插入

    bisect有两个可选的参数lo和hi,简言之就是左右边界

    random_list = []
    for i in range(1000):
        random_list.append(random.randint(1, 1000))
    sorted_list = sorted(random_list)  # 随机生成了1000个1-1000的数将其排序
    index = bisect.bisect(sorted_list, 500, lo=100, hi=900)  # lo 左边界  hi 右边界,默认是整个列
    # 注:我个人不是很推荐使用lo,hi,就好比这个例子:
    #     首先用和没用跑出来的值是一样的,也就是说,lo 和 hi恰好取在了这个范围内,我搜了500,列表是随机生成的,大概率下,前100和后100是不会产生500这个数字的位置的
    #     但是在实际情况中,lo,hi 可能并不是那么好取,而且,这个优化看起来是从1000个数里查找变到了800个数字里查找
    #     但实际上,二分的效率是log(1000) 和 log(800)  ps:这里都是以2为底数的  log(1000) 和 log(800)都是9.x
    #     查找是看次数的,也就是说,查1000比查800可能多查一次,甚至一样
    #     这也就是说,这个提升是微乎其微的,几乎看不到效果的,但是你用来找lo和hi的代价是很大的
    #     除非能很大程度上缩小查找范围(按2的倍数掉)或者有循环嵌套,再是否考虑要做这个优化
    index2 = bisect.bisect(sorted_list, 500)
    print("index:", index, " index2:", index2)

    数组

    通常来说列表是既简单又灵活,但是有时候我们可能会存在更好的选择。比如,要存放1000万个浮点数的话,数组的效率要高的多,因为数组在背后存的不是float对象而是数字的机器翻译,也就是字节表述。再比如,如果需要频繁对序列做先进先出之类的操作,deque会快很多

    # array  数组,详情参考数据结构里的数组,大同小异
    floats = array.array('d', (random() for i in range(10**7)))
    # 这里 d 是指代码类型,d 指的是双精度浮点类似于double
    print(floats[-1])

    内存视图

    memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。

    numbers = array.array('h', [-2, -1, 0, 1, 2])
    memv = memoryview(numbers)
    print(len(memv))
    print(memv[0])
    memv_oct = memv.cast('B')
    print(memv_oct.tolist())
    memv_oct[5] = 4
    print(numbers)

    NumPy

    作者觉得这个很优秀专门跑题来说了(我也觉得NumPy十分优秀,尤其是它现在人工智能领域的应用),下面简单说个例子了解一下,具体还是自行了解

    a = numpy.arange(15)
    print(a)
    print(type(a))
    print(a.shape)
    a.shape = 3, 5  # 改变编排
    print(a)
    print(a[1, 1])  #  打印(1, 1)
    print(a[:, 1])  # 切列 第一列  (从0开始)
    print(a.transpose()) # 行变列,列变行

    队列,利用append和pop方法,我们可以把列表当做队列或者栈来使用。

    但是deque是一个线程安全,可以快速从两端添加或删除元素的数据类型。

    dq = deque(range(15), maxlen = 10) # maxlen 最大可包含数量,截取了后面的10个,不难理解,先进先出
    print(dq)
    dq.rotate(3) # 在3的位置旋转
    print(dq)
    # 左加右加,进出。参考数据结构双端队列
    dq.append(3)
    dq.appendleft(4)
    dq.extend([1, 1])
    dq.extendleft([2, 2])

    杂谈(非正式向)

    对于数据结构,其实还是个老生常谈的问题,我个人还是建议没学过数据结构的人好好学一下,便于你快速理解和掌握其他语言中的数据结构。

    数据结构几乎是代码中不可缺少的一部分,不论是你做开发还是做算法,都会或多或少的用到。理解这些的背后思想,能便于你更快速高效的选择数据结构。

  • 相关阅读:
    试题 E: 迷宫
    对拍程序
    人群中钻出个光头
    HDU-魔咒词典(字符串hash)
    第八集 你明明自己也生病了,却还是要陪着我
    智力问答 50倒计时
    数据结构
    LeetCode刷题 fIRST MISSING POSITIVE
    LeetCode Best to buy and sell stock
    LeetCode Rotatelmage
  • 原文地址:https://www.cnblogs.com/Moriarty-cx/p/12335022.html
Copyright © 2011-2022 走看看