• python编程导论读书笔记【4】终章


    克隆

    在python中,切片不是克隆列表的唯一办法。表达式list(L)会返回列表L的一份副本。如果大夫值得列表包含可变对象,而且你想复制这些可变对象,那么可以导入标准库模块copy,然后使用函数copy.deepcopy。、

    列表推导式

    列表推导式提供了一个简洁的方式,讲某种操作应用到序列中的一个值上。它会创建一个新的列表,其中的每个元素都是一个列表中的值(如另一个列表的元素)应用给定操作后的结果。


    L = [x** for i in range(1,7)]
    print(L)
    会输出:
    [1,4,9,16,25,36]

    高阶函数:参数本事是函数。

    python中有一个内置的高阶函数map,被设计与for循环结合使用。在map函数的最简形式中,第一个参数是个一元函数(即只有一个参数的函数),第二个参数是有序的值集合,集合中的值可以一元函数的参数。

    在for 循环中使用map 函数时,它的作用类似于range函数,为循环的每次迭代返回一个值。这些值是对第二个参数中的每个元素应用一元函数生成的。

    python 还支持创建匿名函数,这时要使用保留字lambda,lambda表达式一般形式为:lambda<sequence of variable names> : <exprsssion>

    举例来说,lambda表达式lambda x,y x*y 会返回一个函数,这个函数的返回值为两个参数的乘积。Lambda表达式经常用作高阶函数的实参。

    字符串、元组、范围与列表

    序列类型的通用操作:


    seq[i]: 返回序列中的第i个元素
    len(sep): 返回序列长度
    seq1+seq2: 返回两个序列的连接(不适用于range)
    n*seq: 返回一个重复了n次seq的序列
    seq[start:end]: 返回序列的一个切片
    e in seq:如果序列包含e,则返回True,否则返回False
    e not in seq:如果序列不包含e,则返回True,否则返回False
    for e in seq: 遍历序列中的元素

    序列类型对比:

    类型元素类型字面量示例是否可变
    str 字符型 'a','','abc'
    tuple 任意类型 (),(3,),('abc',4)
    range 整型 range(10),range(1,10,2)
    list 任意类型 [],[3],['abc',4]

    Python程序员使用列表的评率远超元组,因为列表是可变的,它们可以在计算过程中逐步构建。

    元组的一个优势在于他不可变的,所有别名对它来说不是什么问题。与列表不同,元组作为不可变对象的另一个优势是可以作为字典的键。

    字符串的使用方法:(注意,字符串是不可变的,所以这些方法都返回一个值,而不会对原字符串产生副作用)


    s.count(S1): 计算字符串s1在s中出现的次数
    s.find(s1): 返回字符串s1在s中第一次出现时的索引值,如果s1不在s中,则返回-1。
    s.rfind(si): 功能与find相同,只是从s的末尾开始反向搜索(rfind中的r表示反向)
    s.index(s1): 功能与find相同,如果s1不在s中,则抛出一个异常。
    s.rindex(s1): 功能与index相同,只是从s的末尾开始
    s.lower(): 将s中的所有大写字母转换为小写
    s.replace(old,new): 将s中出现过的所有字符串old替换为字符串new
    s.rstrip: 去掉s末尾的空白字符
    s.split(d): 使用d作为分隔符拆分字符串s,返回s的一个子字符串列表。 如果d被省略,则使用任意空白字符串拆分子字符串。

    split是比较重要的内置方法之一,它使用两个字符串作为参数。第二个字符串设定了一个分隔符,将第一个参数拆分成一系列子字符串。

    第二个参数是可选的,如果省略该参数,则使用任意空白字符(空格、制表符、换行符、回车和分页符)组成的字符串拆分第一个字符串。

    字典

    字典类型的对象与列表很类似,区别在于字典使用键对其中的值进行引用,可以将字典看作一个键/值对的集合。字典类型的字面量用大括号表示,其中的元素写法是键加冒号再加上值。

    dict的项目是无序的,不能通过索引引用。和列表一样,字典是可变的。

    并非所有对象都可以用作字典键,键必须是一个可散列类型的对象。如果一个类型具有以下两条性质,就可以说它是“可散列的”:

    1. 具有__ hash __ 方法,可以将一个这种类型的对象映射为一个int值,而且对于每一个对象,由 __ hash __返回的值在这个对象的生命周期中是不变的。

    2. 具有__ eq __ 方法,可以比较两个对象是否相等。

    所有Python内置的不可变类型都是可散列的,并且所有Python内置的可变类型都是不可散列的。

    一些常用的字典操作:


    len(d): 返回d中项目的数量
    d.keys(): 返回d中所有键的视图
    d.values():返回d中所有值的视图
    k in d: 如果k在d中,返回true
    d[k]: 返回d中键为k的项目
    d.get(k,v): 如果k在d中,则返回d[k],否则返回v
    d[k] = v: 在d中将值v与键k关联。如果已经有一个与k相关的值,则替换
    del d[k]: 从d中删除键k
    for k in d: 遍历d中的键

    测试与调试

    测试:指通过运行程序以确定它是否按照预期工作。调试是指修复已知的未按预期工作的程序。

    如果一个白盒测试套件可以测试程序中所有潜在路径,那我们就可以认为它是路径完备的。一般来说,路径完备是不可能达成的,因为这取决于程序中循环的次数和递归的深度。

    尽管白盒测试有许多局限性,但它提供的一些经验准则仍然值得我们参考

    • 测试所有if语句的所有分支

    • 必须测试每个except子句

    • 对于每个for循环,需要一下测试用例

      • 未进入循环(例如,如果使用循环遍历列表中的所有元素,则必须测试空列表)

      • 循环体只能被执行一次

      • 循环体被执行多于一次

    • 对于每个while循环:

      • 包括上面for循环中的所有用例

      • 还要包括对应于所有跳出循环方式的测试用例。例如,对于以while len(L)> 0 and not L[i]==e 开始的循环,测试用例应该包括因为len(L)不大于0和因为L[i]==e 而跳出循环的情况。

    • 对于递归函数,测试用例应该包括函数没有递归调用就返回、只执行一次递归调用和执行多次递归调用的情况。

    执行测试

    测试驱动程序,程序会自动进行一下工作:

    • 建立调用待测试程序(或单元)所需的环境

    • 使用一个预先定义的或自动生成的输入序列来调用待测试程序(或单元);

    • 保存以上调用结果

    • 坚持测试结果是否可以接受

    • 自动生成一个合适的报告

    测试桩:用来模拟待测试单元要使用的部分程序。理想情况应具有以下功能:

    • 调用者提供的环境和参数是否合理(使用不恰当的参数调用函数是很常见的错误);

    • 修改实参和全局变量,使它们符合规范

    • 返回与规范一致的值

    注: 解决测试桩工作量过大的方法,限制测试桩可以接受的参数集合并创建一个表格,在其中列出测试套件使用的每种参数组合及其对应的返回值。

    调试

    运行时错误按照以下两个维度进行分类:

    • 显性-->隐性:显性错误有明显的表现,如程序崩溃或运行时间异常长(可能永不停止)。隐性错误没有明显的表现,程序会正常结束,不出任何问题——除了给出一个错误答案。多数错误都在二者之间,一个错误是否是显性的取决于你检查程序行为的周密程度。

    • 持续——>间歇:持续性错误在程序每次使用相同的输入运行时都会发生。间歇性错误仅在某些时候出现,即使程序使用相同输入并在相同条件下运行。

    一个只是偶尔出现隐性错误的程序造成的危害总是远大于一直出错的程序。既是隐性又是间歇性的错误始终是最难发现和修复的。

    设计实验

    一般来说,最好的方法是执行二分查找。先找到代码的中间点,然后设计一个实验,确定是否因为中间点前面存在问题才导致程序出现这种症状。(当然,中间点后面也可能存在问题,但最好一次只解决一个问题。)中间点最好选在能够提供某些中间值的地方,这些中间值应该既易于检查,又能提供有价值的信息。

    如果某个中间值与你的预期不符,那么中间点之前就可能存在问题。如果中间值都没有问题,那么错误就可能在代码的后半部分的某个地方。可以一直重复这个过程,直到将存在问题的区域缩减到几行代码。

    遇到麻烦时

    • 排除常见错误

      • 以错误的顺序向函数传递实参

      • 拼写一个名称,如将大写字母写成小写

      • 变量重新初始化失败

      • 检验两个浮点数是否相等(==),而不是近似相等(请记住,浮点数的运算与学校里面的运算不一样)

      • 在应该检验对象相等(如id(L1)==id(L2))的时候,检验值相等(例如,使用表达式L1==L2比较两个列表)

      • 忘记一些内置函数具有副作用;

      • 忘记使用()将对function类型对象的引用转换为函数调用

      • 意外的创建了一个别名

      • 其他一些你常犯的错误

    • 不要问自己为什么程序没有按照你的想法去做,而是要问自己程序为什么像现在这样做。

    • 记住,错误可能在你不认为会出错的地方。

    • 试着向其他人解释程序的问题

    • 不要盲目相信任何书面上的东西

    • 暂停调试,开始写文档

    • 出去散散步,明天接着做。

    异常与断言

    最常见的异常类型:TypeError、IndexError、NameError和ValueError。

    程序因为一个异常被抛出而终止时,我们称抛出了一个未处理异常。

    将异常用作控制流

    在很多编程语言中,处理错误的标准方法是使函数返回一个特定值(与python中的None很相似)来表示出现错误。每次函数调用必须检查返回值是否是这个特定值。在python中,更常见的做法是,当函数不能返回一个符合规格说明的结果时,就抛出一个异常。

    python中的raise语句可以控制引发一个特定的异常。raise语句的形式如下:

    raise exceptionName(arguments) 通常是一个内置的异常。

    断言

    assert 语言有以下两种形式:

    assert Boolean expression 或者 assert Boolean expression,argument

    执行assert语句时,先对布尔表达式求值。如果值为True,程序就愉快地继续向下执行;如果值为false,就抛出一个AssertionError异常。

    断言是一种非常有用的防御性编程工具,可以用来确保函数参数具有恰当的类型。它同时也是一种非常有用的调试工具,可以确保中间值符合预期,或者确保函数返回一个可接受的值。

    面向对象编程

    面向对象编程的关键是将对象看作数据和可以在数据上执行的方法的集合。

    抽象数据类型与类

    抽象数据类型是一个由对象以及对象上的操作组成的集合,对象和操作被捆绑为一个整体,可以从程序的一个部分传递到另一个部分。

    接口建立了一个抽象边界,将程序的其他部分与实现类型抽象的数据结构、算法和代码隔离开来。

    编程时,要使程序易于修改,以控制程序复杂度。有两个强大的编程机制可以完成这个任务:分解和抽象。

    抽象的关键是隐藏合适的细节,这就是数据抽象的根本目标。

    类支持两种操作:

    • 实例化,创建类的实例。

    • 属性引用:通过点标记法访问与类关联的属性。

    对一个抽象类型的实现需要以下几部分:

    • 类型方法的实现

    • 能够整体表示类型值的数据结构

    • 关于方法实现如何使用数据结构的约定。一个关键的约定由表示不变性给出。

    表示不变性定义了数据属性中的哪个值对应着类示例的有效表示。

    使用抽象数据类型设计程序

    可重用抽象的使用不但能减少开发时间,一般程序还能提高程序的可靠性,因为成熟的软件通常比新软件更可靠。

    继承

    继承提供了一种方便的机制,可以建立一组彼此相关的抽象。它使程序员能够建立一个类型的层次结构,其中每个类型都可以从上层的类型继承属性。

    除了继承属性之外,子类还可以做如下事情:

    • 添加新的属性。

    • 覆盖——也就是替换——超类中的属性。如果一个方法被覆盖,那么调用这个方法时使用的版本就要根据调用这个方法的对象来确定,如果这个对象的类型是子类,那么就使用定义在子类的方法版本;如果对象的类型是超类,那么就使用超类中的版本。

    替换原则

    使用子类定义一个类型的层次结构时,子类应该被看做对超类行为的扩张,这种扩展是通过添加新属性或对继承自超类的属性来实现的。

    封装与信息隐藏

    面向对象编程的核心思想有两个重要概念:封装和信息隐藏

    有些编程语言(如Java和C++)提供了强制隐藏信息的机制,程序员可以使用类的属性成为私有,这样类的客户代码只能通过对象方法访问数据。在python3中,可以使用命名惯例使属性在类之外不可见。当一个属性的名称以 __ 开头但不以 __ 结束时,这个属性在类外就是不可见的。

    请注意:一个子类想使用其超类中的隐藏属性时,会产生一个AttributeError异常,这使得在Python中实现信息隐藏有一点麻烦。

    静态语义检查相对较弱是python的一个缺点,但并不致命。一个训练有素的程序员会自觉遵守这条合理的规则,即不在类的客户代码中直接访问类的数据属性。

    算法复杂度简介

    背景:编写高效程序并不容易,最简单直接的方法一般都不是最有效率的。有效的算法一般会使用一些巧妙的技巧,这使得它们非常难以理解。结果经常就是,程序员努力的减少了技术复杂度,却增加了概念复杂度。

    例如:考虑以下代码中实现的线性搜索算法:


    def lineSearch(L,x):
    for e in L :
      if e==x:
      return True
    return False

    一般而言,我们需要考虑三种常见的情形:

    最佳情形:运行时间是在最有利的情况下算法的运行时间。也就是说,在给定输入规模的情况下运行的最短时间,对lineSearch而言,最佳情形与L的大小无关。

    最差情形运动时间是在给定输入规模的情况下最长的运行时间。对于lineSearch,最差情形运行时间与L的大小成正比。

    平均情形(也称为期望情形)运行时间是在给定输入规模的情况下的平均运行时间。此外,关于输入值的分布有一些鲜艳信息。(例如,在90%的情形下,x在L中),也应该将这些信息考虑在内。

    渐近表示法

    我们用渐近表示法讨论运算时间与输入规模之间的关系。作为一种对“特别大”的表示方法,渐近表示法描述了输入规模趋近于无穷大时的运算复杂度。

    我们可以用以下规则描述算法的渐近复杂度:

    • 如果运行时间是一个多项式的和,那么保留增长速度最快的项,去掉其他项

    • 如果剩下的项是个乘积,那么去掉所有常数。

    最常用的渐近表示法称为“大O”表示法。大O表示法可以给出一个函数渐近增长(通常称为增长级数)的上界。

    如果我们说f(x)的复杂度是O(x^2),那么其实是在暗示x^2既是渐近最差情形运行时间的上界,也是其下界。被称为紧界。

    一些重要的复杂度

    下面列出了一些最常用的大O表示法实例。n表示函数的输入规模。

    • O(1)表示常数运行时间。

    • O(logn)表示对数运行时间。

    • O(n)表示线性运行时间

    • O(nlogn)表示对数线性运行时间

    • O(n^k)表示多项式运行时间,注意k是常数

    • O(c^n)表示指数运行时间,这时常数c为底数,复杂度为c的n次方。

    常数复杂度

    常数复杂度的意义:渐近复杂度与输入规模无关。常数运行时间并不意味着代码中没有循环或递归调用,但确实可以说明迭代和递归调用的速度与输入规模无关。

    对数复杂度

    对于这种函数的复杂度来说,它的增长速度至少是某个输入的对数。例如,二分查找的复杂度就是待搜索列表的长度的对数。顺便说一下,我们不关心对数的底数,因为对于某个对数来说,使用另一个底数的区别只是相当于原来底数的对数乘以一个常数。

    代码中只有一个循环,所以我们只需找出迭代次数。迭代次数就是一直用i除以10做整数除法,在结果为0之前能够做的整数除法的次数。

    线性复杂度

    很多处理列表或者其他类型程序具有线性复杂度,因为它们对序列中的每个元素都进行常数(大于0)次处理。

    与时间复杂度不同,要想感觉到空间复杂度的影响比较困难。这就是时间复杂度通过对空间复杂度更受关注的原因。运行程序所需的存储空间超过了计算机内存时,空间复杂度才能更受关注。

    对数线性复杂度

    它是两个项的乘积,每个项都依赖于输入的规模。这个复杂度非常重要,因为很多实用算法的复杂度都是对数线性的。最常用的对数线性复杂度算法可能是归并排序法,它的复杂度是O(nlog(n)),这里的n是待排序列的长度。

    多项式复杂度

    最常见的多项式算法复杂度是评分复杂度,也就是说,算法复杂度按照输入规模的平方增长。

    一些简单算法和数据结构

    高效算法的实现非常困难。我们要做的是,学会在面对问题时将复杂性减到最少,并将它们转换成以前已解决的问题。

    • 理解问题的内在复杂度

    • 思考如何将问题分解成多个子问题。

    • 将这些子问题与已经有高效算法的其他问题联系起来。

    一般来说,比较好的策略是先用最简单直接的方式解决手头的问题,再仔细测试找出计算上的瓶颈,然后仔细研究造成瓶颈的那部分程序,并找出改善计算复杂度的方法。

    搜索算法

    搜索算法就是在项目集合中找出一个或一组具有某种特点的项目。我们将项目集合称为搜索空间。在实际工作中,大量问题都可以转换为搜索问题。

    三种搜索可行解空间的算法:穷举法、二分查找法和牛顿--拉佛森法。

    线性搜索与间接应用元素

    计算机中的最重要的实现技术之一:间接引用。

    一般来说,间接引用就是要访问目标元素时,先访问另一个元素,在通过包含在这个元素汇总的引用访问目标元素。我们每次使用变量引用与变量绑定的对象时,就是这么做的。当我们使用一个变量访问列表并使用保存在列表中的引用访问另一个对象时,实际上进行了双重间接引用。

    二分查找和利用假设

    二分查找的思路非常简单:

    • 选择一个可以将列表L大致一分为二的索引i

    • 检查是否有L[i]==e

    • 如果不是,检查L[i]大于还是小于e;

    • 根据上一步的结果,确定在L的左半部分还是右半部分搜索e。

    二分查找的最简单直接的方式就是使用递归。

    递减函数

    1. 它可以将形参绑定的值映射成一个非负整数

    2. 当它的值为o时,递归结束

    3. 对于每次递归调用,递减函数的值都会小于作出调用函数实例中的递减函数的值。

    排序算法

    选择排序的工作原理是维持一个循环不变式,它会将列表分成前缀部分(L[0:i])和后缀部分(L[i+1:len(L)]),前缀部分已经排好序,而且其中的每一个元素都不大于后缀部分的最小元素。

    归并排序

    分治算法具有以下特征:

    • 一个输入规模的阀值,低于这个阀值的问题不会进行分解

    • 一个实例分解成子实例的规模和数量

    • 合并子解的算法。

    阀值有时被称为递归基。

    归并排序是一种典型的分治算法。用递归方式描述它是最容易的:

    1. 如果列表的长度是0或1,那么它已经排好序了;

    2. 如果列表包含多于1个元素,就将其分成两个列表,分别使用归并排序法进行排序;

    3. 合并结果

    Python中的排序

    list.sort方法和sorted函数都可以有两个附加参数。参数key的作用和我们实现归并排序的compare一样的:提供用于排序的比较函数。参数reverse指定对列表进行升序还是降序排列,升序和降序都是相对于函数来说的。

    绘图以及类的进一步扩展

    使用PyLab绘图

    PyLab是一个python标准库模块,提供了MATLAB的很多功能。


    from matplotlib import pylab
    pylab.figure(1)             #创建图1
    pylab.plot([1,2,3,4],[1,7,3,5])          #在图1上绘图
    pylab.show()                             #在屏幕上显示

    pylab.plot中的两个参数必须是同样长度的序列。第一个参数的所有点代表x坐标,第二个参数指定Y轴坐标。两个参数一起提供了一个序列,其中4个坐标对:[(1,1),(2,7),(3,3),(4,5)]。这些点依次绘制在图中,绘制每个点时,都用一条直线与前面的点相连。

    ***pylab.show()会挂起python进程,直到图形被关掉。常规的避免方式是确保pylab.show()是最后一行可执行代码。


    from matplotlib import pylab
    pylab.figure(1)             #创建图1
    pylab.plot([1,2,3,4],[1,2,3,4])          #在图1上绘图
    pylab.figure(2)                          #创建图2
    pylab.plot([1,4,3,2],[5,6,7,8])          #在图2上绘图
    pylab.savefig('Figure_Addie')            #保存图2
    pylab.figure(1)             #回到图1
    pylab.polt([5,6,10,3])    #继续在图1上绘图
    pylab.savefig('Figure_Jane')             #保存图1

    会生成两张.png的图片

    最后一个pylab.plot只使用了一个参数,这个参数提供了Y值,相应的X值默认由range(len([5,6,10,3]))产生的序列。在本例子中为0-3的整数。

    pylab.plot(values,'ko')表示用黑色原点绘制图片。

    pylab.rcParams['lines.linewidth']=6,将默认线宽设置为6点。

    动态规划

    动态规划适用于解决具有重复子问题和最优子结构的问题。

    函数第一次调用结果保存下来,然后在需要的时候直接查找,而不是重新计算,这种方法叫备忘录法,是动态规划的核心思想。

    基于备忘录法的斐波那契函数:


    def fastFIb(n, memo={}):
    #假设n是非负整数,memo只进行递归调用返回第n个斐波那契数
      if n == 0 or n == 1:
          return 1
      try:
          return memo[n]
      except KeyError:
          result = fastFIb(n-1)+fastFIb(n-2)
          memo[n] = result
          return result

    动态规划与分治算法

    分治算法的基础是找到规模远小于初始问题的子问题。

    动态规划解决的子问题的规模只稍稍小于初始问题。

    分支算法的小蓝不取决于算法结构,所以同样的问题会被重复解决。相比之下,只有在不同子问题的数量远远小于所有子问题的数量时,动态规划才有效率的。

    负重前行
  • 相关阅读:
    AHOI2012 信号塔 | 最小圆覆盖模板
    BZOJ1337 最小圆覆盖
    HAOI2014 走出金字塔
    HAOI2012 外星人
    HAOI2014 遥感监测
    HAOI2012 道路
    NOI2007 社交网络
    HAOI2012 高速公路
    HAOI2012 容易题
    HAOI2011 Problem c
  • 原文地址:https://www.cnblogs.com/astride/p/11225584.html
Copyright © 2020-2023  润新知