注释

// 单行注释

/*
多行注释
*/

/**
 * JavaDoc:文档注释
 * 
 * 
 */

标识符

类名变量名方法名都被称为标识符

  • 关键字不能作为标识符
  • 所有的标识符都必须以字母、$或下划线_开始;首字符之后还可以选择数字作为字符。
  • 标识符中不能使用#*-等字符
  • 标识符是大小写敏感

数据类型

  • 强类型语言

    要求变量的使用严格符合规定,变量都必须先定义再使用。Java就是强类型语言。

  • 弱类型语言

    JavaScript就是弱类型语言。


Java的数据类型分为两大类

基本类型(primitive type, 8个)

  • 数值类型

    • 整数类型

      整数类型 大小 表示范围
      byte 8位 -128($-2^7$) ~ 127($2^7-1$)
      short 16位 $(-2^{15})$~$(2^{15}-1)$
      int 32位 $(-2^{31})$~$(2^{31}-1)$
      long 64位 $(-2^{63})$~$(2^{63}-1)$
      • long类型的变量,赋值时需要在最后加一个L,便于和int类型区分。(不加L也能通过编译,但是可读性差小写的l辨识度低,可读性也差)
      • float类型的变量,赋值时必须在最后加一个F
    • 浮点数类型

      浮点类型 大小
      float 32位
      double 64位

      最好完全不要使用浮点数进行比较,因为浮点数是有限位的、离散的、有舍入误差的数据类型。

      那么银行类业务应该用什么数据类型表示钱?——数学工具类 BigDecimal

    • 字符类型

      char,16位,赋值使用单引号''字符串String不是关键字,而是一个类,赋值使用双引号""

    long num1 = 30L; 
    float num2 = 3.14F;
    char c = 'A';
    String str = "AS";

    JDK7的特性:赋值时,可以用下划线_分割过长的数字(整数&浮点数均可),提高可读性

  • 布尔类型 boolean

    1位,值为true/false,由所占空间大小可知,**实际是使用(1/0)**来表示。


引用类型(reference type)

    • String

      • 判断字符串是否相等

        str1.equals(str2)重写了继承自Object的方法

        而不是通过==来判断,因为String是引用类型中的==作用于对象实际比较的是地址

      •   String str = "test" // 实际等价于 String str = new String("test");
          
          	
          		**不用创建对象的写法可以看作Java为了方便的设计,并不意味着String类是和char一样的基本类型**。
        
        - 接口
        
        - 数组
        
        ---
        
        ### 数据类型扩展
        
        #### 不同进制整数的特殊表示方法
        
        - **二**进制:**0b**开头
        - **八**进制:**0**开头
        - **十六**进制:**0x**开头
        
        ```java
        int num1 = 0b101; // 0b开头,2进制
        int i = 10;
        int i2 = 010; // 0开头,8进制
        int i3 = 0x10F; // 0x开头,16进制

字符转换成数字后的输出

char c1 = 'a';
char c2 = '\u0061' // \u为转义字符 Unicode,后跟4位16进制的数字,有对应的字符
    
System.out.println(c1);
system.out.println((int)c1); // 强制类型转换
System.out。println(c2);

类型转换

由于Java是强类型语言,所以进行某些运算的时候,需要用到类型转换。


基本数据类型的优先级

// 低 --------------------------------------------> 高
byte, short, char, int, long, float, double
  • 虽然float是32位表示,long是64位表示,但浮点数优先级就是比整数高
  • 布尔类型不能进行强制转换
  • 对象类型不能转换为不相干的类型
  • 类型转换可能存在内存溢出或者精度问题

字符串连接符

String类型的操作数**+另外类型的操作数,会将其他类型的操作数都转换成Stirng类型**后再连接。

System.out.println("" + 2 + 3); // 输出23
System.out.println(2 + 3 + ""); // 输出5

变量、常量、作用域

变量 Variable

// 数据类型 变量名 = 值
type varName [=value] [{, varName[=value]}];
  • 可以使用逗号隔开来声明多个同类型的变量,但是不建议这样操作(后续查找、修改的操作会变得麻烦,可读性差)。

  • 变量的默认初始化

    • 基本数据类型:

      • 整数类型:0
      • 浮点数类型:0.0
      • 字符类型:’\u0000’
      • 布尔类型:false
    • 引用数据类型:

      默认初始化为空值null,如String。


作用域

  • 类变量:从属于类,使用关键字static修饰
  • 实例变量:从属于对象,需要创建出对象后,才能使用
  • 局部变量:位于方法中

e.g. 见Demo1.java文件


常量 Constant

  • 初始化后不能更改,使用关键字final修饰;

修饰符不存在先后顺序

e.g. 以下两种写法等效

public static final double PI = 3.14;
static public final double PI = 3.14;

变量和常量的命名规范

  • 变量

    • 首字母小写驼峰原则:monthSalary
    • 方法名的规范同变量名
    • 类名要求首字母大写和驼峰原则
  • 常量

    常量一般用大写字母和下划线命名,便于和变量区分,提高可读性;


Math类

很多运算,我们会使用一些工具类来操作。

  • pow(double a, double b)

    返回一个结果为$a^b$的double值


运算符

位运算符

位运算与普通的四则运算相比执行效率高很多,因为位运算是直接以计算机底层的运行机制为基础操作的。

  • &

  • |

  • ~ 取反

  • 移位运算符

    • << 左移

      每移1位,等效于*2

    • >> 右移

      每移1位,等效于/2


三元运算符x?y:z

如果x == true,结果为y,否则结果为z


包机制

为了更好地组织类,Java提供了包机制,用于区别类名的命名空间包的本质 == 文件夹

  • 一般利用公司域名倒置作为包名

    com.hunter

  • 包语句的语法格式

    package package1[.pkg2[.pkg3...]];

    必须是类文件中的首句

  • 为了能够使用某一个包的成员,需要在Java文件中使用import语句明确导入该包

    import package1[.pkg2...].(classname|*); // *是通配符,导入指定包下所有的类
  • 尽量不要让不同包下的类名重复

    同一个类文件中,无法使用位于不同包下相同类名的类(会报错)


JavaDoc生成文档

  • javadoc命令是用来生成API文档

  • 文档注释

    /**
    *
    * 
    */
    • 加在类上就是类注释
    • 加在方法上就是方法注释
  • 参数信息

    • @auther 作者名
    • @version 项目版本号
    • @since 指明需要最早使用的jdk版本
    • @param 参数名
    • @return 返回值情况
    • @throw 异常抛出情况

卸载JDK

  1. 删除Java的安装目录及其下所有文件
  2. 删除系统环境变量 - JAVA_HOME
  3. 删除系统环境变量Path下关于Java的目录
  4. 通过java -version命令,验证是否成功卸载(报错即成功卸载)

安装JDK

  1. 下载JDK 8
  2. 双击安装JDK
  3. 记住安装的路径

配置环境变量

  1. 我的电脑 - 右键 - 属性 - 高级系统设置 - 环境变量

  2. 系统变量 - 新建

    1. 变量名:JAVA_HOME
    2. 变量值:C:\Program Files\Java\jdk1.8.0_201
  3. 系统变量 - Path - 编辑 - 新建

    • 输入%JAVA_HOME%\bin%%的作用:引用路径
    • 输入%JAVA_HOME%\jre\bin

    确定,保存。

  4. 测试JDK是否安装成功

    打开命令行,输入java -version


Hello World 及 简单语法规则

  1. 创建一个java文件

    • 文件后缀名为.java
  2. 编写代码

    public class Hello {
        public static void main(String[] args) {
             System.out.print("Hello, World!");
        }
    }
  3. 编译java文件

    javac xxx.java,会生成一个class文件

  4. 运行class文件

    java xxx(不写文件类型后缀)

注意事项

  1. Java是严格区分大小写
  2. 文件名类名必须保持一致,并且首字母大写

Java程序运行机制


使用IDEA

什么是IDE

IDE(Integrated Development Environment,集成开发环境)


IDEA 使用技巧

如何管理邮件

在**收件箱(inbox)**创建3个子文件夹:

  • Action 1.【行动】

    存放需要我做出执行的邮件。

    在其中将邮件按紧急程度归类。

    • 紧急(需当天执行
    • 不紧急
    • 他人响应后处理

    按类别排序,可以让你按不同的紧急程度来浏览处理邮件

  • Read【阅读】

    需要花时间去仔细阅读,但不需要我做出回复

  • Archive【存档】

    • 涉及基础需要的邮件
    • 不想删除的邮件

    通过会话的排序规则来整理这个文件夹,能对这些邮件沟通有一个宏观的回顾,同时隔离掉相同的邮件会话内容


创建类别

类别能将邮件、任务、日历、会议等功能都实现分组。(类别不只对应邮件

创建类别的方式:

  1. 导航栏 - 类别 - 管理类别

  2. 选中红色类别,重命名为紧急

  3. 选中橙色类别,重命名为不紧急

  4. 选中蓝色类别,重命名为他人响应后处理


创建和运用快速步骤

拖曳的方式移动邮件、邮件,过于低效累人。

利用快速步骤(Quick Steps),能够快速完成邮件的整理,从而易于思考和行动。

快速步骤

点击快速步骤面板右下角的小箭头,对快速步骤进行管理设置。

移动邮件

  1. 选择移动邮件(move to ?) - 编辑 - 命名为【存档】
  2. 选中移至文件夹指令,再选中已创建的【存档】文件夹
  3. 想把【存档】文件夹里的邮件标记为已读,所以选中已读标记
  4. 添加动作 - Clear Categories)
  5. 添加动作 - Clear flags on message
  6. 保存

删除其余用不到的快速步骤


创建新的快速步骤执行指令

  • 新建 - xxx - 命名为【行动 - 紧急
  • 换一个图标,如惊叹号
  • 选中移至文件夹指令,选中【行动】文件夹
  • 添加动作 - Flag Message - today
  • 添加动作 - categories message - 紧急
  • 完成

以此类推,【行动 - 不紧急】:

  • 把图标换为旗帜
  • 添加动作 - Flag Message - this week
  • 添加动作 - categories message - 不紧急

【行动 - 他人响应后处理】:

  • 把图标换为信息(一个i)
  • 添加动作 - Flag Message - no date
  • 添加动作 - categories message - 他人响应后处理

【阅读】

  • 把图标换为书本(一个i)
  • 不用添加动作

【行动】文件夹里各类别邮件的日程安排

当通过上述操作整理好邮件后,就能真正专注于处理【行动】文件夹中的邮件。

例如有些邮件是不紧急的,但接下来几个小时里我无法跟进,就需要安排进我的日程中,从而避免忘记或影响当前的工作

将邮件转为日历只需拖住该封邮件到日历选项即可。

  • 单击导航栏 - 计划(scheduling)
  • 单击 自动选取下一个(autopick next)按钮,让我能在我的日程中快速找到下一个空闲的时间段,保存并关闭。

定制outlook的启动文件夹视图

如果想始终将精力放在最重要的事情上,最好的方法就是定制outlook视图。就是先看到、处理【行动】文件夹里的内容。

文件 - 选项 - 高级 - outlook启动和退出 - 在此文件夹中启动outlook - 浏览 - 找到【行动】文件夹,确定即可。


处理邮件的4D决策模型

理想状态下,阅读邮件的时间安排:

  1. 早上 9:00 - 10:00 (1个小时)
  2. 下午1:30 - 2:00 (半个小时)
  3. 当天工作结束后,半个小时

大多数人好奇或习惯不停查看收件箱,但这一行为会让自己变得非常低效,因为无法专注在优先级的工作上。这意味着,每个时间段处理邮件需要快速做出决策

4D决策模型:

  1. Delete it 删除(归档)

  2. Do it 执行

    2分钟问题(2-minute drill),针对当前邮件我需要采取的行动在两分钟内完成吗?

    如果可以,就立即执行。如果能在两分钟内完成任务,就不需要关闭电子邮件或退出outlook

  3. Delegate it 委派

    2-minute drill答案为否时,能转发给一个合适的团队成员来处理吗?

    如果是,就花两分钟时间撰写要求、情况并转发出去。然后就可以将邮件归档以便追踪

  4. Defer it 推迟

    上述3D都无法完成,这意味着邮件中要求的工作只有自己才能完成,而且需要2分钟以上的时间来处理的事务 。那么就需要推迟它,在查阅处理完邮件再去执行它。进行推迟操作时,记得将这些讯息用相应的快速步骤标记类别到【行动】文件夹。

每天运用4D决策模型,就能很容易、快速地完成大量的邮件处理


创建邮件规则

处理灰色邮件(广告、垃圾邮件)

……


摘取自:借助Outlook最大化提升你的个人工作生产力①_腾讯视频

求三位数组合

已知有一个list,里面有4个数字,分别是3,6,2,7 ,这四个数字能组成多少个互不相同且无重复数字的三位数?比如362算一个,326算一个,请逐个输出他们。


分析

  1. 三层嵌套循环,每一层循环从list中取出一个数字,这三个数字组成一个三位数
  2. 需要对取出来的三个数字进行去重

解答

#coding=utf-8
lst = [3,6,2,7]
number_lst = []
for i in lst:
   for j in lst:
       for k in lst:
           if i != j and i != k and j != k: # 去重
               number_lst.append(
			       '{first}{second}{third}'.format(
            	   first=i, second=j, third=k))

print('共有{count}个符合条件的数字'.format(
   count=len(number_lst)))
for item in number_lst:
   print item

小结

注意字符串format的用法,首先要在字符串里定义哪个区域是需要被替换的,并在大括号里定义一个名字,然后在format函数的参数里指明这个名字被谁替换


完全平方数

  1. 打印10000以内的完全平方数
  2. 如果一个数加100是一个完全平方数,加268也是一个完全平方数,求10000以内所有符合这个要求的数值

解答

# 第一题 方法一
numbers = [x*x for x in range(1, 10001)]
for num in numbers:
    print(num)
    
# 第一题 方法二
num = 1
while  num < 10001:
    print(num*num)
    num += 1
    
# 第二题
num = 1
squares = []
while True:
    square = num*num
    if square > 10268:
        break
    num += 1
    squares.append(square)

for square in squares:
    if (square + 168) in squares:
        print(square - 100)
  • 有一个数,假设加100后的值为A,加268之后的值是B,根据题目要求,A和B都是完全平方数,那么B-A = 168
  • 程序先找出10268以内的所有完全平方数,然后遍历,如果一个完全平方数加168 以后还是完全平方数,那么这个完全平方数减去100,就是要找的数值

打印乘法口诀表

打印乘法口诀表,效果如下表所示:

1*1 = 1  
1*2 = 2 	2*2 = 4  
1*3 = 3 	2*3 = 6 	3*3 = 9  
1*4 = 4 	2*4 = 8 	3*4 = 12 	4*4 = 16  
1*5 = 5 	2*5 = 10	3*5 = 15 	4*5 = 20 	5*5 = 25  
1*6 = 6 	2*6 = 12 	3*6 = 18 	4*6 = 24 	5*6 = 30 	6*6 = 36  
1*7 = 7 	2*7 = 14 	3*7 = 21 	4*7 = 28 	5*7 = 35 	6*7 = 42 	7*7 = 49  
1*8 = 8 	2*8 = 16 	3*8 = 24 	4*8 = 32 	5*8 = 40 	6*8 = 48 	7*8 = 56 	8*8 = 64  
1*9 = 9 	2*9 = 18 	3*9 = 27 	4*9 = 36 	5*9 = 45 	6*9 = 54 	7*9 = 63 	8*9 = 72 	9*9 = 81

解答

def get_line(number):
    line = ""
    str_foramt = "{left}*{right} = {result}"
    for i in range(1, number+1):
        if i != 1:
            line += " \t"
        line += str_foramt.format(left=i, right=number, result=i*number)
    return line

for i in range(1,10):
    print(get_line(i))

输出水仙花数

题目

输出所有的水仙花数,所谓水仙花数是指一个三位数各个位上的数的立方相加在一起等于这个三位数。比如153,1的3次方 + 5的三次方 + 3的三次方 等于153。


解答

for n in range(100,1000):
    i = n / 100         # 获取百位数
    j = n / 10 % 10     # 获取十位数
    k = n % 10          # 获取个位数
    if n == i**3 + j**3 + k**3:
        print(n)

输出学生分数的考评等级

允许用户从终端输入一个分数,程序输出这个分数所属的考评等级,90到100分是A,60到89是B,60分以下是C


解答

while True:
    scores = int(input("请输入分数:"))
    
    if scores >= 90 and scores <= 100:
        grade = 'A'
    elif scores >= 60:
        grade = 'B'
    elif scores >= 0:
        grade = 'C'
    else:
        break
    print(grade)

生成数据

  • 数据可视化:通过可视化表示来探索数据
  • 数据挖掘:使用代码来探索数据集的规律和关联

安装 matplotlib

pip

pip是一个以Python写成的软件包管理系统,它可以安装和管理软件包

大多数较新的Python版本都自带pip,因此首先可检查系统是否已经安装了pip。

  • 在Windows系统中检查是否安装了pip

    打开一个终端窗口,并执行如下命令:

    > python -m pip --version
    pip 20.1.1 from C:\Users\Hunter\AppData\Local\Programs\Python\Python38-32\lib\site-packages\pip (python 3.8)

在 Windows 系统中安装 matplotlib

在Windows系统中,首先需要安装Visual Studio。接下来,访问matplotlib · PyPI,查找与使用的Python版本、操作系统相匹配的.whl文件。

将这个.whl文件复制到项目所在文件夹,打开命令窗口,使用pip来安装matplotlib:

python -m pip install --user matplotlib-3.2.2-cp38-cp38-win32.whl

测试 matplotlib

安装必要的包后,对安装进行测试。为此,首先使用命令python或python3启动一个终端会话,再尝试导入matplotlib:

$ python3
>>> import matplotlib

如果没有出现任何错误消息,就说明成功安装了 matplotlib


绘制简单的折线图

我们将使用平方数序列1、4、9、16和25来绘制这个图表。

import matplotlib.pyplot as plt

%matplotlib inline # 让生成的图形嵌入jupyter notebook中

    squares = [1, 4, 9, 16, 25]
    plt.plot(squares)
    plt.show()
  • 模块pyplot包含很多用于生成图表的函数。

  • plt.show()打开matplotlib查看器,并显示绘制的图形。查看器能够缩放和导航图形,另外,单击磁盘图表可将图形保存起来

修改标签文字和线条粗细

上述代码通过matplotlib查看器显示的图形,标签文字太小、线条太细。下面通过一些定制来改善这个图形的可读性:

import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]
plt.plot(squares, linewidth=5)

# 设置图表标题,并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', labelsize=14)

plt.show()
  • 参数linewidth决定了plot()绘制的线条的粗细
  • 函数title()给图表指定标题
  • 参数fontsize指定了图表中文字的大小
  • 函数xlabel()ylabel()
  • 函数tick_params()设置刻度的样式

校正图形

图形更容易阅读后,我们发现没有正确地绘制数据:折线图的终点指出4.0的平方为25。下面来修复这个问题。

当你向plot()提供一系列数字时,它假设第一个数据点对应的x坐标值为0,但我们的第一个点对应的x值为1。为改变这种默认行为,我们可以给plot()同时提供输入值和输出值

import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
plt.plot(input_values, squares, linewidth=5)

# 设置图表标题,并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', labelsize=14)

plt.show()

使用 scatter() 绘制散点图并设置其样式

有时候,需要绘制散点图设置各个数据点的样式。例如,你可能想以一种颜色显示较小的值,用另一种颜色显示较大的值。绘制大型数据集时,还可以对每个点都设置相同的样式,再使用不同的样式选项重新绘制某些点,以突出它们

要绘制简单的点,可使用函数scatter(),并向它传递一对x和y坐标,它将在指定位置绘制一个点

import matplotlib.pyplot as plt

plt.scatter(2, 4)
plt.show()

下面来设置输出的样式,使其更有趣:添加标题,给轴加上标签,并确保所有文本都大到能够看清

import matplotlib.pyplot as plt

plt.scatter(2, 4, s=200)

# 设置图表标题并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', which='major', labelsize=14)
# 显示图例
plt.legend()

plt.show()
  • plt.scatter(2, 4, s=200)实参s设置了绘制图形时使用的点的尺寸

使用 scatter() 绘制一系列点

要绘制一系列的点,可向scatter()传递两个分别包含x值和y值的列表,如下所示:

import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]

plt.scatter(x_values, y_values, s=100)

# 设置图表标题并给坐标轴加上标签
--snip--

--snip--

# 绘制一个展示 船票Fare 与 乘客年龄和性别 之间关系的散点图


# 方式一
# 利用query对性别和年龄进行筛选
x_values_1 = titanic.query("Sex == 'male' & Age != 'NaN'")['Age']
y_values_1 = titanic.query("Sex == 'male' & Age != 'NaN'")['Fare']

x_values_2 = titanic.query("Sex == 'female' & Age != 'NaN'")['Age']
y_values_2 = titanic.query("Sex == 'female' & Age != 'NaN'")['Fare']

plt.scatter(x_values_1, y_values_1, s=10, c='b', label='Male')
plt.scatter(x_values_2, y_values_2, s=10, c='r', label='Female')

# 方式二
titanic.loc[titanic['Sex'] == 'male', 'color'] = 'b' # 该处loc[]的用法为:指出 符合条件的行的列值 和 列标签
titanic.loc[~(titanic['Sex'] == 'male'), 'color'] = 'r'
plt.scatter(titanic['Age'].dropna(), titanic['Fare'].dropna(), s=10, c=titanic[color])



plt.title("船票与年龄", fontsize=24)
plt.xlabel("Age", fontsize=14)
plt.ylabel("Fare", fontsize=14)
# 显示图例
plt.legend()

plt.show()

可利用query()排除一些极端值的干扰

# 可视化 消费金额和购买数量的关系散点图
gu = grouped_user.sum().query('order_amount < 6000')
plt.scatter(gu['order_amount'], gu['order_products'])

自动计算数据

下面是绘制1000个点的代码:

import matplotlib.pyplot as plt

x_values = list(range(1, 1001))
y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, s=40)

# 设置图表标题并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置每个坐标轴的取值范围
plt.axis([0, 1100, 0, 1100000])

plt.show()

由于这个数据集较大,我们将点设置得较小,并使用函数axis()指定了每个坐标轴的取值范围


删除数据点的轮廓

matplotlib允许你给散点图中的各个点指定颜色,默认为蓝色点黑色轮廓。在散点图包含的数据点不多时效果很好,但绘制很多点时,黑色轮廓可能会粘连在一起

要删除数据点的轮廓,可在调用scatter()时传递实参edgecolor='none'

plt.scatter(x_values, y_values, edgecolor='none', s=40)

注意:matplotlib 2.0.0版本之后,scatter()函数的实参edgecolor默认为’none'


自定义颜色

修改数据点的颜色,可向scatter()传递参数c,并将其设置为要使用的颜色的名称,如下所示:

plt.scatter(x_values, y_values, c='red', s=40)

还可以使用RGB颜色模式自定义颜色:

plt.scatter(x_values, y_values, c=(0, 0, 0.8), s=40)

值越接近0,指定的颜色越深,值越接近1,指定的颜色越浅


使用颜色映射

颜色映射(colormap)是一系列颜色,它们从起始颜色渐变到结束颜色。在可视化中,颜色映射用于突出数据的规律,例如你可能用较浅的颜色来显示较小的值,并用较深的颜色来显示较大的值。

模块pyplot内置了一组颜色映射

import matplotlib.pyplot as plt

x_values = list(range(1, 1001))
y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues,
           s=40)

# 设置图表标题并给坐标轴加上标签
--snip--

我们将参数c设置成了一个y值列表,并使用参数cmap告诉pyplot使用哪个颜色映射。y值较小的点显示为浅蓝色,y值较大的点显示为深蓝色。

注意:要了解pyplot中所有的颜色映射,访问Colormap reference


自动保存图表

要让程序自动将图表保存到文件中,可将对plt.show()的调用替换为对plt.savefig()的调用:

plt.savefig('suqares_plot.png', bbox_inches='tight')

  • 第一个实参指定要以什么样的文件名保存图表,这个文件将存储到当前程序所在的目录中
  • 第二个实参指定将图表多余的空白区域剪掉。如果要保留图表周围多余的空白区域,可省略这个实参

绘制饼图

matplotlib可视化饼图 - 知乎

# 绘制一个展示男女乘客比例的扇形图
males = (titanic['Sex'] == 'male').sum()
females = (titanic['Sex'] == 'female').sum()

proportions = [males, females]

# 绘制饼图
plt.pie(
    proportions,
    
    labels = ['Males', 'Females'],
    
    #是否 添加阴影效果
    shadow = False,
    
    # 指定填充色
    colors = ['blue', 'red'],
    
    # 每一块偏移中心的距离
    explode = (0.15 , 0.3),

    # 饼图的初始摆放角度,默认图是从x轴正方向逆时针画起,如设定=90则从y轴正方向画起
    startangle = 90,
    
    # 显示百分比, 小数点前后表示最少的位数 末尾两个%%是输出%自身
    autopct = '%1.1f%%'
    
)


# x,y轴刻度设置一致,保证饼图为圆形
plt.axis('equal')

# 设置图表标题
plt.title("男女乘客比例")

# tight_layout会自动调整子图参数,使之填充整个图像区域
plt.tight_layout()

plt.show()

随机漫步

随机漫步是这样行走得到的路径每次行走都完全是随机的,没有明确的方向,结果是由一系列随机决策决定的。在自然界、物理学、生物学、化学和经济领域,随机漫步都有其实际用途。例如,漂浮在水滴上的花粉因不断受到水分子的挤压而在水面上移动。水滴中的分子运动是随机的,因此花粉在水面上的运动路径犹如随机漫步。

创建 RandomWalk() 类

创建一个RandomWalk()类,这个类需要三个属性:

  1. 存储随机漫步次数的变量
  2. 随机漫步经过的每个点的x坐标
  3. 随机漫步经过的每个点的y坐标

RandomWalk()类只包含两个方法:

  1. __init__()
  2. fill_walk():计算随机漫步经过的所有点

先来看看__init__():

from random import choice

class RandomWalk():
    """一个生成随机漫步数据的类"""
    
    def __init__(self, num_points=5000):
        """初始化随机漫步的属性"""
        self.num_points = num_points
        
        # 所有随机漫步都始于(0, 0)
        self.x_values = [0]
        self.y_values = [0]

为做出随机决策,我们将所有可能的选择都存储在一个列表中,并在每次做决策时都使用choice()来决定使用哪种选择


选择方向

我们将使用fill_walk()来生成漫步包含的点,并决定每次漫步的方向,如下所示:

def fill_walk(self):
        """计算随机漫步包含的所有点"""
        
        # 不断漫步,直到列表到达指定的长度
        while len(self.x_values) < self.num_points:
            # 决定前进方向以及沿这个方向前进的距离
            x_direction = choice([1, -1])
            x_distance = choice([0, 1, 2, 3, 4])
            x_step = x_direction * x_distance
            
            y_direction = choice([1, -1])
            y_distance = choice([0, 1, 2, 3, 4])
            y_step = y_direction * y_distance
            
            # 拒绝原地踏步
            if x_step == 0 and y_step == 0:
                continue
                
            # 计算下一个点的x和y值
            next_x = self.x_values[-1] + x_step
            next_y = self.y_values[-1] + y_step
            
            self.x_values.append(next_x)
            self.y_values.append(next_y)
  • choice([-1, 1])从-1和1中随机选择一个值
  • choice([0, 1, 2, 3, 4])从0~4中随机选择一个值

绘制随机漫步图

rw_visual.py

import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk()
rw.fill_walk()
plt.scatter(rw.x_values, rw.y_values, s=15)
plt.show()

模拟多次随机漫步

要在不多次运行程序的情况下模拟多次随机漫步,一种办法是将这些代码放在一个while循环中,如下所示:

import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例,并将其包含的点都绘制出来
	rw = RandomWalk()
	rw.fill_walk()
	plt.scatter(rw.x_values, rw.y_values, s=15)
	plt.show()
	
	keep_running = input("Make another walk? (y/n): ")
	if keep_running == 'n':
		break

给点着色

我们将使用颜色映射来指出漫步中各点的先后顺序。为根据漫步中各点的先后顺序进行着色,我们传递参数c,并将其设置为一个列表,其中包含各点的先后顺序

由于这些点是按顺序绘制的,因此给参数c指定的列表只需包含数字1~5000

--snip--
while True:
    # 创建一个RandomWalk实例,并将其包含的点都绘制出来
	rw = RandomWalk()
	rw.fill_walk()
    
	point_numbers = list(range(rw.num_points))
	plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=15)
	plt.show()
	
	keep_running = input("Make another walk? (y/n): ")
	--snip--

重新绘制起点和终点

--snip--
while True:
    --snip--
    plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=15)
    
    # 突出起点和终点
    plt.scatter(0, 0, c='green', s=100)
    plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', s=100)
    
    plt.show()
    --snip--

隐藏坐标轴

--snip--
while True:
    --snip--
    plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', s=100)
    
    # 隐藏坐标轴
    plt.axes().get_xaxis().set_visible(False)
    plt.axes().get_yaxis().set_visible(False)
    
    plt.show()
    --snip--

为修改坐标轴,使用了函数plt.axes()来将每条坐标轴的可见性设置为False。


增加点数

在创建RandomWalk实例时增大num_points的值,并在绘图时调整每个点的大小,如下所示:

--snip--
while True:
    # 创建一个RandomWalk实例,并将其包含的点都绘制出来
    rw = RandomWalk(5000)
    rw.fill_walk()
    
    # 绘制点并将图形显示出来
    point_numbers = list(range(rw.num_points))
    plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=1)
    --snip--

调整尺寸以适合屏幕

图表适合屏幕大小时,更能有效地将数据中的规律呈现出来。为让绘图窗口更适合屏幕大小,可像下面这样调整matplotlib输出的尺寸

--snip--
while True:
    # 创建一个RandomWalk实例,并将其包含的点都绘制出来
	rw = RandomWalk()
	rw.fill_walk()
    
    # 设置绘图窗口的尺寸
    plt.figure(figsize=(10, 6))
    --snip--

函数figure()用于指定图表的宽度、高度、分辨率和背景色

需要给形参figsize指定一个元组,向matplotlib指出绘图窗口的尺寸单位为英寸

Python假定屏幕分辨率为80像素/英寸,如果知道自己系统的分辨率,可使用形参dpi向figure()传递该分辨率,以有效利用可用的屏幕空间:

plt.figure(dpi=128, figsize=(10, 6))


使用 Pygal 模拟掷骰子

本节将使用Python可视化包Pygal来生成可缩放的矢量图形文件,它们将自动缩放,以适合观看者的屏幕。如果你打算以在线的方式使用图表,请考虑使用Pygal来生成它们,这样在任何设备上显示都会很美观。

在这个项目中,我们将对掷骰子的结果进行分析。为确定哪些点数出现的可能性最大,我们将生成一个表示掷骰子结果的数据集,并根据结果绘制出一个图形

安装 Pygal

在 Windows 系统中,命令:

python -m pip install --user pygal==1.7


Paygal 画廊

要了解使用Pygal可创建什么样的图表,可以查看Chart types — pygal documentation


创建 Die 类

下面的类模拟掷一个骰子:

from random import randint

class Die():
    """表示一个骰子的类"""
    
    def __init__(self, num_sides=6):
        """骰子默认为6面"""
        self.num_sides = num_sides
        
    def roll(self):
        """返回一个位于1和骰子面数之间的随机数"""
        return randint(1, self.num_sides)
  • 方法roll()使用函数randint()返回一个1和面数之间的随机整数(包括起始值1和终止值num_sides)

骰子根据面数命名,6面的骰子命名为D6,8面的骰子命名为D8。


掷骰子

使用这个类来创建图表前,先来掷D6骰子,将结果打印出来,并检查结果是否合理:

from die import Die

# 创建一个D6
die = Die()

# 掷几次骰子,并将结果存储在一个列表中
results = []
for roll_num in range(100):
    result = die.roll()
    results.append(result)
    
print(results)

分析结果

为分析掷一个D6骰子的结果,我们计算每个点数出现的次数

--snip--
# 掷几次骰子,并将结果存储在一个列表中
results = []
for roll_num in range(1000):
    result = die.roll()
    results.append(result)
    
# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)
    
print(frequencies)
  • 为分析结果,我们创建了空列表frequencies,用于存储每种点数出现的次数

绘制条形图(bar chart)

有了频率列表后,我们就可以绘制一个表示结果的条形图(bar chart)指出各种结果出现的频率

  1. 利用pygal绘制
import pygal
--snip--

# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)
    
# 对结果进行可视化
bar = pygal.Bar()

bar.title = "Results of rolling one D6 1000 times."
bar.x_labels = ['1', '2', '3', '4', '5', '6']
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6', frequencies)
bar.render_to_file('die_visual.svg')
  • 为创建条形图(bar diagram),创建了一个pygal.Bar()实例
  • 将D6骰子的可能结果用作x轴的标签
  • 使用add()将一系列值添加到图表中(传递给添加的值指定的标签将出现在图表中的值的列表)
  • 将图表渲染为一个SVG文件(Scalable Vector Graphics,可缩放的矢量图形,是一种基于XML图像文件格式)

要查看生成的直方图,最简单的方式是使用Web浏览器。为此,在任意浏览器中新建一个标签页,再打开文件die_visual.svg,将看到对应的图表。

注意:Python让这个图表具有交互性:如果将鼠标指向该图表中的任何条形,将看到与之相关联的数据(若无效果,刷新)。


绘制直方图(histogram)

  1. 利用pyplot绘制
import pandas as pd
import matplotlib.pyplot as plt
# 绘制一个展示船票价格的直方图

# 创建直方图
# 参数:需要计算的series ,bins可以是直方图个数(默认为10),也可以是自定义的序列
plt.hist(titanic['Fare'], bins = range(0,600,10))

# Set the title and labels
plt.xlabel('Fare')
plt.ylabel('Frequency')
plt.title('船票价格分布')

# show the plot
plt.show()


# 用户消费金额的分布图,利用query消除极值对可视化呈现效果的干扰
plt.hist(grouped_user.sum().query('order_amount < 1000')['order_amount'], bins = 20)
plt.show()

同时掷两个骰子

import pygal

from die import Die

# 创建两个D6骰子
die_1 = Die()
die_2 = Die()

# 掷骰子多次,并将结果存储到一个列表中
results = []
for roll_num in range(1000):
    result = die_1.roll() + die_2.roll()
    results.append(result)
    
# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(2, max_result+1):
    frequency = results.count(value)
    frequencies.append(frequency)
    
# 可视化结果
bar = pygal.Bar()

bar.title = "Results of rolling two D6 dice 1000 times."
bar.x_labels = ['2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6 + D6', frequencies)
bar.render_to_file('dice_visual.svg')

同时掷两个面数不同的骰子

下面来创建一个6面骰子和一个10面骰子,看看同时掷这两个骰子50000次的结果:

from die import Die

import pygal

# 创建一个D6和一个D10
die_1 = Die()
die_2 = Die(10)

# 掷骰子多次,并将结果存储到一个列表中
results = []
for roll_num in range(50000):
    result = die_1.roll() + die_2.roll()
    results.append(result)
    
# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(2, max_result+1):
    frequency = results.count(value)
    frequencies.append(frequency)
    
# 可视化结果
bar = pygal.Bar()

bar.title = "Results of rolling a D6 and a D10 50,000 times."
bar.x_labels = [str(value) for value in range(2, 17)]
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6 + D10', frequencies)
bar.render_to_file('dice_visual.svg')
  • 通过列表解析调整x轴标签

下载数据

网上的数据多得难以置信,且大多未经过仔细检查。如果能够对这些数据进行分析,你就能发现别人没有发现的规律和关联。

CSV 文件格式

要在文本文件中存储数据,最简单的方式是将数据作为一系列以逗号分隔的值(CSV, Comma-Separated Values)写入文件,这样的文件称为CSV文件

例如,下面是一行CSV格式的天气数据:

2014-1-5,61,44,26,18,7,-1,56,30,9,30.34,30.27,30.15,,,,10,4,,0.00,0,,195

CSV文件对人来说阅读起来比较麻烦,但程序可轻松地提取并处理其中的值,这有助于加快数据分析过程。

  • 最好使用用文字编辑器打开csv文件查看Excel可能会更改显示格式

分析 CSV 文件头

csv模块包含在Python标准库中,可用于分析CSV文件中的数据行,让我们能够快速提取感兴趣的值。下面先来看一下sitka_weather_07-2014.csv文件的第一行,其中包含一系列有关数据的描述

import csv

filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
    print(header_row)
  • 调用csv.reader(),并将前面存储的文件对象作为实参传入,从而创建一个与该文件相关联的阅读器对象
  • 模块csv的reader类包含next()方法,调用内置函数next()将一个阅读器对象作为参数传入,将调用阅读器对象的next()方法,从而返回文件中的下一行
  • reader处理文件中以逗号分隔的第一行数据,并将每项数据都作为一个元素存储在列表中

注意:文件头的格式并非总是一致的, 空格和单位可能出现在奇怪的地方,但不会带来任何问题


打印文件头及其位置

为让文件头数据更容易理解,将列表中的每个文件头及其位置打印出来:

--snip--
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
    
    for index, column_header in enumerate(header_row):
        print(index, column_header)
  • 对列表调用了enumerate()(枚举)来获取每个元素的索引及其值

提取并读取数据

首先读取每天的最高气温

import csv

# 从文件中获取最高气温
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
    
    highs = []
    for row in reader:
        highs.append(row[1])
        
    print(highs)
  • for row in reader遍历文件中余下的各行

    每次执行上述循环,我们都将索引1处(第2列)的数据附加到highs末尾。

下面使用int()将这些字符串转换为数字让matplotlib能够读取它们

--snip--
	highs = []
    for row in reader:
        high = int(row[1])
        highs.append(high)
        
    print(highs)

绘制气温图表

为可视化这些气温数据,我们首先使用matplotlib创建一个显示每日最高气温的简单图形

import csv

from matplotlib import pyplot as plt

# 从文件中获取最高气温
--snip--

# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(highs, c='red')

# 设置图形的格式
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()
  • 鉴于还没有添加日期,因此没有给x轴添加标签

  • tick_params()

    which一共3个参数:[‘major’ , ‘minor’. ‘both’]
    默认是major,表示主刻度,后面分布为次刻度主次刻度都显示.

  • 调整x轴日期的显示间隔

    plt.xticks(pd.date_range('1997-01-01', '1997-04-01', freq='10d'))

模块 datetime

下面在图表中添加日期。在天气数据文件中,第一个日期在第二行

获取日期数据时,获得的是一个字符串,因此需要将字符串'2014-7-1'转换为一个表示相应日期的对象。可使用模块datetime中的方法strptime()

>>> from datetime import datetime
>>> first_date = datetime.strptime('2014-7-1', '%Y-%m-%d')
>>> print(first_date)
2014-07-01 00:00:00
  • 传入方法strptime()第2个实参告诉Python如何设置日期的格式

    • %Y-让Python将字符串中第一个连字符前面的部分视为4位的年份
    • %m-让Python将第二个连字符前面的部分视为表示月份的数字
    • %d让Python将最后一部分视为月份中的一天
  • 方法strptime()可接受各种实参,并根据它们来决定如何解读日期:

    实参 含义
    %A 星期的名称,如Monday
    %B 月份名,如January
    %m 数字表示的月份 (01~12)
    %d 数字表示月份中的一天 (01~31)
    %Y 四位的年份,如2015
    %y 两位的年份,如15
    %H 24小时制的小时数 (00-23)
    %I 12小时制的小时数 (01~12)
    %p am或pm
    %M 分钟数 (00~59)
    %S 秒数 (00~61)

在图表中添加日期

import csv
from datetime import datetime

from matplotlib import pyplot as plt

# 从文件中获取日期和最高气温
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
        
    dates, highs = [], []
    for row in reader:
        current_date = datetime.strptime(row[0], "%Y-%m-%d")
        dates.append(current_date)
        
        high = int(row[1])
        highs.append(high)

# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')

# 设置图形的格式
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
plt.gcf().autofmt_xdate() # 绘制斜的日期标签,以免它们彼此重叠
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()
  • 创建了两个空列表:dates, highs = [], []
  • 将包含日期信息的数据转换为datetime对象:datetime.strptime(row[0], "%Y-%m-%d")
  • 将日期和最高气温值传给plot()plt.plot(dates, highs, c='red')

涵盖更长的时间

创建覆盖整年的天气图:

--snip--
# 从文件中获取日期和最高气温
filename = 'stika_weather_2014.csv'
with open(filename) as f:
--snip--
# 设置图形的格式
plt.title("Daily high temperatures - 2014", fontsize=24)
plt.xlabel('', fontsize=16)
--snip--

再绘制一个数据系列

从数据文件中提取最低气温,并添加到图表中:

--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'sitka_weather_2014.csv'
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
    
    dates, highs, lows = [], [], []
    for row in reader:
        current_date = datetime.strptime(row[0], "%Y-%m-%d")
        dates.append(current_date)
        
        high = int(row[1])
        highs.append(high)
        
        low = int(rpw[3])
        lows.append(low)
        
# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')
plt.plot(dates, lows, c='blue')

# 设置图形的格式
plt.title("Daily high and low temperatures - 2014", fontsize=24)
--snip--

给图表区域着色

通过着色来呈现每天的气温范围,为此使用方法fill_between(),它接受一个x值系列和两个y值系列,并填充两个y值系列之间的空间

--snip--
# 根据数据绘制图形
fig = plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red'. alpha=0.5)
plt.plot(dates, lows, c='blue'. alpha=0.5)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
  • 实参alpha指定颜色的透明度
    • 0表示完全透明
    • 1(默认)表示完全不透明
  • fill_between()
    • 实参facecolor指定了填充区域的颜色

错误检查

我们应该能够使用有关任何地方的天气数据来运行highs_lows.py中的代码,但有些气象站会偶尔出现故障,未能收集部分或全部其应该收集的数据缺失数据可能会引发异常,如果不妥善地处理,还可能导致程序崩溃

例如,我们来看看生成加利福尼亚死亡谷的气温图时出现的情况:

--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'death_valley_2014.csv'
withj open(filename) as f:
--snip--

运行结果:

Traceback (most recent call last):
  File "highs_lows.py", line 17, in <module>
    high = int(row[1])
ValueError: invalid literal for int() with base 10: ''

查看文件可知,没有记录2014年2月16日的数据,表示最高温度的字符串为空。为解决这种问题,我们从CSV文件中读取值时执行错误检查代码,对分析数据集时可能出现的异常进行处理

--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'death_valley_2014.csv'
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)
        
    dates, highs, lows = [], [], []
    for row in reader:
        try:
	        current_date = datetime.strptime(row[0], "%Y-%m-%d")
        	high = int(row[1])
            low = int(row[3])
        except ValueError:
            print(current_date, 'missing data')
        else:
            dates.append(current_date)
	        highs.append(high)
            lows.append(low)
            
# 根据数据绘制图形
--snip--

# 设置图形的格式
title = "Daily high and low temperatures - 2014\nDeath Valley, CA"
plt.title(title, fontsize=20)
--snip--

在有些情况下,需要使用continue来跳过一些数据,或者使用remove()(删除列表值)或del(删除 键-值 对)将已提取的数据删除。可采用任何管用的方法,只要能进行精确而有意义的可视化就好。


16-2 比较锡特卡和死亡谷的气温:

为准确地比较锡特卡和死亡谷的气温范围,在y轴上使用相同的刻度,对两地的气温范围进行直接比较:

  • pyplot的方法ylim()可以对y轴的刻度做限制xlim()则可以对x轴的刻度做限制。
# 设置图形的格式
--snip--
plt.ylim(10, 120)

plt.show()

尝试在一个图表中呈现这两个数据集

import csv
from datetime import datetime

from matplotlib import pyplot as plt

def get_weather_data(filename, dates, highs, lows):
    """从文件中获取日期、最高气温和最低气温"""
    with open(filename) as f:
        reader = csv.reader(f)
        header_row = next(reader)
        
        for row in reader:
            try:
                current_date = datetime.strptime(row[0], "%Y-%m-%d")
                high = int(row[1])
                low = int(row[3])
            except ValueError:
                print(current_date, 'missing data')
            else:
                dates.append(current_date)
                highs.append(high)
                lows.append(low)

# 获取锡特卡的气温数据
dates, highs, lows = [], [], []
get_weather_data('stika_weather_2014.csv', dates, highs, lows)

# 根据锡特卡数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red', alpha=0.6)
plt.plot(dates, lows, c='blue', alpha=0.6)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.15)

# 获取死亡谷的气温数据
dates, highs, lows = [], [], []
get_weather_data('death_valley_2014.csv', dates, highs, lows)

# 将死亡谷数据的图形 添加到当前的绘制中
plt.plot(dates, highs, c='red', alpha=0.3)
plt.plot(dates, lows, c='blue', alpha=0.3)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.05)

# 设置图形的格式
title = "Daily high and low temperatures - 2014"
title += "\nSitka, AK and Death Valley, CA"
plt.title(title, fontsize=20)
plt.xlabel('', fontsize=16)
plt.gcf().autofmt_xdate()
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.ylim(10, 120)

plt.show()

制作交易收盘价走势图: JSON 格式

下载收盘价数据

btc_close_2017.json实际是一个很长的Python列表,其中每个元素都是一个包含五个键的字典:统计日期、月份、周数、周几以及收盘价。由于2017年1月1日是周日,作为2017年的第一周实在太短,。因此被计入2016年的第52周。于是2017年的第一周是从1月2日开始的。

可以将收盘价数据文件直接下载到程序所在的文件夹中,也可以用Python 2.x标准库中模块urllib2(Python 3.x版本使用urllib)的函数urlopen()来做,还可以通过Python的第三方模块requests下载数据。

如果采用函数urlopen()来下载数据,可以使用如下代码:

from __future__ import (absolute_import, division, print_function, unicode_literals)

try:
    # Python 2.x 版本
    from urllib2 import urlopen
except ImportError:
    # Python 3.x 版本
    from urllib.request import urlopen
import json

json_url = 'https://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
response = urlopen(json_url)
# 读取数据
req = response.read()
# 将数据写入文件
with open('btc_close_2017_urllib.json', 'wb') as f:
    f.write(req)
# 加载json格式
file_urllib = json.loads(req)
print(file_urllib)
  • python打开文件时w与wb的区别,r与rb的区别

    Windows中的换行符\r\n

    • 文本方式('w')写入,遇到\n自动替换\r\n
    • 二进制文本方式('wb')写入,遇到\n仍然按\n记录
  • json

    • load()

      载入json文件

    • loads()

      载入JSON格式的字符串

函数urlopen的代码稍微复杂一些,第三方模块requests封装了许多常用的方法,让数据下载和读取方式变得非常简单

import requests

json_url = 'https://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
req = requests.get(json_url)
# 将数据写入文件
with open('btc_close_2017_request.json', 'w') as f:
    f.write(req.text)
file_requests = req.json()
  • req.text属性可以直接读取文件数据返回格式是字符串
  • req.json()可以将json文件的数据转换为Python列表,与之前的file_urllib内容相同

提取相关的数据

import json

# 将数据加载到一个列表中
filename = 'btc_close_2017.json'
with open(filename) as f:
    btc_data = json.load(f)
# 打印每一天的信息
for btc_dict in btc_data:
    date = btc_dict['date']
    month = btc_dict['month']
    week = btc_dict['week']
    weekday = btc_dict['weekday']
    close = btc_dict['close']
    print("{} is month {} week {}, {}, the close price is {} RMB".format(date, month, week, weekday, close))

将字符串转换为数字值

btc_close_2017.json中的每个键和值都是字符串。为了能在后边的内容中对交易数据进行计算,需要先将表示周数和收盘价的字符串转换为数值,因此使用函数int()

--snip--

# 打印每一天的信息
for btc_dict in btc_data:
    date = btc_dict['date']
    month = int(btc_dict['month'])
    week = int(btc_dict['week'])
    weekday = btc_dict['weekday']
    close = int(float(btc_dict['close']))
    print("{} is month {} week {}, {}, the close price is {} RMB".format(date, month, week, weekday, close))
  • 在实际工作中,原始数据的格式经常是不统一的。此类数值类型转换造成的ValueError异常十分普遍。该例子中,无法将包含小数点的字符串转换为整数,因此需要先转换为浮点数再转换为整数int(float(btc_dict['close']))

有了这些数据之后,可以结合Pygal的可视化功能来探索一些有趣的信息


绘制收盘价折线图

之前章节介绍过用Pygal绘制条形图(bar chart)的方法,也介绍了用matplotlib绘制折线图(line chart)的方法。下面用Pygal来实现收盘价的折线图

绘制折线图之前,需要获取x轴与y轴数据,因此我们创建了几个列表来存储数据

--snip--

# 创建5个列表,分别存储日期和收盘价
dates = []
months = []
weeks = []
weekdays = []
close = []
# 每一天的信息
for btc_dict in btc_data:
    dates.append(btc_dict['date'])
    months.append(int(btc_dict['month']))
    weeks.append(int(btc_dict['week']))
    weekdays.append(btc_dict['weekday'])
    close.append(int(float(btc_dict['close'])))

由于数据点比较多,x轴要显示346个日期,因此需要利用Pygal的配置参数,对图形进行适当的调整

--snip--

import pygal

line_chart = pygal.Line(x_label_rotation=20, show_mirror_x_labels=False)
line_chart.title = '收盘价(¥)'
line_chart.x_labels = dates
N = 20 # x轴坐标每隔20天显示一次
line_chart.x_labels_major = dates[::N]
line_chart.add('收盘价', close)
line_chart.render_to_file('收盘价折线图(¥).svg')
  • pygal.Line()创建Line实例
    • x_label_rotation=20:让x轴上的标签顺时针旋转20°
    • show_mirror_x_labels=False:告诉图形不用显示所有的x轴标签
  • line_chart.x_labels_major = dates[:20]:x轴坐标每隔20天显示一次

下面对价格做一些简单的探索


时间序列特征初探

进行时间序列分析总是期望发现趋势(trend)周期性(seasonality)噪声(noise),从而能够描述事实、预测未来、做出决策

从收盘价的折线图可以看出,2017年的总体趋势时非线性的,而且增长幅度不断增大,似乎呈指数分布。但是,在每个季度末(3月、6月、9月)似乎有一些相似的波动为了验证周期性的假设,需要先将非线性的趋势(指数增长部分)消除。**对数变换(log transformation)**是常用的处理方法之一。

Python标准库的数学模块math来解决这个问题。math里有许多常用的数学函数,这里以10为底的对数函数math.log10计算收盘价,日期仍然保持不变——这种方式称为半对数(semi-logarithmic)变换

--snip--

import pygal
import math

line_chart = pygal.Line(x_label_rotation=20, show_mirror_x_labels=False)
line_chart.title = '收盘价对数变换(¥)'
line_chart.x_labels = dates
N = 20 # x轴坐标每隔20天显示一次
line_chart.x_labels_major = dates[::N]
close_log = [math.log10(_) for _ in close]
line_chart.add('log收盘价', close_log)
line_chart.render_to_file('收盘价对数变换折线图(¥).svg')

剔除非线性趋势之后,整体的趋势更接近线性增长,且收盘价在每个季度末似乎有显著的周期性。那么,12月会不会再现这一场景?下面来看看收盘价的月日均值周日均值的表现。


收盘价均值

再利用btc_close_2017.json文件中的数据,绘制2017年前11个月的日均值、前49周(01-02~12-10)的日均值,以及每周各天的日均值。虽然这些日均值的数值不同,但都是一段时间的均值,计算方法都是一样的。因此,可以将绘图代码封装成函数,以便重复使用。

--snip--

from itertools import groupby

def draw_line(x_data, y_data, title, y_legend):
    xy_map = []
    for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _: _[0]):
    y_list = [v for _, v in y]
    xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = x_unique
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
  • 由于需要将数据按月份、周数、周几分组,再计算每组的均值,因此导入Python标准库中模块itertools的函数groupby
  • 将x轴与y轴的数据合并排序,再用函数groupby分组
  • 分组之后,求出每组的平均值,存储到xy_map中
  • 最后,将xy_map中存储的x轴与y轴数据分离,就可以像之前一样用Pygal画图了

下面画出收盘价月日均值,由于12月的数据不完整,只取1月到11月的数据。通过dates查找2017-12-01索引的位置,确定周数和收盘价的取值范围

idx_month = dates.index('2017-12-01')
line_chart_month = draw_line(months[:idx_month], close[:idx_month], '收盘价月日均值(¥)',
                            '月日均值')
line_chart_month

收盘价数据仪表盘

每个SVG文件打开之后都是独立的页面,如果能整合在一起,就可以方便地进行长期管理、监测和分析。另外,新的图表也可以方便地加入进来,这样就形成了一个数据仪表盘(dashboard)。下面,将之前绘制的图整合起来,做一个收盘价数据仪表盘:

--snip--

with open('收盘价Dashboard.html', 'w', encoding='utf8') as html_file:
    html_file.write(
        '<html><head><title>收盘价Dashboard</title><meta charset="utf-8"></head><body>\n')
    for svg in [
        '收盘价折线图(¥).svg', '收盘价对数变换折线图(¥).svg', '收盘价月日均值(¥).svg',
        '收盘价周日均值(¥).svg', '收盘价星期均值(¥).svg'
    ]:
        html_file.write(
            '    <object type="image/svg+xml" data="{0}" height=500></object>\n'.format(svg))
    html_file.write('</body></html>')

和常见网络应用的数据仪表盘一样,这个数据仪表盘也是一个完整的网页。


使用 API

使用 Web API

Web API是网站的一部分,用于 与使用非常具体的URL请求特定信息的程序 交互。这种请求称为API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回

依赖于外部数据源的大多数应用程序都依赖于API调用,如集成社交媒体网站的应用程序。

Git 和 GitHub

本章的可视化将基于来自GitHub的信息。我们将使用GitHub的API来请求有关该网站中Python项目的信息,然后使用Pygal生成交互式可视化,以呈现这些项目的受欢迎程度

本章将编写一个程序,它自动下载GitHub上星级最高的Python项目的信息,并对这些信息进行可视化


使用 API 调用请求数据

GitHub的API让你能够通过API调用来请求各种信息。要知道API调用是什么样的,在浏览器中访问如下地址:

https://api.github.com/search/repositories?q=language:python&sort=starshttps://api.github.com/
  • 第一部分https://api.github.com/:将请求发送到GitHub网站中响应API调用的部分
  • 接下来的部分search/repositories:让API搜索GitHub上的所有仓库
  • repositories后面的问号指出我们要传递一个实参
    • q=q表示查询等号让我们能够开始指定查询
    • 通过使用language:python,我们指出只想获取主要语言为Python的仓库的信息
  • 最后一部分&sort=stars:指定将项目按其获得的星级排序

安装 requests

requests包让Python程序能够轻松地向网站请求信息以及检查返回的响应。要安装requests,执行:

pip install --user requests

处理 API 响应

下面来编写一个程序,它执行API调用并处理结果,找出GitHub上星级最高的Python项目:

import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()

# 处理结果
print(response_dict.keys())
  • 使用requests.get()执行API调用

    响应对象包含一个名为status_code的属性

    • 状态码200表示请求成功
  • 这个API返回JSON格式的信息。因此我们使用方法json()将这些信息转换为一个Python字典


处理响应字典

下面来生成一些概述API调用返回的信息的输出:

import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()
print("Total repositories:", response_dict['total_count'])

# 探索有关仓库的信息
repo_dicts = response_dict['items']
print("Repositories returned:", len(repo_dicts))

# 研究第一个仓库
repo_dict = repo_dicts[0]
print("\nKeys:", len(repo_dict))
for key in sorted(repo_dict.keys()):
    print(key)
  • 'items'关联的值是一个列表,其中包含很多字典,每个字典都包含有关一个Python仓库的信息

下面来提取repo_dict中与一些键相关联的值:

--snip--

# 研究第一个仓库
repo_dict = repo_dicts[0]

print("\nSelected information about first repository:")
print('Name:', repo_dict['name'])
print('Owner:', repo_dict['owner']['login'])
print('Stars:', repo_dict['stargazers_count'])
print('Reposiory:', repo_dict['html_url'])
print('Created:', repo_dict['created_at'])
print('Updated:', repo_dict['updated_at'])
print('Description:', repo_dict['description'])

概述最受欢迎的仓库

对这些数据进行可视化时,我们需要涵盖多个仓库。下面就来编写一个循环,打印API调用返回的每个仓库的特定信息,以便能够在可视化中包含所有这些信息:

--snip--
# 研究有关仓库的信息
repo_dicts = response_dict['items']
print("Repositories returned:", len(repo_dicts))

print("\nSelected information about each repository:")
for repo_dict in repo_dicts:
    print('\nName:', repo_dict['name'])
    print('Owner:', repo_dict['owner']['login'])
    print('Stars:', repo_dict['stargazers_count'])
    print('Reposiory:', repo_dict['html_url'])
    print('Description:', repo_dict['description'])

监视 API 的速率限制

大多数API都存在速率限制,即你在特定时间内可执行的请求数存在限制。要获悉你是否接近了GitHub的限制,在浏览器中输入https://api.github.com/rate_limit,将得到类似于下面的响应:

{
    "resources":{
        "core":{
            "limit":60,
            "remaining":0,
            "reset":1593698255
        },
        "graphql":{
            "limit":0,
            "remaining":0,
            "reset":1593700880
        },
        "integration_manifest":{
            "limit":5000,
            "remaining":5000,
            "reset":1593700880
        },
        "search":{
            "limit":10,
            "remaining":10,
            "reset":1593697340
        }
    },
    "rate":{
        "limit":60,
        "remaining":0,
        "reset":1593698255
    }
}

我们关心的信息是搜索API的速率限制。从18~22行可知,极限为每分钟10个请求;当前这一分钟内,还可执行10个请求;reset值指的是配额将重置的Unix时间或新纪元时间(1970-01-01午夜后多少秒)。用完配额后,将收到一条简单的响应,由此知道已到达API极限。到达极限后必须等待配额重置

注意:很多API都要求你注册获得密钥后才能执行API调用。GitHub没有这样的要求,但获得API密钥后,配额将高得多。


使用 Pygal 可视化仓库

我们来进行可视化,呈现GitHub上Python项目的受欢迎程度。创建一个交互式条形图,条形的高度表示项目获得了多少颗星。单击条形将进入项目在GitHub上的主页

import requests
import pygal
from pygal.style import LightColorizedStyle as LCS, LightenStyle as LS

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()
print("Total repositories:", response_dict['total_count'])

# 研究有关仓库的信息
repo_dicts = response_dict['items']

names, stars = [], []
for repo_dict in repo_dicts:
    names.append(repo_dict['name'])
    stars.append(repo_dict['stargazers_count'])
    
# 可视化
my_style = LS('#333366', base_style=LCS)
chart = pygal.Bar(style=my_style, x_label_rotation=45, show_legend=False)
chart.title = 'Most-Starred Python Projects on GitHub'
chart.x_labels = names

chart.add('', stars)
chart.render_to_file('python_repos.svg')
  • LS('#333366', base_style=LCS):使用LightenStyle(别名LS)定义了一种样式,并将基色设置为深蓝色;还传递了实参base_style,以使用LightColorizedStyle类(别名LCS)
  • show_legend=False隐藏了图例,因为只在图表中绘制一个数据系列
  • 由于不需要给这个数据系列添加标签,因此添加数据时,将标签设置成了空字符串

改进 Pygal 图表

我们将进行多个方面的定制,因此先来稍微调整代码结构,创建一个配置对象,其中包含要传递给Bar()的所有定制:

--snip--

# 可视化
my_style = LS('#333366', base_style=LCS)

my_config = pygal.Config()
my_config.x_label_rotation = 45
my_config.show_legend = False
my_config.title_font_size = 24
my_config.label_font_size = 14
my_config.major_label_font_size = 18
my_config.truncate_label = 15
my_config.show_y_guides = False
my_config.width = 1000

chart = pygal.Bar(my_config, style=my_style)
chart.title = 'Most-Starred Python Projects on GitHub'
chart.x_labels = names

chart.add('', stars)
chart.render_to_file('python_repos.svg')
  • 9~11行设置了图表标题、副标签和主标签的字体大小。

    这个图表中副标签是x轴上的项目名和y轴上的大部分数字;主标签是y轴上为5000整数倍的刻度,这些标签更大,以与副标签区分开。

  • truncate_label=15将较长的项目缩短为15个字符(将鼠标指向被截短的项目名,将显示完整的项目名)

  • show_y_guides=False隐藏图表中的水平线

  • width=1000:自定义的宽度,让图表更充分地利用浏览器中的可用空间


添加自定义工具提示

在Python中,将鼠标指向条形将显示它表示的信息,这通常称为工具提示。下面来创建一个自定义工具提示,以同时显示项目的描述。

来看一个简单的示例,它可视化前三个项目,并给每个项目对应的条形都指定自定义标签。为此,我们向add()传递一个字典列表,而不是值列表:

import pygal
from pygal.style import LightColorizedStyle as LCS, LightenStyle as LS

my_style = LS('#333366', base_style=LCS)
chart = pygal.Bar(style=my_style, x_label_rotation=45, show_legend=False)

chart.title = 'Python Projects'
chart.x_labels = ['httpie', 'django', 'flask']

plot_dicts = [
    {'value': 16101'label': 'Description of httpie.'},
    {'value': 15028, 'label': 'Description of django.'},
    {'value': 14798, 'label': 'Description of flask.'},
]

chart.add('', plot_dicts)
chart.render_to_file('bar_descriptions.svg')
  • plot_dicts列表包含三个字典,'value'对应的值确定条形高度'label'对应的值给条形创建工具提示

根据数据绘图

为根据数据绘图,我们将自动生成plot_dicts。其中包含API调用返回的30个项目的信息:

--snip--
# 研究有关仓库的信息
repo_dicts = response_dict['items']
print("Number of items:", len(repo_dicts))

names, plot_dicts = [], []
for repo_dict in repo_dicts:
    names.append(repo_dict['name'])
    
    plot_dict = {
        'value': repo_dict['stargazers_count'],
        'label': repo_dict['description'],
    }
    plot_dicts.append(plot_dict)
    
# 可视化
my_style = LS('#333366', base_style=LCS)
--snip--

chart.add('', plot_dicts)
chart.render_to_file('python_repos.svg')

在图表中添加可单击的链接

Pygal还允许将图表中的每个条形用作网站的链接。为此,在为每个项目创建的字典中,添加一个键为'xlink'的 键-值 对:

--snip--
names, plot_dicts = [], []
for repo_dict in repo_dicts:
    names.append(repo_dict['name'])
    
    plot_dict = {
        'value': repo_dict['stargazers_count'],
        'label': repo_dict['description'],
        'xlink': repo_dict['html_url'],
    }
    plot_dicts.append(plot_dict)
--snip--

单击图表中的任何条形,都将在浏览器中打开一个新的标签页,并在其中显示相应项目的GitHub页面。


Hacker News API

为探索如何使用其他网站的API调用,我们来看看

Hacker News网站,用户分享编程和技术方面的文章,并就这些文章展开积极的讨论。Hacker News的API让你能够访问有关该网站所有文章和评论的信息,且不要求通过注册获得密钥。

已被墙。


问题

Jupyter Notebook 用matplotlib作图显示中文乱码

plt.rc('font', family='SimHei', size=7)  ##显示中文,字体大小根据需要调整

存在极端值干扰

  • 使用query()做范围的筛选

  • 根据切比雪夫定理做筛选:

    • 所有数据中,至少有75%的数据位于平均数2个标准差范围内
  • 所有数据中,至少有88.9%的数据位于平均数3个标准差范围内

    • 所有数据中,至少有96%的数据位于平均数5个标准差范围内

条形图和直方图的区别

  • 条形图(bar chart)

    1. 条形的高度表示各类别频数的多少,其宽度(表示类别)则是固定的;
    2. 条形图各矩形分开排列(有间隙)
    3. 主要用于展示分类数据
  • 直方图(Histogram)

    1. 面积表示各组频数的多少,矩形的高度表示每一组的频数或频率宽度则表示各组的组距
    2. 分组数据具有连续性,直方图各矩形通常是连续排列
    3. 主要用于展示数值型数据

绘图时x轴/y轴的刻度标签重叠

Matplotlib绘图时x轴标签重叠的解决办法 - 云+社区 - 腾讯云

  • 方法一:

    拉长画布

  • 方法二:

    调整刻度标签的字体大小:plt.tick_params(axis='x', labelsize=8)

  • 方法三:

    将纵向图改为横向,如条形图:plt.barh()

  • 方法四

    旋转刻度标签的角度plt.xticks(rotation=30)


Pandas常用图表简介

箱线图(箱型图/盒型图)

直方图用于显示一组数据的分布情况,而箱线图用于显示一组数据内部的分散情况。例如: 最大值、最小值,中位数,4分位数、异常值等等。

  • 箱线图各部位的含义

    箱线图各部位的含义

    • 上四分位数(QU):第75%位数

    • 下四分位数(QL):第25%位数

    • 异常值的标准:

      • IQR(Inter-Quatile range):四分位距

        IQR = QU - QLIQR包含了全部观察值的一半

      • 过小的异常值为 $<$ QL-1.5IQR的值

      • 过大的异常值 $>$ QU+1.5IQR的值

    Matplotlib中绘制箱线图的函数为DataFrame.boxplot()


一文学会matplotlib绘图

快捷键

快捷键 功能
Ctrl + R 运行查询语句
Ctrl + F 查找,并有替换选项可勾选

练习一

准备数据

建表语句

CREATE TABLE students
(sno VARCHAR(3) NOT NULL, 
sname VARCHAR(4) NOT NULL,
ssex VARCHAR(2) NOT NULL, 
sbirthday DATETIME,
class VARCHAR(5));

CREATE TABLE courses
(cno VARCHAR(5) NOT NULL, 
cname VARCHAR(10) NOT NULL, 
tno VARCHAR(10) NOT NULL);

CREATE TABLE scores
(sno VARCHAR(3) NOT NULL,
cno VARCHAR(5) NOT NULL,
degree NUMERIC(10, 1) NOT NULL);

CREATE TABLE teachers 
(tno VARCHAR(3) NOT NULL, 
tname VARCHAR(4) NOT NULL, tsex VARCHAR(2) NOT NULL, 
tbirthday DATETIME NOT NULL, prof VARCHAR(6), 
depart VARCHAR(10) NOT NULL);

插入数据

INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (108 ,'曾华' ,'男' ,'1977-09-01',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (105 ,'匡明' ,'男' ,'1975-10-02',95031);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (107 ,'王丽' ,'女' ,'1976-01-23',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, classS) VALUES (101 ,'李军' ,'男' ,'1976-02-20',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (109 ,'王芳' ,'女' ,'1975-02-10',95031);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (103 ,'陆君' ,'男' ,'1974-06-03',95031);

INSERT INTO COURSES(cno, cname, tno) VALUES ('3-105' ,'计算机导论',825);
INSERT INTO COURSES(cno, cname, tno) VALUES ('3-245' ,'操作系统' ,804);
INSERT INTO COURSES(cno, cname, tno) VALUES ('6-166' ,'数据电路' ,856);
INSERT INTO COURSES(cno, cname, tno) VALUES ('9-888' ,'高等数学' ,100);

INSERT INTO SCORES(sno, cno, degree) VALUES (103,'3-245',86);
INSERT INTO SCORES(sno, cno, degree) VALUES (105,'3-245',75);
INSERT INTO SCORES(sno, cno, degree) VALUES (109,'3-245',68);
INSERT INTO SCORES(sno, cno, degree) VALUES (103,'3-105',92);
INSERT INTO SCORES(sno, cno, degree) VALUES (105,'3-105',88);
INSERT INTO SCORES(sno, cno, degree) VALUES (109,'3-105',76);
INSERT INTO SCORES(sno, cno, degree) VALUES (101,'3-105',64);
INSERT INTO SCORES(sno, cno, degree) VALUES (107,'3-105',91);
INSERT INTO SCORES(sno, cno, degree) VALUES (108,'3-105',78);
INSERT INTO SCORES(sno, cno, degree) VALUES (101,'6-166',85);
INSERT INTO SCORES(sno, cno, degree) VALUES (107,'6-166',79);
INSERT INTO SCORES(sno, cno, degree) VALUES (108,'6-166',81);

INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (804,'李诚','男','1958-12-02','副教授','计算机系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (856,'张旭','男','1969-03-12','讲师','电子工程系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (825,'王萍','女','1972-05-05','助教','计算机系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (831,'刘冰','女','1977-08-14','助教','电子工程系');

练习题

  1. 查询student表中的所有记录的sname、ssex和class列。

    解答:

    SELECT sname, ssex, class
    FROM students;
  2. 查询教师所有的单位,即不重复的depart列。

    解答:

    SELECT DISTINCT depart
    FROM teachers;
  3. 查询students表的所有记录。

    解答:

    SELECT *
    FROM students;
  4. 查询scores表中成绩在60到80之间的所有记录。

    解答:

    # 方法一
    SELECT *
    FROM scores
    WHERE degree >= 60 AND degree <= 80;
    
    # 方法二
    SELECT *
    FROM scores
    WHERE degree BETWEEN 60 AND 80;
  5. ★查询scores表中成绩为85,86或88的记录

    解答:

    # 方法一
    SELECT *
    FROM scores
    WHERE degree IN (85, 86, 88);
    
    # 方法二
    SELECT *
    FROM scores
    WHERE degree = 85 OR degree = 86 OR degree = 88;
  6. 查询students表中**“95031”班性别为“女”**的同学记录。

    解答:

    SELECT *
    FROM students
    WHERE class = '95031' OR ssex = '女';
  7. class降序查询students表的所有记录。

    解答:

    SELECT *
    FROM students
    ORDER BY class DESC;
  8. ★以cno升序degree降序查询scores表的所有记录。

    解答:

    SELECT *
    FROM scores
    ORDER BY con, degree DESC;
  9. 查询student表中“95031”班的学生人数

    解答:

    SELECT COUNT(*) AS stu_num
    FROM students
    WHERE class = '95031';
  10. ★★★查询scores表中的最高分的学生学号和课程号。

    解答:子查询

    SELECT sno, cno
    FROM scores
    WHERE degree = (SELECT MAX(degree)
                    FROM scores);
  11. ★查询scores表中‘3-105’号课程的平均分

    解答:

    SELECT AVG(degree)
    FROM scores
    WHERE cno = '3-105';
  12. ★★★查询scores表中至少有5名学生选修的并以3开头的课程平均分数

    解答:分组HAVING子句用于过滤分组)

    # 方法一
    SELECT cno, AVG(degree) AS avg_degree
    FROM scores
    WHERE cno LIKE '3%'
    GROUP BY cno
    HAVING COUNT(*) >= 5;
    
    # 方法二
    SELECT cno, AVG(degree) AS avg_degree
    FROM scores
    GROUP BY cno
    HAVING COUNT(*) >= 5 AND cno LIKE '3%';
  13. ★★★查询scores表中最低分大于70最高分小于90的sno列。

    解答:

    SELECT sno
    FROM scores
    GROUP BY sno
    HAVING MIN(degree) > 70 AND MAX(degree) < 90;
  14. ★★查询所有学生的sname、cno和degree列。

    解答:

    • 外部联结(OUTER JOIN, OUTER可省略)
    • 内部联结(INNER JOIN)
    # 方法一:外部联结, LEFT JOIN 左边的表 所有记录都会列出
    SELECT stu.sname, sco.cno, sco.degree
    FROM students AS stu LEFT JOIN scores AS sco
    ON stu.sno = sco.sno
    ORDER BY sname;
    
    # 方法二:内部联结, INNER JOIN 只返回两个表中联结字段相等的行
    SELECT stu.sname, sco.cno, sco.degree
    FROM students AS stu INNER JOIN scores AS sco
    ON stu.sno = sco.sno
    ORDER BY sname;
  15. ★查询所有学生的sno、cname和degree列。

    解答:外部联结

    SELECT s.sno, c.cname, s.degree
    FROM scores AS s
    LEFT JOIN courses AS c ON s.cno = c.cno
    ORDER BY sno;
  16. ★★★查询所有学生的sname、cname和degree列。

    解答:外部联结

    SELECT st.sname, c.cname, sc.degree
    FROM students AS st 
    LEFT JOIN scores AS sc ON st.sno = sc.sno
    LEFT JOIN courses AS c ON sc.cno = c.cno
    ORDER BY sname;
  17. ★★★查询“95033”班所选课程的平均分。

    解答:

    SELECT cname, AVG(degree)
    FROM students AS st
    INNER JOIN scores AS sc ON st.sno = sc.sno
    INNER JOIN courses AS c ON sc.cno = c.cno
    WHERE class = '95033'
    GROUP BY c.cno
    ORDER BY cname;
  18. ★★★假设使用如下命令建立了一个grade表:

    CREATE TABLE grade(low INT(3), upp INT(3), rank CHAR(1));
    INSERT INTO grade VALUES(90,100,'A');
    INSERT INTO grade VALUES(80,89,'B');
    INSERT INTO grade VALUES(70,79,'C');
    INSERT INTO grade VALUES(60,69,'D');
    INSERT INTO grade VALUES(0,59,'E');
    COMMIT;

    现查询所有同学的sno、cno和rank列。

    解答:

    SELECT sno, cno, rank
    FROM scores INNER JOIN grade
    ON scores.degree BETWEEN grade.low AND grade.upp
    ORDER BY sno;
  19. ★★★查询选修“3-105”课程成绩高于“109”号同学成绩的所有同学的记录。

    解答:

    • 自联结
    SELECT sname, s1.degree
    FROM scores AS s1
    INNER JOIN scores AS s2 ON s1.cno = s2.cno AND  s1.degree > s2.degree
    INNER JOIN students AS st ON s1.sno = st.sno
    WHERE s1.cno = '3-105' AND s2.sno = '109'
    ORDER BY s1.degree;
  20. ★★★查询scores中选学一门以上课程的同学中分数为非最高分成绩的记录。

    解答:

    • 条件:
      1. 选学一门以上课程
      2. 列出这些同学的所有非最高分成绩
    SELECT scores.sno, cno, degree, max_degree
    FROM scores INNER JOIN 
    			(SELECT sno, MAX(degree) AS max_degree 
                 FROM scores 
                 GROUP BY sno 
                 HAVING COUNT(*) > 1) AS max 
    ON scores.sno = max.sno AND degree < max_degree
    ORDER BY sno;
  21. 查询成绩高于学号为“109”、课程号为“3-105”的成绩的所有记录。

    解答:

    SELECT st.sname, s1.cno, s1.degree
    FROM scores AS s1 INNER JOIN scores AS s2
    ON s1.cno = s2.cno AND s1.degree > s2.degree
    INNER JOIN students AS st
    ON s1.sno = st.sno
    WHERE s1.cno = '3-105' AND s2.sno = '109'
    ORDER BY s1.degree;
  22. ★★★查询和学号为108的同学同年出生的所有学生的sno、sname和sbirthday列。

    解答:函数YEAR(d)

    SELECT s1.sno, s1.sname, s1.sbirthday
    FROM students AS s1 INNER JOIN students AS s2
    ON YEAR(s1.sbirthday) = YEAR(s2.sbirthday)
    WHERE s2.sno = '108'
    ORDER BY sbirthday;
  23. 查询“张旭“教师任课的学生成绩。

    解答:

    SELECT sno, degree
    FROM scores INNER JOIN courses
    ON scores.cno = courses.cno
    INNER JOIN teachers
    ON courses.tno = teachers.tno
    WHERE teachers.tname = '张旭'
    ORDER BY degree;
  24. 查询选修某课程的同学人数多于5人教师姓名

    解答:

    SELECT tname
    FROM scores AS s INNER JOIN courses AS c
    ON s.cno = c.cno
    INNER JOIN teachers AS t
    ON t.tno = c.tno
    GROUP BY c.cno
    HAVING COUNT(c.cno) > 5;
  25. 查询95033班和95031班全体学生的记录

    解答:

    SELECT *
    FROM students
    WHERE class IN ('95033', '95031')
    ORDER BY class;
  26. 查询有85分以上成绩的课程cno。

    解答:DISTINCT关键字

    SELECT DISTINCT c.cno
    FROM scores
    WHERE degree > 85;
  27. 查询出“计算机系“教师所教课程的成绩表。

    解答:

    SELECT t.tname, s.cno, cname, sno, degree
    FROM scores AS s INNER JOIN courses AS c
    ON s.cno = c.cno
    INNER JOIN teachers AS t
    ON t.tno = c.tno
    WHERE t.depart = '计算机系'
    ORDER BY t.tname, cname, degree DESC;
  28. 查询“计算机系”中与“电子工程系“的教师不同职称教师的tname和prof。

    解答:

    SELECT tname, prof
    FROM teachers
    WHERE depart = '计算机系' AND prof NOT IN 
    								(SELECT prof
                                    FROM teachers
                                    WHERE depart = '电子工程系');
  29. 查询选修编号为“3-105”课程且成绩至少高于任意选修编号为“3-245”的同学的成绩的cno、sno和degree。

    解答:

    SELECT cno, sno, degree
    FROM scores
    WHERE cno = '3-105' AND degree > (SELECT MIN(degree)
        							FROM scores
        							WHERE cno = '3-245')
    ORDER BY degree DESC;
  30. 查询选修编号为”3-105“且成绩高于所有选修编号为”3-245“课程的同学的cno、sno和degree。

    解答:

    SELECT cno, sno, degree
    FROM scores
    WHERE cno = '3-105' AND degree > (SELECT MAX(degree)
        							FROM scores
        							WHERE cno = '3-245')
    ORDER BY degree DESC;
  31. 查询所有教师和同学的name、sex和birthday。

    解答:组合查询UNION

    • UNION中每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
    • 列数据类型必须兼容。类型不必完全相同,但必须是DBMS可以隐含地转换的类型(如不同的数值类型不同的日期类型)。
    SELECT tname AS name, tsex AS sex, tbirthday AS birthday
    FROM teachers
    UNION 
    SELECT sname AS name, ssex AS sex, sbirthday AS birthday
    FROM students
    ORDER BY birthday;
  32. 查询所有“女”教师和“女”同学的name、sex和birthday。

    解答:

    SELECT tname AS name, tsex AS sex, tbirthday AS birthday
    FROM teachers AS t
    WHERE t.tsex = '女'
    UNION 
    SELECT sname AS name, ssex AS sex, sbirthday AS birthday
    FROM students AS s
    WHERE s.ssex = '女'
    ORDER BY birthday;
  33. ★★★查询成绩比该课程平均成绩低的同学的成绩表。

    解答:

    SELECT s1.*, avg_degree
    FROM scores AS s1 INNER JOIN (
    	SELECT cno, AVG(degree) AS avg_degree
    	FROM scores
    	GROUP BY cno) AS s2
    ON s1.cno = s2.cno AND s1.degree < s2.avg_degree;
  34. 查询所有任课教师的tname和depart。

    解答:

    SELECT DISTINCT tname, depart
    FROM courses AS c INNER JOIN teachers AS t
    ON c.tno = t.tno
    ORDER BY tname;
    
    SELECT tname, depart
    FROM teachers
    WHERE tno IN(
    	SELECT tno
    	FROM courses)
    ORDER BY tname;
  35. ★查询所有未讲课的教师的Tname和Depart。

    解答:

    SELECT tname, depart
    FROM teachers
    WHERE tno NOT IN(
    	SELECT tno
    	FROM courses)
    ORDER BY tname;
  36. ★★查询至少有2名男生的班号。

    解答:

    SELECT class
    FROM students
    WHERE ssex = '男'
    GROUP BY class
    HAVING COUNT(*) >= 2;
  37. ★查询students表中**不姓“王”**的同学记录。

    解答:

    • LIKE操作符
    • 通配符%
    SELECT *
    FROM students
    WHERE sname NOT LIKE '王%';
  38. ★★查询students表中每个学生的姓名和年龄

    解答:

    • 当前日期函数CURDATE()
    • 当前日期和时间函数NOW()
    SELECT sname, TIMESTAMPDIFF(YEAR, sbirthday, CURDATE()) AS age
    FROM students;
  39. ★★查询students表中最大最小的sbirthday日期值。

    解答:

    SELECT DATE(MAX(Sbirthday)) AS max_birthday, DATE(MIN(Sbirthday)) AS min_birthday,
    FROM Students;
  40. 班号年龄从大到小的顺序查询students表中的全部记录。

    解答:

    SELECT *
    FROM students
    ORDER BY class DESC, TIMESTAMPDIFF(YEAR, sbirthday, CURDATE()) DESC;
  41. 查询“男”教师及其所上的课程

    解答:

    SELECT tname, cname
    FROM teachers AS t INNER JOIN courses AS c
    ON t.tno = c.tno
    WHERE tsex='男'
    ORDER BY tname;
  42. ★查询最高分同学的sno、cno和degree列。

    解答:

    SELECT sno, cno, degree
    FROM scores
    GROUP BY cno
    HAVING degree=MAX(degree);
  43. 查询和“李军”同性别的所有同学的sname。

    解答:

    # 方法一 子查询
    SELECT sname
    FROM students
    WHERE ssex = (
    			SELECT ssex
    			FROM students
    			WHERE sname = '李军');
    			
    # 方法二 联结表
    SELECT s1.Sname
    FROM students AS s1 INNER JOIN students AS s2
    ON s1.ssex=s2.ssex
    WHERE s2.sname = '李军';
  44. ★★查询和“李军”同性别同班的同学sname。

    解答:

    SELECT sname
    FROM students AS s1 INNER JOIN students AS s2
    ON s1.ssex = s2.ssex AND s1.class = s2.class
    WHERE s2.sname = '李军';
  45. 查询所有选修“计算机导论”课程的**“男”**同学的成绩表。

    解答:

    SELECT sname, degree
    FROM scores AS s1 INNER JOIN courses AS c
    ON s1.cno = c.cno
    INNER JOIN students AS s2
    ON s1.sno = s2.sno
    WHERE c.cname = '计算机导论' AND s2.ssex = '男'
    ORDER BY degree DESC;

练习二

准备数据

建表语句

CREATE TABLE userinfo (
  userId int(11) NOT NULL,
  sex varchar(2),
  birth date,
  PRIMARY KEY (userId)
) ENGINE = InnoDB;

CREATE TABLE orderinfo (
  orderId int(11) NOT NULL,
  userId int(11) NOT NULL,
  isPaid varchar(10),
  price float(11, 2),
  paidTime timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (orderId)
) ENGINE = InnoDB;

导入数据

  • 获取链接:用户消费行为分析数据;提取码:yu63

  • 数据文件

    • user_info_utf.csv
    • order_info_utf.csv
  • 通过navicat,选择对应的数据库表,选择导入

    1. 导入类型:CSV File

      • 导入从:选择需要导入的数据文件
      • 编码:UTF-8(默认)
    2. 记录分隔符:CRLF(默认)

      • 字段名行:若数据文件不包含数据表的列名,则输入0(默认为第1行);
      • 第一个数据行:若文件不包含数据表的列名,则输入1(默认为第2行);
      • 日期排序:注意年月日的顺序
    3. 确认目标表

    4. 确认对应的目标字段

    5. 将记录添加到目标表/删除目标表中原数据,用导入的记录代替

    6. 开始执行


练习题

  1. 统计不同月份的下单人数

    SELECT MONTH(paidTime), COUNT(DISTINCT userId)
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY MONTH(paidTime);
  2. 统计用户三月份复购率和回购率(三月份购买的用户,四月份也购买)

    # 复购率
    SELECT COUNT(cnt) AS 下单用户数, COUNT(IF(cnt > 1, 1, NULL)) AS 复购用户数
    FROM (SELECT userId, COUNT(userId) as cnt
          FROM orderinfo
          WHERE isPaid = '已支付' AND MONTH(paidTime) = 3
          GROUP BY userId) AS t;
          
    # 3月份的回购率
    SELECT t1.m, COUNT(t1.m), COUNT(t2.m) 
    FROM (SELECT userId, DATE_FORMAT(paidTime, '%Y-%m-01') AS m
    			FROM orderinfo
    			WHERE isPaid = '已支付'
    			GROUP BY userId, DATE_FORMAT(paidTime, '%Y-%m-01')) AS t1
    LEFT JOIN (SELECT userId, DATE_FORMAT(paidTime, '%Y-%m-01') AS m
    			FROM orderinfo
    			WHERE isPaid = '已支付'
    			GROUP BY userId, DATE_FORMAT(paidTime, '%Y-%m-01')) AS t2
    ON t1.userId = t2.userId AND t1.m = DATE_ADD(t2.m, INTERVAL -1 MONTH)
    GROUP BY t1.m;
  3. 统计男女用户的消费频次(平均数)是否有差异

SELECT sex, AVG(cnt)
FROM (SELECT o.userId, sex, COUNT(*) AS cnt  # count(*)的作用为 统计消费次数
      FROM orderinfo AS o
      INNER JOIN (SELECT *
                  FROM userinfo
                  WHERE sex != '') AS t
      ON o.userId = t.userId
      GROUP BY o.userId, sex) AS t2
GROUP BY sex;
  1. 统计多次消费的用户,第一次和最后一次消费间隔是多少

    SELECT userId, DATEDIFF(MAX(paidTime), MIN(paidTime)) 
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY userId
    HAVING COUNT(*) > 1;
  2. 统计不同年龄段,用户的消费金额是否有差异?

    SELECT ageGroup, AVG(cnt)
    FROM (SELECT o.userId, ageGroup, count(o.userId) AS cnt
    FROM orderinfo AS o
    INNER JOIN (SELECT userId, CEIL((YEAR(NOW()) - YEAR(birth)) / 10) AS ageGroup
    					FROM userinfo
    					WHERE birth > '1901-00-00') AS t
    ON o.userId = t.userId
    WHERE isPaid = '已支付'
    GROUP BY o.userId, ageGroup) AS t2
    GROUP BY ageGroup;
  3. 统计消费的二八法则,消费的top20%用户,贡献了多少额度

    SELECT COUNT(userId), SUM(total)
    FROM (SELECT userId, SUM(price) AS total
    	FROM orderinfo AS o
    	WHERE isPaid = '已支付'
    	GROUP BY userId
    	ORDER BY total DESC
    	LIMIT 17000) AS t;

练习三

准备数据

CREATE TABLE datafrog_test1
(userid VARCHAR(20),
changjing VARCHAR(20),
int_time VARCHAR(20)
);

INSERT INTO datafrog_test1 
VALUES 	(1,1001,1400),
		(2,1002,1401),
        (1,1002,1402),
        (1,1001,1402),
        (2,1003,1403),
        (2,1004,1404),
        (3,1003,1400),
        (4,1004,1402),
        (4,1003,1403),
        (4,1001,1403),
        (4,1002,1404),
        (5,1002,1402),
        (5,1002,1403),
        (5,1001,1404),
        (5,1003,1405);

问题

求用户id对应的前两个不同场景。(场景重复的话,选场景的第一个访问时间,场景不足两个也输出其场景)


解答


Leetcode 上的SQL题



牛客网上的SQL题

  • 获取当前薪水第二多的员工的emp_no以及其对应的薪水salary

  • 不让使用ORDER BY排序时,利用子查询MAX()

  • 查找所有员工自入职以来的薪水涨幅情况

    • 嵌套查询
  • 对所有员工的薪水按照salary进行按照1-N的排名

    • 窗口函数
    • 排序细节
  • 获取所有非manager员工当前的薪水情况

    • 使用INNER JOIN而不是LEFT JOIN
    • 一个部门里可能有多个manager,所以用NOT IN比用!=更合理
  • 获取员工其当前的薪水比其manager当前薪水还高的相关信息

    • 创建两张表(一张记录当前所有员工的工资,另一张只记录部门经理的工资)进行比较
  • 给出每个员工每年薪水涨幅超过5000的员工编号emp_no

    • 题意模糊:应该为给出薪水与去年相比丈夫超过5000的员工编号
    • 使用INNER JOIN而不是LEFT JOIN
    • 时间线的判断方法很有参考价值
  • 查找描述信息中包含robot的电影对应的分类名称以及电影数目,而且还需要该分类对应电影数量>=5部

    • 筛选方法
  • 创建一个actor表,包含如下列信息

    • 创建表的时候,有些地方必须加括号
  • 批量插入数据,不使用replace操作

    • MySQL插入数据,如果数据已存在则忽略的语句是INSERT IGNORE table_name VALUES(...);
  • 对first_name创建唯一索引uniq_idx_firstname

    • 创建索引
  • 针对actor表创建视图actor_name_view

    • 创建视图
  • 针对上面的salaries表emp_no字段创建索引idx_emp_no

    • 强制索引(MySQL 为 FORCE INDEX)
  • 构造一个触发器audit_log

    • 此触发器必须按照AFTER INSERT执行,因为在BEFORE INSERT语句执行之前,新的行数据还没有生成
      • 通常,将BEFORE用于数据验证和净化
  • 删除emp_no重复的记录,只保留最小的id对应的记录

    •   DELETE FROM table_name 
        WHERE xxx
        
      
      - [将所有to_date为9999-01-01的全部更新为NULL](https://www.nowcoder.com/practice/859f28f43496404886a77600ea68ef59?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- ```sql
      		UPDATE table_name
      		SET xxx
      		WHERE xxx;
  • 将id=5以及emp_no=10001的行数据替换成id=5以及emp_no=10005

    • REPLACE(s, s1, s2)函数
  • 将titles_test表名修改为titles_2017

    • 修改表名的两种方式
      • RENAME TABLE table_name1 TO table_name2;
      • ALTER TABLE table_name1 RENAME TO table_name2;
  • 在audit表上创建外键约束,其emp_no对应employees_test表的主键id

    •   ALTER TABLE table_name
        ADD [CONSTRAINT fk_name] -- CONSTRAINT fk_name 定义外键名,可不定义
        FOREIGN KEY (xx) REFERENCES another_table (xx);
        
      
      - [将所有获取奖金的员工当前的薪水增加10%](https://www.nowcoder.com/practice/d3b058dcc94147e09352eb76f93b3274?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- MySQL不支持`*=`这样的简化字符使用
      
      - [使用含有关键字exists查找未分配具体部门的员工的所有信息](https://www.nowcoder.com/practice/c39cbfbd111a4d92b221acec1c7c1484?tpId=82&tags=&title=&diffculty=0&judgeStatus=&rp=1)
      
      	- EXISTS和IN的选择
      	- EXISTS的用法
      
      - [获取有奖金的员工相关信息](https://www.nowcoder.com/practice/5cdbf1dcbe8d4c689020b6b2743820bf?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- CASE语句
      
      - [统计salary的累计和running_total](https://www.nowcoder.com/practice/58824cd644ea47d7b2b670c506a159a6?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- 窗口函数
      
      - [对于employees表中,给出奇数行的first_name](https://www.nowcoder.com/practice/e3cf1171f6cc426bac85fd4ffa786594?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- 窗口函数
      
      - [刷题通过的题目排名](https://www.nowcoder.com/practice/cd2e10a588dc4c1db0407d0bf63394f3?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      	- 窗口函数
      	- 排序
      
      - [异常的邮件概率](https://www.nowcoder.com/practice/d6dd656483b545159d3aa89b4c26004e?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      	
      	- COUNT函数非NULL的都会计数
      	
      - [牛客每个人最近的登录日期(三)](https://www.nowcoder.com/practice/16d41af206cd4066a06a3a0aa585ad3d?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      	- 在执行乘法`*`前,**注意括号的使用**
      	- `WHERE XX IN XXX`**可以用于对一个组合的查询**
      	
      - ⭐[牛客每个人最近的登录日期(四)](https://www.nowcoder.com/practice/e524dc7450234395aa21c75303a42b0a?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      	- 解法一:LEFT JOIN + IFNULL函数
      		- 笛卡尔积**如果WHERE不满足,整个所生成的元组就不会显示**,**如果要显示的话,就必须使用JOIN连接**
      	- 解法二:窗口函数 + SUM函数
      	
      - ⭐⭐[牛客每个人最近的登录日期(五)](https://www.nowcoder.com/practice/ea0c56cd700344b590182aad03cc61b8?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      - ⭐[牛客每个人最近的登录日期(六)](https://www.nowcoder.com/practice/572a027e52804c058e1f8b0c5e8a65b4?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      
      ---
      
      ## 最近登陆时间,增量更新:
      
      table_1是一个按天分区的 用户日志表,每次一个用户登陆都会新加一行:
      
      day           timestamp             uid
      
      2018-08-01     2018-08-01 11:00:00    1
      
      2018-08-01     2018-08-01 14:00:00    1
      
       
      
      需要根据table_1新建一个table_2:
      
      Day           uid        latest_login_time
      
      2018-08-01    1          2018-08-01 14:00:00 
      
       
      
      每天新增一个昨日的分区的全量用户表,其中latest_login_time是自用户登录起最近一次登录的时间
      
      ```sql
      CREATE TABLE tabel2 (
          day	DATE NOT NULL,
          uid	INT NOT NULL,
          `latest login time` TIMESTAMP
      );
      
      INSERT INTO table2(day,
                        uid,
                        `latest login time`)
      SELECT day, uid, MAX(`timestamp`)
      FROM table_1
      WHERE DATEDIFF(CURDATE(), day) = 1 -- 昨天
      GROUP BY uid;

累计用户数计算

table a是一个用户注册时间的记录表,一个用户只有一条数据,a表如下:

create_time uid

2018-08-01 14:07:09 111

2018-08-02 14:07:09 134

需要计算8月份累计注册用户数(从8月1日开始累计计算,8月1日为8月1日注册用户数,8月2日为8月1日-2日两天的注册用户数),计算结果格式如下:

时间 累计用户数

2018-08-01 2000000

2018-08-02 2100000

…………….

2018-08-31 10000000

SELECT DATE(create_time) AS 时间, 
		COUNT(uid) OVER (ORDER BY creat_time) AS 累计用户数
FROM `table a`
WHERE YEAR(create_time) = 2018 AND MONTH(create_time) = 8;

连续访问天数

table a 是一个用户登陆时间记录表,每登陆一次会记录一条记录,a表如下:

log_time uid

2018-07-01 12:00:00 123

2018-07-01 13:00:00 123

2018-07-02 14:08:09 456

需要计算出7月1日登陆的用户,在后续连续登陆7天,14天,30天的人数

计算结果格式如下:

7月1日登陆总用户数 连续登陆7天用户数 连续登陆14天用户数 。。。。

1000 500 200

SELECT COUNT(table_a.uid) AS '7月1日登录总用户数', COUNT(table_b.uid) AS '连续登录7天的用户数', COUNT(table_c.uid) AS '连续登录14天的用户数'
FROM (SELECT DISTINCT uid
			FROM consecutive_task
			WHERE DATE(login_time) = '2018-07-01') table_a
LEFT JOIN (SELECT uid, (login_date - rank_num) AS rnk
           FROM (SELECT uid, login_date, (ROW_NUMBER() OVER (PARTITION BY uid
                                                             ORDER BY login_date)) AS rank_num
                 FROM (SELECT uid, DATE(login_time) AS login_date
                       FROM consecutive_task
                       WHERE DATE(login_time) BETWEEN '2018-07-01' AND '2018-07-07'
                       GROUP BY uid, login_date) b1 ) b2
           GROUP BY uid, rnk
           HAVING COUNT(rnk) = 7) table_b									
ON table_a.uid = table_b.uid
LEFT JOIN (SELECT uid, (login_date - rank_num) AS rnk
           FROM (SELECT uid, login_date, (ROW_NUMBER() OVER (PARTITION BY uid
                                                             ORDER BY login_date)) AS rank_num
                 FROM (SELECT uid, DATE(login_time) AS login_date
                       FROM consecutive_task
                       WHERE DATE(login_time) BETWEEN '2018-07-01' AND '2020-07-14'
                       GROUP BY uid, login_date) c1 ) c2
           GROUP BY uid, rnk
           HAVING COUNT(rnk) = 14) table_c									
ON table_a.uid = table_c.uid;

新增用户的近7日留存率

table1:用户新增表,一个设备首次激活都新加一行:

timestamp,device 【新增的日期,新增的设备】

table2: 是一个按天分区的用户活跃表,每次一个用户登陆都会新加一行:

day,device 【活跃的日期,活跃的设备】

需要计算用户新增用户的留存数,留存率【1-7日】

计算结果格式如下:

新增日期 新增设备数 次日留存数 次日留存率 2日留存数 2日留存率 ….

timestamp 1000 500 50% 450 45%

SELECT a.first AS 新增日期, COUNT(DISTINCT a.device) AS 新增设备数, 
			COUNT(DISTINCT IF(first - b.day = 1, a.device, NULL)) AS 次日留存数, 
            	CONCAT(ROUND(COUNT(DISTINCT IF(first - b.day = 1, a.device, NULL)) / COUNT(DISTINCT a.device) * 100, 0), '%') AS 次日留存率 
FROM (SELECT a.device, DATE(a.timestamp) AS first, b.day 
      FROM table1 a LEFT JOIN table2 b 
      ON a.device = b.device) c
GROUP BY a.first; -- 按 新增用户的日期 分组

计算日活用户签到,开宝箱,阅读行为的用户各自占比

table1: 是一个按天分区的用户活跃表,每次一个用户登陆都会新加一行:

day,uid 【活跃的日期,活跃的用户id】

table2:是一个按天分区的用户行为表,每一种行为都会新加一行:

day ,uid,type 【日期,用户id,type类型:1表示签到,2表示开宝箱,3表示阅读】

需要计算,签到占日活的比例,开宝箱占日活的比例,阅读占日活的比例

计算结果格式如下:

日期 活跃用户数 签到占日活的比例 开宝箱占日活的比例 阅读占日活的比例

SELECT day AS 日期, COUNT(DISTINCT uid) AS 活跃用户数, 
			COUNT(IF(type = 1, 1, NULL)) / COUNT(uid) AS 签到占日活的比例,
			COUNT(IF(type = 2, 1, NULL)) / COUNT(uid) AS 开宝箱占日活的比例,
			COUNT(IF(type = 3, 1, NULL)) / COUNT(uid) AS 阅读占日活的比例
FROM (SELECT a.*, b.type 
      FROM table_1 AS a LEFT JOIN table_2 AS b 
      ON a.uid = b.uid AND a.day = b.day) c
GROUP BY day;

练习四

准备数据

CREATE TABLE Employee 
(
	id INT(20),
	name VARCHAR(20),
	salary INT(20),
	departmentid INT(20)
);

INSERT INTO Employee 
VALUES (1,"Joe",70000,1), 
		(2,"Henry",80000,2),
		(3,"Sam",60000,2),
		(4,"Max",90000,1);

CREATE TABLE Department 
(
    id INT(20),
    name VARCHAR(20)
);

INSERT INTO Department 
VALUES (1,"IT"),
		(2,"Sales");

练习题

找出每个部门工资最高的员工

SELECT d.name Department, e.name Employee, MAX(Salary) Salary
FROM Employee e LEFT JOIN Department d
ON e.departmentid = d.id
GROUP BY d.id;

准备数据

CREATE TABLE customer
(
    Id INT(10),
	Email VARCHAR(20)
);

INSERT INTO customer
VALUES (1,'a@b.com'),
		(2,'c@d.com'),
		(3,'a@b.com');

练习题

查找 customer 表中所有重复的电子邮箱

SELECT Email
FROM customer
GROUP BY Email
HAVING COUNT(Email)>1;

准备数据

CREATE TABLE Customers
( 
    Id INT(10),
    Name VARCHAR(20)
);

INSERT INTO Customers 
VALUES (1,'Joe'),
		(2,'Henry'),
		(3,'Sam'),
		(4,'Max');

CREATE TABLE Orders
(
	Id INT(10),
	CustomerId INT(10)
);

INSERT INTO Orders
VALUES (1,3),
		(2,1);

练习题:找出所有从不订购任何东西的客户

-- 方法一
SELECT name AS customers
FROM customers c LEFT JOIN orders o
ON c.id = o.customerId
WHERE o.id IS NULL;

-- 方法二
SELECT name AS customers
FROM customers
WHERE id NOT IN (SELECT customerid
								FROM orders);

准备数据

CREATE TABLE Scores
( 
    id VARCHAR(20), 
    score FLOAT(4,2)
);

INSERT INTO scores 
VALUES (1,3.5), 
		(2,3.65), 
		(3,4.00), 
		(4,3.85), 
		(5,4.00),
		(6,3.65);

练习题:通过查询实现分数排名

-- 窗口函数 ROW_NUMBER()
SELECT score, ROW_NUMBER() OVER (ORDER BY score DESC) AS `Rank`
FROM scores
ORDER BY score DESC;

-- ROW_NUMBER() 对应 使用变量 的做法
SELECT score, @curRank=@curRank+1 AS `Rank`
FROM scores, (SELECT @RANK:=0) r
ORDER BY score DESC;

-- RANK() (有并列名次的行,会占用下一名次的位置)对应使用变量的做法
SELECT score, `rank`
FROM (SELECT score, @curRank = IF(@preScore = score, @curRank, @incRank) AS `rank`, -- 同分同排名,不同分下一排名
                @incRank:=@incRank+1; -- 作用等同于 ROW_NUMBER()
                @preScore:=score
      FROM scores, (SELECT @curRank:=0, @preScore:=NULL, @incRank:=1) r
      ORDER BY score DESC) s;
      
-- DENSE_RANK() 对应使用变量的做法

SELECT score, `rank`
FROM (SELECT score, @curRank=IF(@preScore=score, @curRank, @curRank+1) AS `rank`,
				@preScore=score
	  FROM scores, (SELECT @curRank:=0, @preScore:=NULL) r
	  ORDER BY socre DESC) s;
	  
-- 使用CASE WHEN更简洁
SELECT score, CASE 
				WHEN score=@preScore THEN @curRank
                WHEN @preScore:=score THEN @curRank:=@curRank+1 -- @preScore:=score 赋值语句必为true
              END AS `rank`
FROM scores, (SELECT @curRank:=0, @preScore:=NULL) r
ORDER BY score DESC;

准备数据

CREATE TABLE seat 
( 
    id INT(20),
	student VARCHAR(20)
); 

INSERT INTO seat 
VALUES (1,'Abbot'),
		(2,'Doris'),
		(3,'Emerson'),
		(4,'Green'),
		(5,'Jeames');

练习题:座位id 是连续递增的,改变相邻俩学生的座位。
要求:

  • 如果学生人数是奇数,则不需要改变最后一个同学的座位。
SELECT (CASE
            WHEN id % 2 = 1 AND id != cnt THEN id+1 -- 奇数且不是最后一个
            WHEN id % 2 = 1 AND id = cnt THEN id
            ELSE id - 1 -- 偶数
        END) AS id, student
FROM seat, (SELECT COUNT(*) AS cnt -- 用于判断当前是否是最后一个学生(奇数的特殊判断)
            FROM seat) t
ORDER BY id;

准备数据

DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee
( 
		Id INT(10), 
		Name VARCHAR(20),
		Salary INT(20), 
		ManagerId INT(10)
);

INSERT INTO Employee 
VALUES (1, 'Joe', 70000, 3), 
		(2, 'Henry', 80000, 4), 
		(3, 'Sam', 60000, NULL), 
		(4, 'Max', 90000, NULL);

练习题:编写一个 SQL 查询,获取收入超过他们经理的员工的姓名。

SELECT e1.Name
FROM Employee e1 LEFT JOIN Employee e2
ON e1.ManagerId = e2.Id
WHERE e1.Salary > e2.Salary;

练习五

题目:X 市建了一个新的体育馆,每日人流量信息被记录在这三列信息中:序号 (id)、日期 (visit_date)、 人流量 (people)。
请编写一个查询语句,找出
人流量的高峰期

要求:

  • 高峰期时,至少连续三行记录中的人流量不少于100
  • 每天只有一行记录,日期随着 id 的增加而增加
CREATE TABLE stadium 
( 
    id INT(20), 
	visit_date DATE, 
	people INT(20)
); 

INSERT INTO stadium 
VALUES (1,'2017-01-01',10), 
		(2,'2017-01-02',109), 
		(3,'2017-01-03',150), 
		(4,'2017-01-04',99), 
		(5,'2017-01-05',145), 
		(6,'2017-01-06',1455), 
		(7,'2017-01-07',199), 
		(8,'2017-01-08',188);
-- 方法一:朴实的自联结
SELECT DISTINCT t1.*
FROM stadium t1, stadium t2, stadium t3
WHERE (t1.people >= 100     -- 筛选表中 人流量>=100 的行
		AND t2.people >= 100
		AND t3.people >= 100
		AND ( (t1.id - t2.id = 1 -- 错位联结,从t1的id开始连续3天有人流高峰
				 AND t1.id - t3.id = 2
			 	 AND t2.id - t3.id = 1
                ) 
             	OR (t2.id - t1.id = 1 -- 补充 t1之后只有1天人流高峰,但之前也有连续高峰 的情况
					 AND t2.id - t3.id = 2
					 AND t1.id - t3.id = 1)
				OR (t3.id - t2.id = 1 -- 补充t1为人流高峰最后一天 的情况
					 AND t2.id - t1.id = 1
					 AND t3.id - t1.id = 2) 
             )
        )
ORDER BY t1.id; 
       
       
-- 方法二:MySQL 8.0 之后开始支持 WITH AS 语句(子查询部分,作用类似于一个视图,但with as 属于一次性的,而且必须和其他sql一起使用才行) WITH子句的查询必须用括号括起来
WITH t1 AS
(SELECT id, visit_date, people,
        (id - ROW_NUMBER() OVER (ORDER BY id)) rk
FROM stadium
WHERE people >= 100)

SELECT id, visit_date, people
FROM t1
WHERE rk IN (SELECT rk 
            FROM t1
            GROUP BY rk
            HAVING COUNT(*) >= 3);       

挑战题

SELECT 日期, COUNT(DISTINCT a.uid) 活跃用户数, COUNT(DISTINCT IF(day-first=1, a.uid, NULL)) 次日留存用户数,
			COUNT(DISTINCT IF(day-first=3), a.uid, NULL) 三日留存用户数,
			COUNT(DISTINCT IF(day-first=7), a.uid, NULL) 七日留存用户数,
			CONCAT(ROUND(COUNT(DISTINCT IF(day-first=1, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 次日留存率, CONCAT(ROUND(COUNT(DISTINCT IF(day-first=3, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 三日留存率, CONCAT(ROUND(COUNT(DISTINCT IF(day-first=7, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 七日留存率
FROM (SELECT uid, DATE_FORMAT(dayno, '%Y-%m-%d') AS first
      FROM act_user_info
      WHERE app_name = '相机') a LEFT JOIN (SELECT uid, DATE_FORMAT(dayno, '%Y-%m-%d') AS day
                                           FROM act_user_info
                                           WHERE app_name = '相机') b
ON a.uid = b.uid
GROUP BY first;

PDD笔试题

2. 用户行为分析

表1——用户行为表tracking_log,大概字段有(user_id‘用户编号’,opr_id‘操作编号’,log_time‘操作时间’)

  • 统计每天符合以下条件的用户数A操作之后是B操作,AB操作必须相连
-- 使用窗口函数
SELECT DATE(log_time), COUNT(DISTINCT user_id)
FROM (SELECT DATE(log_time), user_id, opr_id,
     			LEAD(opr_id, 1) OVER() AS pre_opr
     FROM tracking_log) table_1
WHERE opr_id='A' AND pre_opr='B'
GROUP BY DATE(log_time);


-- 使用用户变量
SELECT DATE(log_time), COUNT(DISTINCT user_id)
FROM (SELECT DATE(log_time), user_id,
     			(CASE
     				WHEN @pre_opr='A' AND opr_id='B' THEN @isJoin:=True AND @pre_opr:=opr_id
      				ELSE @pre_opr:=opr_id
      			END) isJoin
      FROM tracking_log, (SELECT @pre_opr:=NULL, @isJoin:=FALSE) a) table_1
WHERE isJoin=True
GROUP BY DATE(log_time);

3. 用户新增留存分析

表1——用户登陆表user_log,大概字段有(user_id‘用户编号’,log_time‘登陆时间’)

  • 获取每日新增用户数,以及第2天、第30天的回访比例
SELECT COUNT(user_id) 新增用户数, 
		(COUNT(DISTINCT IF(DATEDIFF(DATE(log_time), 注册日期)=1, u1.user_id, NULL)) / COUNT(user_id)) AS2天回访比例, 
		(COUNT(DISTINCT IF(DATEDIFF(DATE(log_time), 注册日期)=29, u1.user_id, NULL)) / COUNT(user_id)) AS30天回访比例
FROM (SELECT user_id, DATE(MIN(log_time)) AS 注册日期
      FROM user_log
      GROUP BY user_id) u1 LEFT JOIN user_log u2
ON u1.user_id = u2.user_id
GROUP BY 注册日期;

4. 贝叶斯公式的应用

数据分析笔试题(1) - 知乎


SELECT Partition_date, 
		(CASE
			WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=1 THEN COUNT(DISTINCT t1.user_id) 
		END) AS '流失1天',
		(CASE
        	WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=2 THEN COUNT(DISTINCT t1.user_id) 
        END) AS '流失2天',
		(CASE
        	WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=3 THEN COUNT(DISTINCT t1.user_id) 
        END) AS '流失3天',
		(CASE
        	WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)>=30 THEN COUNT(DISTINCT t1.user_id) 
        END) AS '流失30天以上'
FROM (SELECT user_id, Partition_date
      FROM user_active
      WHERE daily_active_status_map=1) t1 LEFT JOIN (SELECT user_id, Partition_date
                                                     FROM usre_active
                                                     WHERE daily_active_status_map=1) t2
ON t1.user_id=t2.user_id
WHERE t2.user_id IS NULL
GROUP BY Partition_date
ORDER BY Partition_date DESC;

无法插入中文字符

  • 报错内容:

    1366 - Incorrect string value: ‘\xE6\x9B\xBE\xE5\x8D\x8E’ for column ‘sname’ at row 1

  • 原因

    MySQL默认配置:

    variable_name value
    character_set_database latin1
    character_set_server latin1

    可通过SHOW VARIABLES LIKE '%char%';查看。

  • 解决方法

    1. 修改C:\ProgramData\MySQL\MySQL Server 5.7\my.ini

      • 找到[mysql]

        在下方添加default-character-set=utf8

      • 找到[mysqld]

        在下方添加character-set-server=utf8

      • 找到[client]

        在下方添加default-character-set=utf8

    2. 重启MySQL,重新登录

      • Win + R - services.msc

      • 找到MySQL57(57为版本号),右键选择重新启动

      • Win + R - cmd(或cmder)

        通过mysql -u -root -p命令并输入密码后访问数据库。

      • 通过SHOW VARIABLES LIKE '%char%';查看,字符集变为utf8即成功修改。


  • 报错内容:

    2059 - Authentication plugin ‘caching_sha2_password’ cannot be loaded

  • 原因:

    MySQL8之前的版本中加密规则为mysql_native_password,而在MySQL8以后的加密规则为caching_sha2_password

  • 解决方法:

    在命令行中登录MySQL后,使用命令行:

    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

第1章 了解SQL

  • 数据库软件应称为DBMS(数据库管理系统)。
  • 数据库是通过DBMS创建和操纵的容器。
  • 在很大程度上说,数据库究竟是文件还是别的什么东西并不重要,因为你并不直接访问数据库;你使用的是DBMS,它替你访问数据库。
  • 模式(schema): 关于数据库布局及特性的信息。
  • 有时,模式用作数据库的同义词。遗憾的是,模式的含义通常在上下文中并不是很清晰。
  • 主键(primary key)
  • SQL(发音为字母S-Q-L或sequel)是**结构化查询语言(Structured Query Language)**的缩写。
阅读全文 »

Python和Java一样大小写敏感

变量和简单数据类型

字符串

在Python中,用引号(可以是单引号,也可以是双引号)括起的都是字符串。

使用方法修改字符串的大小写

  • title():

    将字符串中每个单词 首字母改为大写其余字母小写

  • upper()

    将字符串中所有字母改为

  • lower()

    将字符串中所有字母改为


合并(拼接)字符串

Python使用+来合并字符串。


使用方法删除多余的空格

  • strip()

    删除字符串首尾空格

  • lstrip():

    删除字符串开头的空格

  • rstrip()

    删除字符串末尾的空格


将字符串中的特定单词都替换为另一个单词

replace()

例子:

message = "I really like dogs."
message.replace('dog', 'cat')

Python2中的print语句

例如:print "Hello Python 2.7 world!"

无需将要打印的内容放在括号内。从技术上说,Python 3中的print是一个函数,因此括号必不可少。


数字

整数

  • Python用**表示次方运算

    3**2的结果为9。


浮点数


使用函数str()避免类型错误

str()将非字符串值表示为字符串


Python允许在数字中间以_分隔,提高可读性

JDK7的特性:赋值时,可以用下划线_分割过长的数字(整数&浮点数均可),提高可读性


注释

在Python中,用**井号#**标识注释。


列表简介

列表是什么

列表由一系列按特定顺序排列的元素组成,像一个。可以创建包含字母表中所有字母、数字、所有家庭成员姓名的列表,也可以加入任何东西,其中的元素之间可以没有任何关系

在Python中,用方括号[]表示列表,并用逗号来分隔其中的元素。如:

bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles)

Python会打印列表的内部表示包括方括号

['trek', 'cannondale', 'redline', 'specialized']

访问列表元素

bicycles[0],请求获取列表元素时,Python只返回该元素,而不包括方括号和引号,是整洁干净的输出

还可以对任何列表元素调用字符串方法。


索引从0开始

  • Python还为访问最后一个列表元素提供了一种特数语法:将索引指定为-1即可访问最后一个列表元素。这种约定也适用于其他负数索引,如**-2返回倒数第二个**列表元素,以此类推。

    列表为空时,这种访问最后一个元素的方式会导致错误


使用列表中的各个值

可以像使用其他变量一样使用列表中的各个值。


修改、添加和删除元素

修改列表元素

指定列表名和要修改的元素的索引,再指定该元素的新值即可。如:

motorcycles = ['honda', 'yamaha', 'suzuki']
motorcycles[0] = 'ducati'

在列表中添加元素

  • 在列表末尾添加元素:使用append()方法

    motorcycles.append('ducati')

    append()方法让动态地创建列表易如反掌。

  • 在列表中插入元素:使用insert()方法(需要指定新元素的索引和值)

    motorcycles.insert(0, 'ducati')

从列表中删除元素

  • 使用del语句删除元素

    知道要删除的元素在列表中的位置,可以用del语句。

    del motorcycles[0]

    删除元素后,其余元素索引会相应发生变动。

  • 使用**方法pop()**删除元素

    方法pop()可以删除列表末尾元素,并能让你接着使用它

    popped_motorcycle = motorcycle.pop()
  • 弹出列表中任何位置处的元素

    在方法pop()的括号中指定要删除元素的索引即可。

    first_owned = motorcycles.pop(0)

  • 使用方法remove()根据值删除元素

    不知道要从列表中删除的值所处的位置,只知道要删除元素的值是,可以使用方法remove()

    方法remove()只删除第一个指定的值。如果要删除的值可能在列表中出现多次,就需要使用循环来判断是否删除了所有这样的值


组织列表

使用方法sort()对列表进行永久性排序

  • 无参数:按字母顺序排序
  • 填入参数reverse=True:按字母逆序排序
cars = ['bmw', 'audi', 'toyota', 'subaru']

# 按字母顺序排序
cars.sort()
# 得到结果cars = ['audi', 'bmw', 'subaru', 'toyota']


# 按字母逆序排序
cars.sort(reverse=True)
print(cars)
# 得到结果cars = ['toyota', 'subaru', 'bmw', 'audi']

并非所有的值都是小写时,按字母顺序排列列表要复杂些。决定排列顺序时,有多种解读大写字母的方式,要指定准确的排列顺序可能比较复杂。但是大多数排序方式都基于本节介绍的知识。


使用函数sorted()对列表进行临时排序

print(sorted(cars))

反转列表元素

  • reverse()方法

    注意:与sort(reverse=True)中的reverse作用不同。

cars.reverse()

确定列表的长度

  • len()方法
len(cars)

使用列表时避免索引错误


错误示例:

motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[3])

即类似其他语言的数组越界访问,会引起索引错误


操作列表

遍历整个列表

语句为for xxx in xxx:(注意不要遗漏冒号

例子:

magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(magician)
  • 编写for循环时,对于用于存储列表中每个值的临时变量,可指定任何名称
  • Python根据缩进来判断代码行与前一个代码行的关系。在for循环后面,没有缩进的代码就不是循环的一部分

避免缩进错误

  • 两行独立的代码语句,如果不小心缩进,Python将会报错。

创建数字列表

使用函数range()

for value in range(1, 5):
    print(value)

打印结果为:

1
2
3
4

即输出范围左闭右开

习题:使用一个for循环打印数字1~20()

for value in range(1, 21):
    print(value)

使用range()创建数字列表

  • 要创建数字列表,可使用函数list()range()的结果直接转换成列表。
numbers = list(range(1,6))
# 得到numbers = [1, 2, 3, 4, 5]
  • 使用range()函数还可以指定步长
even_numbers = list(range(2, 11, 2))
# 得到numbers = [2, 4, 6, 8, 10]
  • 创建包含(1-10)的平方的数字列表
squares = []
for value in range(1,11):
    squares.append(value**2)
  • 对数字列表执行简单的统计计算

    • min()
    • max()
    • sum()
    digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
    min(digits)
    max(digits)
    sum(digits)

列表解析

列表解析将for循环和创建新元素的代码合并成一行,并自动附加新元素。

如创建平方数列表可以写成:

squares = [value**2 for value in range(1, 11)]

要使用这种语法,首先指定一个描述性的列表名,如squares;然后定义一个表达式,用于生成要存储到列表中的值

在上述例子中,表达式为value**2


使用列表的一部分

Python称列表的部分元素切片


切片

创建切片可指定要使用的第一个元素的索引最后一个元素的索引+1(创建范围左闭右开)。

如:

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[0:3])
  • 如果没有指定第一个索引,Python将自动从列表开头开始

    player[:4]

  • 让切片终止于列表末尾,也可通过省略终止索引实现。

    player[2:]

  • 如果想输出最后三名队员,可通过切片player[-3:]实现


遍历切片

for player in players[:3]:
    print(player);

复制列表

要复制列表,可以通过同时省略起始索引和终止索引创建一个包含整个列表的切片

例如,假设有一个列表,其中包含你最喜欢的三种食品,而你还想创建另外一个列表,其中包含一位朋友喜欢的所有食物。不过你喜欢的视频,这位朋友都喜欢,因此可以通过复制来创建这个列表:

my_foods = ['pizza', 'falafel', 'carrot cake']
friend_foods = my_foods[:]

# 如下直接赋值,无法得到两个独立的列表
friend_foods = my_foods

元组

列表可以修改的,非常适合用于存储在程序运行期间可能变化的数据集。但有时候需要创建一系列不可修改的元素,元组可以满足这种需求。

Python将不可变的列表称为元组


定义元组

元组看起来犹如列表,但使用圆括号来标识。

例如有一个大小不应改变的矩形,可将其长宽存储在一个元组中:

dimensions = (200, 50)

遍历元组中的所有值

同遍历列表相同


修改元组变量

虽然不能修改元组中的元素,但可以给存储元组的变量赋值。因此如果要修改前述矩形的尺寸,可重新定义整个元组

dimensions = (200, 50)

# 重新定义元组
dimensions = (400, 100)

设置代码格式

PEP(Python Enhancement Proposal):Python改进提案

PEP 8是最古老的PEP之一,提供了代码格式设置指南。


缩进

PEP 8建议每级缩进都使用四个空格,既提高可读性,又留下了足够的多级缩进空间。

字处理文档中,大家常常使用 制表符 而不是 空格 来缩进。对于字处理文档来说,这样做的效果很好,但混合使用制表符和空格会让Python解释器感到迷惑。每款文本编辑器都提供了将输入的制表符转换为指定数量的空格的设置。在编写代码时应该使用制表符键,但一定要对编辑器进行设置,使其在文档中插入空格而不是制表符


行长

建议每行不超过80字符,PEP 8还建议注释的行长不超过72字符,因为有些工具为大型项目自动生成文档时,会在每行注释开头添加格式化字符。在大多数编辑器中,都可以设置一个视觉标志——通常是一条垂直参考线,让我们知道不能越过的界限在什么地方。


if 语句

条件测试

值为True或False的表达式被称为条件测试

检查是否相等时,大小写的影响

  • 在Python中检查是否相等时区分大小写,两个大小写不同的值会被视为不相等

  • 如果大小写无关紧要,可将变量的值转换为小写,再进行比较:

    car = 'Audi'
    car.lower() == 'audi'
    # 结果为True

检查多个条件

  • 使用and检查多个条件(Java/C/C++ 中为 &&)
  • 使用or检查多个条件(Java/C/C++ 中为 ||)

检查特定值是否包含在列表中

有时候,执行操作前必须检查列表是否包含特定的值。例如,结束用户的注册过程前,可能需要检查用户名是否已包含在用户名列表中。在地图程序中,可能需要检查用户提交的位置是否包含在已知位置列表中。

要判断特定的值是否已包含在列表中,可使用关键字in

requested_toppings = ['mushrooms', 'onions', 'pineapple']
print('mushrooms' in requested_toppings)

检查特定值是否不包含在列表中

使用关键字not in。例如,如果有一个列表,其中包含被禁止在论坛上发表评论的用户,就可在允许用户提交评论前检查他是否被禁言:

banned_users = ['andrew', 'carolina', 'david']
user = 'marie'

if user not in banned_users:
    print(user.title() + ", you can post a response if you wish.")

布尔表达式

布尔表达式不过是条件测试的别名

布尔值通常用于记录条件,如游戏是否正在运行,或用户是否可以编辑网站的特定内容:

game_active = True
can_edit = False

if语句

简单的 if 语句

if conditional_test:
    do something

假设有一个表示某人年龄的变量,而你想知道这个人是否够投票的年龄,可使用如下代码:

age = 19
if age >= 18:
	print("You are old enough to vote!")

if - else 语句

age = 19
if age >= 18:
	print("You are old enough to vote!")
else:
	print("Sorry, you are too young to vote.")

if - elif - else 结构

在现实世界中,很多情况下需要考虑的情形都超过两个。

age = 12

if age < 4:
    price = 0
elif age < 18:
    price = 5
else:
    price = 10
    
print("Your admission cost is $" + str(price) + ".")

使用if语句处理列表

检查特殊元素

requested_toppings = ['mushrooms'. 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
    if requested_topping == 'green peppers':
        print("Sorry, we are out of green peppers right now.")
    else:
        print("Adding " + requested_topping + '.')
        
print("\nFinished making your pizza!")

确定列表不是空的

requested_toppings = []

if requested_toppings:
    for requested_topping in requested_toppings:
        print("Adding " + requested_topping + '.')    
    print("\nFinished making your pizza!")
else:
    print("Are you sure you want a plain pizza?")
  • if语句将 列表名 用在条件表达式中时,Python将在列表至少包含一个元素时返回True,并在列表为空时返回False

使用多个列表

下列示例定义两个列表,其中第一个列表包含披萨店供应的配料,第二个列表包含顾客点的配料。这次对于顾客要求的每个配料,都检查是否时披萨店供应的配料:

avaliable_toppings = ['mushrooms', 'olives', 'green peppers',
                     'pepperoni', 'pineapple', 'extra cheese']
requested_toppings = ['mushrooms'. 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
    if requested_topping in avaliable_toppings:
        print("Adding " + requested_topping + ".")
    else:
        print("Sorry, we don't have " + requested_topping + ".")
print("\nFinished making your pizza!")

字典

  • 列表:用括号[]标识

  • 元组:用括号()标识(不可修改)

  • 字典:用括号{}标识


使用字典

在Python中,字典是一系列 键-值 对。每个键都与一个值相关联,可以使用键来访问与之相关联的值可以将任何Python对象用作字典中的值

键和值之间冒号:分隔,键-值对之间逗号分隔。如:alien_0 = {'color': 'green', 'points': 5}


访问字典中的值

要获取与键相关联的,可依次指定字典名放在方括号内的键

如:

alien_0 = {'color': 'green', 'points': 5}
new_points = alien_0['points']

添加 键-值 对

字典是一种动态结构可随时添加 键-值 对。要添加 键-值 对,可依次指定字典名、用方括号括起的键相关联的值

如:

alien_0['x_position'] = 0
alien_0['y_position'] = 25
  • 键-值 对的排列顺序添加顺序不同。Python只关心 键-值 对之间的关联关系

先创建一个空字典

可先使用一对空的花括号定义一个字典,再分行添加各个 键-值 对。

使用字典存储用户提供的数据或在编写能自动生成大量 键-值 对的代码时,通常都需要先定义一个空字典


修改字典中的值

修改字典中的值,可依次指定字典名、用方括号括起的以及与该键相关联的新值


删除 键-值 对

对于字典中不再需要的信息,可使用del语句将相应的 键-值 对彻底删除。使用del语句时,必须指定字典名要删除的键

如:

del alien_0['points']
  • 删除的 键-值 对永远消失

由类似对象组成的字典

字典可以存储一个对象的多种信息,也可以存储众多对象的同一种信息

favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

遍历字典

遍历所有的 键-值 对

user_0 = {
    'username': 'efermi'.
    'first': 'enrico',
    'last': 'fermi',
	}
for key, value in user_0.items():
    print("\nKey: " + key)
    print("Value: " + value)

如例子所示,要编写用于遍历字典的for循环。可声明两个变量用于存储键和值,这两个变量可以使用任何名称。

  • 方法items()返回一个 键-值 对列表
  • 即使遍历字典时,键-值 对的返回顺序与存储顺序不同

遍历字典中的所有键

  • 方法keys()返回一个键列表

    for name in favorite_languages.keys():
        print(name.title())
  • 遍历字典时,会默认遍历所有的。因此for name in favorite_languages.keys():for name in favorite_languages:效果相同

下面遍历一下字典中的名字,但在名字为指定朋友的名字时,打印一条消息,指出其喜欢的语言:

friends = ['phil', 'sarah']
for name in favorite_languages.keys():
    print(name.title())
    if name in friends:
        print(" Hi " + name.title() +
             ", I see your favorite language is " +
             favorite_languages[name].title() + "!")
  • 还可以使用keys()确定某个人是否接受了调查:

    if 'erin' not in favorite_languages.keys():
        print("Erin, please take our poll!")
  • 方法keys()并非只能用于遍历,实际上,它返回一个包含字典中所有键的列表。


按顺序遍历字典中的所有键

要以特定的顺序返回元素,一种办法是在for循环中对返回的键进行排序。为此,可以使用函数sorted()(临时排序)来获得按特定顺序排列的键列表的副本

for name in sorted(favorite_languages.keys()):
    print(name.title() + ", thank you for taking the poll.")

遍历字典中的所有值

  • 方法values()返回一个值列表

    这种做法提取字典中所有的值,没有考虑是否重复。为剔除重复项,可使用集合(set)。(在C/C++中,set是一个内部自动递增排序不含重复元素的容器)

    for language in set(favorite_languages.values()):
        print(language.title())

嵌套

在列表中存储字典

字典alien_0包含一个外星人的各种信息,但无法存储第二个外星人的信息。如何管理成群结队的外星人呢?一种办法是创建一个外星人列表,其中每个外星人都是一个字典,包含有关该外星人的各种信息(即字典列表)。

# 创建一个用于存储外星人的空列表
aliens = []

# 创建30个绿色的外星人
for alien_number in range(0, 30):
    new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
    aliens.append(new_alien)
    
# 显示前五个外星人
for alien in aliens[:5]:
    print(alien)
print("...")

# 显示创建了多少个外星人
print("Total number of aliens: " + str(len(aliens)))
  • 获取列表长度:函数len(列表名)

在字典中存储列表

有时候,需要将列表存储在字典中。例如,你要如何描述顾客点的披萨呢?如果使用列表,只能存储要添加的披萨配料;但使用字典,还可以包含其他有关披萨的描述。

pizza = {
    'crust': 'thick',
    'toppings': ['mushrooms', 'extra cheese'],
}

for topping in pizza['topping']:
    print("\t" + topping)
  • 每当需要在字典中将一个键关联到多个值时,都可以在字典中嵌套一个列表

在本章前面有关喜欢的编程语言的示例中,如果将每个人的回答都存储在一个列表中,被调查者就可以选择多种喜欢的语言。因此,在遍历该字典的for循环中,我们需要再使用一个for循环来遍历与被调查者相关联的语言列表:

favorite_languages = {
    'jen': ['python', 'ruby'],
    'sarah': ['c'],
    'edward': ['ruby', 'go'],
    'phil': ['python', 'haskell'],
	}

for name, languages in favorite_languages.items():
    print("\n" + name.title() + " 's favorite languages are:")
    for language in languages:
        print("\t" + language.title())

在字典中存储字典

可以在字典中嵌套字典,但这样做时,代码可能很快复杂起来。


将列表转换为字典


用户输入和while循环

函数input()的工作原理

函数input()让程序暂停运行,等待用户输入一些文本获取用户输入后,Python将其存储在一个变量中,Python将用户输入解读为字符串

例如,下面的程序让用户输入一些文本,再将这些文本呈现给用户:

message = input("Tell me something, and I will repeat it back to you: ")
print(message)
  • 函数input()接受一个参数,即要向用户显示的提示或说明
  • 程序等待用户输入,并在用户按回车键后继续运行

编写清晰的程序

有时候,提示可能超过一行,例如,你可能需要指出获取特定输入的原因。这种情况下,可将提示存储再一个变量中,再将该变量传递给函数input()。

prompt = "If you tell us who you are, we can personalize the messages you see."
prompt += "\nwhat is your first name? "

name = input(prompt)

使用int()来获取数值输入

函数int()让Python将输入视为数值

height = input("How tall are you, in inches? ")
height = int(height)

在Python 2.7 中获取输入

Python 2.7应使用函数raw_input()来提示用户输入,这个函数与Python 3 中的nput()一样,将输入解读为字符串。

Python 2.7 也包含函数input(),但它将用户输入解读为Python代码,并尝试运行它们。因此最好的结果是出现错误,指出Python不明白输入的代码;最糟糕的结果是,将运行原本无意运行的代码。


while循环简介

使用标志

在要求很多条件都满足才继续运行的程序中,可定义一个变量,用于判断真个程序是否处于活动状态,这个变量被称为标志。这样,在while语句中,只需检查一个条件——while的当前值是否为True

active = True
while active:
    message = input()
    if message == 'quit':
        active = false
    else:
        print(message)

使用 break 退出循环

while True:
    city = input()
    if city == 'quit':
        break;
    else:
        print("xxxxx")

在循环中使用 continue


避免无限循环

如果程序陷入无限循环,可按Ctrl + C,也可关闭显示程序输出的终端窗口。

有些编辑器(如 Sublime Text)内嵌了输出窗口,这可能导致难以结束无限循环,因此不得不关闭编辑器来结束无限循环。


使用 While 循环来处理列表和字典

for循环是一种遍历列表的有效方式,但在for循环中不应修改列表,否则将导致Python难以跟踪其中的元素。要在遍历列表的同时对其进行修改,可使用while循环。将while循环同列表和字典结合起来使用,可收集、存储并组织大量输入,供以后查看和显示。


在列表之间移动元素

假设一个列表,其中包含新注册但还未验证的网站用户;验证这些用户后,如何将他们移到另一个已验证用户列表中呢?一种办法是使用一个while循环,在验证用户的同时将其从未验证用户列表中提取出来,再将其加入到另一个已验证用户列表中

unconfirmed_users = ['alice', 'brian', 'candace']
confirmed_users = []

# 验证每个用户,直到没有未验证用户为止
# 将每个经过验证的用户都移到已验证用户列表中
while unconfirmed_users:
    current_user = unconfirmed_users.pop()
    confirmed_user.append(current_user)
  • 方法pop()删除列表末尾用户

删除包含特定值的所有列表元素

在第3章中,我们使用方法remove()删除列表中的特定值(只删除第一个指定的值)。如果要删除列表中所有包含特定值的元素,该怎么办?

pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']

while 'cat' in pets:
    pets.remove('cat')

函数

现有函数

  • round(number, ndigits=None)
    • number:需要进行四舍五入的数字
    • ndigits: 指定的位数,按此位数进行四舍五入

定义函数

下面是一个打印问候语的简单函数:

def greet_user():
    """显示简单的问候语"""
    print("Hello!")

greet_user()
  • 使用关键字def告诉Python要定义一个函数,定义以冒号结尾
  • 第二行的文本"""显示简单的问候语"""是被称为文档字符串(docstring)注释文档字符串三引号括起,Python用它们来生成有关程序中函数的文档

向函数传递信息

def greet_user(username):
    """显示简单的问候语"""
    print("Hello, " + username.title() + "!")

greet_user('jesse')

传递实参

向函数传递实参的方式很多,可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由形参名和值组成;还可使用列表和字典

  • 关键字实参(顺序无关紧要)

    def describe_pet(animal_type, pet_name):
        """显示宠物的信息"""
        print("\nI have a " + animal_type + ".")
        print("My " + animal_type + "'s name is " + pet_name.title() + ".")
        
    describe_pet(animal_type='hamster', pet_name = 'harry')

默认值

编写函数时,可为每个形参指定默认值,那么在函数调用中就可以省略相应的实参。

def describe_pet(pet_name, animal_type='dog'):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
    
describe_pet(pet_name='willie')
  • 由于给animal_type 指定了默认值,因此在函数调用中只包含一个实参——宠物的名字。然而,Python依然将这个实参视为位置实参,因此如果函数调用中只包含宠物的名字,这个实参将关联到函数定义中的第一个形参,因此需要将pet_name放在形参列表开头。这样,就能在函数调用中只提供小狗的名字了:

    describe_pet('willie')
  • 被指定默认值的形参,可以通过显式地提供实参忽略默认值


返回值

返回简单值

def get_formatted_name(first_name, last_name):
    """返回整洁的姓名"""
    full_name = first_name + ' ' + last_name
    return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')

让实参变成可选的

def get_formatted_name(first_name, last_name, middle_name=''):
    """返回整洁的姓名"""
    if middle_name:
        full_name = first_name + ' ' + middle_name + ' ' + last_name
    else:
        full_name = first_name + ' ' + last_name
    return full_name.title()
  • Python将非空字符串解读为True

返回字典

def build_person(first_name, last_name, age=''):
    """返回一个字典,其中包含有关一个人的信息"""
    person = {'first': first_name, 'last': last_name}
    if age:
        person['age'] = age
    return person

musician = build_person('jimi', 'hendrix')

传递列表

假设有一个用户列表,我们要问候其中的每位用户。下面的示例将一个名字列表传递给一个名为greet_users()的函数,这个函数问候列表中的每个人:

def greet_users(names):
    """向列表中的每位用户都发出简单的问候"""
    for name in names:
        msg = "Hello, " + name.title() + "!"
        print(msg)
        
usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)

禁止函数修改列表

为了防止函数修改列表,可向函数传递列表的副本而不是原件

将列表的副本传递给函数,可以像下面这样做:

function_name(list_name[:])
  • 切片表示法[:]创建列表的副本

虽然像函数传递列表的副本可保留原始列表的内容,但除非有充分的理由需要传递副本否则还是应该将原始列表传递给函数。因为让函数使用现成列表避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。


传递任意数量的实参

有时候,你预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参

def make_pizza(*toppings):
    """打印顾客点的所有配料"""
    print(toppings)
    
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
  • 形参名*toppings中的星号*让Python创建一个名为toppings的空元组(用圆括号()标识,不可修改),并将收到的所有值都封装到这个元组中。

结合使用位置实参和任意数量实参

如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参关键字实参,再将余下的实参都收集到最后一个形参中

例如,如果前面的函数还需要一个表示披萨尺寸的实参必须将该形参放在形参*toppings前面

def make_pizza(size, *toppings):
    """概述要制作的披萨"""
    print("\nMaking a " + str(size) +
         "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)
    
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

使用任意数量的关键字实参

有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。可将函数编写成能够接受任意数量的 键-值 对——调用语句提供了多少就接受多少。

一个示例是创建用户简介:你知道将受到有关用户的信息,但不确定是什么样的信息。

def build_profile(first, last, **user_info):
    """创建一个字典,其中包含我们知道的有关用户的一切"""
    profile = {}
    profile['first_name'] = first
    profile['last_name'] = last
    for key, vaule in user_info.items():
        profile[key] = value
    return profile

user_profile = build_profile('albert', 'einstein',
                            location='princeton',
                            field='physics')
  • 形参名**user_info中的两个星号**让Python创建一个名为user_info的空字典(用花括号{}标识),并将收到的所有 名称-值 对都封装到这个字典中。

将函数存储在模块中

使用函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。还可以更进一步,将函数存储在被称为模块的独立文件中再将模块导入到主程序中import语句允许在当前运行的程序文件中使用模块中的代码

导入整个模块

要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件包含要导入到程序中的代码

下面来创建一个包含函数make_pizza()的模块。为此,我们将文件pizza.py中除函数make_pizza()之外的其他代码都删除:

def make_pizza(size, *toppings):
    """概述要制作的披萨"""
    print("\nMaking a " + str(size) +
         "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)

接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入到刚创建的模块,再调用make_pizza()两次:

import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
  • Python读取这个文件时,代码行import pizza让Python打开pizza.py,并将其中的所有函数都复制到这个程序中

  • 要调用被导入的模块中的函数,可指定导入的模块的名称pizza和函数名make_pizza(),并用句点分隔它们。如果你使用这种import语句导入了名为module_name.py的整个模块,就可使用下面的语法来使用其中任何一个函数:

    module_name.function_name()

导入特定的函数

from module_name import function_0, function_1, function_2
  • 通过逗号分隔函数名,可根据需要从模块中导入任意数量的函数
  • 使用这种语法,调用函数时就无需使用句点

使用 as 给函数指定别名

要给函数指定别名,需要在导入时这样做。

from module_name import function_name as fn
  • 上面的语句将函数function_name()重命名为fn()

使用 as 给模块指定别名

import module_name as mn

导入模块中的所有函数

使用星号*运算符可让Python导入模块中的所有函数

from pizza import *

make_pizza(16 'pepperoni')

import语句中的星号让Python将模块pizza中的每个函数都复制到这个文件中。由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法:Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。

最佳的做法是,要么只导入需要使用的函数,要么导入整个模块并使用句点表示法,这能让代码更清晰,更容易阅读和理解。


函数编写指南

  1. 给函数指定描述性名称,且只使用小写字母下划线

  2. 每个函数都应包含简要地阐述其功能的注释,该注释应紧跟在函数定义后面,并采用文档字符串格式

  3. 给形参指定认值时,等号两边不要有空格

    def function_name(parameter_0, parameter_1='default value')
  4. 对于函数调用中的关键字实参等号两边不要有空格

    function_name(value_0, parameter_1='value')
  5. 如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左括号后按回车键,并在下一行按两次Tab键,从而将形参列表和只缩进一层的函数体区分开来

    def function_name(
    		parameter_0, parameter_1, parameter_2,
    		parameter_3, parameter_4, parameter_5):
        function body...
  6. 如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。


高阶函数

  • range() 还可指定步长

  • map()

    def func(x):
        return x*x
    
    list(map(func, [1, 2, 3, 4, 5]))
    # 效果等价于 [i**2 for i in range(1, 6)]
    
    
    a = [1, 2, 3, 4, 5]
    list(map(str, map(func, a)))
    # 结果为 ['1', '4', '9', '16', '25']

匿名函数

使用lambda

lambda x:x*x

输入为x,输出x*x

# 避免显式构造函数func,简约
list(map(lambda x:x*x, [1, 2, 3, 4, 5]))

list(map(lambda x:'char is:'+str(x), [1, 2, 3, 4, 5]))
# 结果为 ['char is:1', 'char is:2', 'char is:3' ...]

第三方包

  • collections
import collections
a = [1, 2, 3, 1, 2, 31, 2, 1]
print(collections.Counter(a))

# 输出:Counter({1: 3, 2: 3, 3: 1, 31: 1})
  • csv
  • datetime
  • math
  • pandas
  • numpy

创建和使用类

创建 Dog 类

class Dog():
    """一次模拟小狗的简单尝试"""
    def __init__(self, name, age):
        """初始化属性name和age"""
        self.name = name
        self.age = age
        
    def sit(self):
        """模拟小狗被命令蹲下"""
        print(self.name.title() + " is now sitting.")
        
    def roll_over(self):
        """模拟小狗被命令时打滚"""
        print(self.name.title() + " rolled over!")
  • 根据约定,Python中首字母大写的名称指的是。这个类定义中的括号是空的,因为我们要从空白创建这个类
  1. 方法__init__()

    • 类中的函数称为方法。就目前而言,函数和方法唯一重要的差别是调用方法的方式

    • 上例中的方法__init__()是一个特殊的方法,每当根据Dog类创建新实例时,Python都会自动运行它

    • 在这个方法名称中,开头和末尾各有两个下划线这是一种约定,旨在避免Python默认方法与普通方法发生名称冲突

    • 这个方法的定义中,形参self必不可少,还必须位于其他形参的前面

      为何必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法将自动传入实参self每个与类相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用让实例能够访问类中的属性和方法

      我们创建Dog实例时,Python将调用Dog类的方法__init__()。我们将通过实参向Dog()传递name和age,self会自动传递

    • self为前缀的变量都可供类中的所有方法使用,我们还可以通过类的任何实例来访问这些变量

      self.name = name获取存储在形参name中的值,并将其存储到变量name中,然后该变量被关联到当前创建的实例

      像这样可以通过实例访问的变量称为属性

    • sit()roll_over()方法不需要额外的信息,因此它们只有一个形参self

  2. 在 Python 2.7 中创建类

    在 Python 2.7 中创建类时,需要做细微的修改——在括号内包含单词object:

    class ClassName(object):

根据类创建实例

my_dog = Dog('willie', 6)

遇到上述代码时,Python使用实参willie和6调用Dog类中的方法__init__()。方法__init__()并未显式地包含return语句,但Python自动返回一个表示这条小狗的实例。在这里,命名约定很有用:我们通常可以认为首字母大写的名称指的是小写的名称指的是根据类创建的实例

  1. 访问属性

    my_dog.name,在Dog类中引用这个属性使用的是self.name

  2. 调用方法

    my_dog.sit()
    my_dog.roll_over()

使用类和实例

Car 类

class Car():
    """一次模拟汽车的简单尝试"""
    
    def __init__(self, make, model, year):
        """初始化描述汽车的属性"""
        self.make = make
        self.model = model
        self.year = year
        
    def get_descriptive_name(self):
        """返回整洁的描述性信息"""
        long_name = str(self.year) + ' ' + self.make + ' ' + self.model
        return long_name.title()
    
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())

运行结果:

2016 Audi A4

给属性指定默认值

class Car():
    
    def __init__(self, make, model, year):
        """初始化描述汽车的属性"""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0
        
    def get_descriptive_name(self):
        --snip--
        
    def read_odometer(self):
        """打印一条指出汽车里程的消息"""
        print("This car has " + str(self.odometer_reading) + " miles on it.")
    
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()

修改属性的值

  1. 直接修改属性的值

    my_new_car.odometer_reading = 23
  2. 通过方法修改属性的值

    class Car():
        --snip--
        
        def update_odometer(self, mileage):
            """将里程表读数设置为指定的值"""
            self.odometer_reading = mileage
        
    my_new_car = Car('audi', 'a4', 2016)
    print(my_new_car.get_descriptive_name())
    
    my_new_car.update_odometer(23)
    my_new_car.read_odometer()

    可对方法update_odometer()进行扩展,禁止任何人将里程表读数往回调

    class Car():
        --snip--
        
        def update_odometer(self, mileage):
            """
            将里程表读数设置为指定的值
            禁止将里程表读数往回调
            """
            if mileage >= self.odometer_reading:
                self.odometer_reading = mileage
            else:
            	print("You can't roll back an odometer!")
  3. 通过方法对属性的值进行递增

    class Car():
        --snip--
        
        def update_odometer(self, mileage):
            --snip--
        
        def increment_odometer(self, miles):
            """将里程表读数增加指定的量"""
            self.odometer_reading+= miles

继承

一个类继承另一个类时,它将自动获得另一个类的所有属性和方法。原有的类称为父类,新类称为子类。子类同时还可以定义自己的属性和方法

子类的方法__init__()

创建子类的实例时,Python首先需要给父类的所有属性赋值,为此,子类的方法__init__()需要父类施以援手

下面来创建一个简单的ElectricCar类版本,它具备Car类的所有功能

class ElectricCar(Car):
    """电动汽车的独特之处"""
    
    def __init__(self, make, model, year):
        """初始化父类的属性"""
        super().__init__(make, model, year)
        
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
  • 创建子类时,父类必须包含在当前文件中,且位于子类前面
  • 定义子类时,必须在括号内指定父类的名称
  • super()是一个特殊函数,帮助Python将父类和子类关联起来父类也称为超类(superclass),名称super因此而得名。

Python 2.7 中的继承

在Python 2.7 中,继承语法稍有不同。

# 在 Python 2.7 中创建类时,需要在括号内包含单词object
class Car(object):
    def __init__(self, make, model, year):
        --snip--
        
class ElectricCar(Car):
    def __init__(self, make, model, year):
        super(ElectricCar, self).__init__(make, model, year)
  • 函数super()需要两个实参:子类名和对象self。

给子类定义属性和方法

class ElectricCar(Car):
    """电动汽车的独特之处"""
    
    def __init__(self, make, model, year):
        """
        电动汽车的独特之处
        初始化父类的属性,再初始化电动汽车特有的属性
        """
        super().__init__(make, model, year)
        self.battery_size = 70
        
    def describe_battery(self):
        """打印一条描述电瓶容量的消息"""
        print("This car has a " + str(self.battery_size) + "-kwh battery.")
        
my_tesla = ElectricCar('tesla', 'model s', 2016)
my_tesla.describe_battery()

重写父类的方法

对于父类的方法,只要它不符合子类模拟的实物的行为都可对其进行重写。为此,可在子类中定义一个与要重写的父类方法同名的方法

假设Car类有一个名为fill_gas_tank()的方法,它对全电动汽车来说毫无意义。下面演示一种重写方式:

class ElectricCar(Car):
    --snip--
    
    def fill_gas_tank(self):
        """电动汽车没有油箱"""
        print("This car doesn't need a gas tank!")

使用继承时,可让子类保留从父类继承而来的精华,并剔除不需要的糟粕。


将实例用作属性

使用代码模拟实物时,可能会发现给类添加的细节越来越多:属性方法清单以及文件越来越长。这种情况下,可能需要将类的一部分作为一个独立的类提取出来,将大型类拆分称多个协同工作的小类。

例如,不断给ElectricCar类添加细节时,我们可能会发现其中包含很多专门针对汽车电瓶的属性和方法。可以将这些属性和方法提取出来,放到另一个名为Battery的类中,并将一个Battery实例用作ElectricCar类的一个属性

class Car():
    --snip--
    
class Battery():
    """一次模拟电动汽车电瓶的简单尝试"""
    
    def __init__(self, battery_size=70):
        """初始化电瓶的属性"""
        self.battery_size = battery_size
        
    def describe_battery(self):
        """打印一条描述电瓶容量的消息"""
        print("This car has a " + str(self.battery_size) + "-kwh battery.")
        
class ElectricCar(Car):
    """电动汽车的独特之处"""
    
    def __init__(self, make, model, year):
        """
        初始化父类的属性,再初始化电动汽车特有的属性
        """
        super().__init__(make, model, year)
        self.battery = Battery()
        
my_tesla = ElectricCar('tesla', 'model s', 2016)
my_tesla.battery.describe_battery()

这看似做了很多额外的工作,但现在我们想多详细地描述电瓶都可以,且不会导致ElectricCar类混乱不堪


导入类

Python允许将类存储在模块中,然后在主程序中导入所需的模块(模块是扩展名为.py的文件包含要导入到程序中的代码)。

导入单个类

下面是模块car.py,其中只包含Car类的代码:

"""一个可用于表示汽车的类"""

class Car():
    """一次模仿汽车的简单尝试"""
    
    def __init__(self, make, model, year):
        """初始化描述汽车的属性"""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0
        
    def get_descriptive_name(self):
        """返回整洁的描述性名称"""
        long_name = str(self.year) + ' ' + self.make + ' ' + self.model
        return long_name.title()
    
    def read_odometer(self):
        """打印一条消息,指出汽车的里程"""
        print("This car has " + str(self.odometer_reading) + " miles on it.")
        
    def update_odometer(self. mileage):
        """
        将里程表读数设置为指定的值
        拒绝将历程表往回拨
        """
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")
            
    def increment_odometer(self, miles):
        """将里程表读数增加指定的量"""
        self.odometer_reading += miles

下面创建另一个文件——my_car.py,在其中导入Car类并创建其实例

from car import Car

my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23
my_new_car.reading_odometer()

导入类是一种有效的编程方式,让大部分逻辑存储在独立的文件中使主程序文件变得整洁而易于阅读


在一个模块中存储多个类

虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。

类Battery和ElectricCar都可以帮助模拟汽车,因此可以将其加入模块car.py中。


从一个模块中导入多个类

from car import Car, ElectricCar
  • 从一个模块中导入多个类时,用逗号分隔各个类

导入整个模块

可以导入整个模块,再使用句点表示法访问需要的类。这中导入方法很简单,代码也易于阅读。由于创建类实例的代码都包含模块名,因此不会与当前文件使用的任何名称发生冲突

import car

my_beetle = car.Car('volkswagen', 'beetle', 2016)

my_tesla = car.ElectricCar('tesla', 'roadster', 2016)

导入模块中的所有类

要导入模块中的每个类,可使用下面的语法:

from module_name import *

但是不推荐这种导入方式(同导入模块中的所有元素),原因有二:

  1. 这种导入方式没有明确地指出你使用了模块中的哪些类
  2. 还可能引发名称方面的困惑。如果不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误

需要从一个模块中导入很多类时,最好导入整个模块,并module_name.class_name语法来访问类


在一个模块中导入另一个模块

有时候,需要将类分散到多个模块中,以免模块太大,或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。这种情况下,可在前一个模块中导入必要的类。

例如,下面将Car类存储在一个模块中,并将ElectricCar和Battery类存储再另一个模块中。我们将第二个模块命名为electric_car.py:

"""一组可用于表示电动汽车的类"""

from car import Car

class Battery():
    --snip--
    
class ElectricCar(Car):
    --snip--

现在可以分别从每个模块中导入类,以根据需要创建任何类型的汽车了:

from car import Car
from electric_car import ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())

Python 标准库

Python标准库是一组模块,安装好的Python都包含它。可使用标准库中的任何函数和类,为此只需在程序开头包含一条简单的import语句

下面来看模块collection中的一个类——OrderedDict。要创建字典并记录其中的键-值对的添加顺序,即可使用模块collections中的OrderedDict类。再来看一看第6章的favorite_languages.py示例:

from collections import OrderedDict

favorite_languages = OrderedDict()

favorite_languages['jen'] = 'python'
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'python'

for name, language in favorite_languages.items():
    print(name.title() + "'s favorite language is " +
         language.title() + ".")

这是一个很不错的类,它兼具列表和字典的主要优点(在将信息关联起来的同时保留原来的顺序)。

模块random

模块random包含以各种方式生成随机数的函数,其中的randint()返回一个位于指定范围内的整数

例如,下面的代码返回一个1~6内的整数:

from random import randint
x = randint(1, 6)

类编码风格

  • 类名:应采用驼峰命名法
  • 实例名模块名:应采用小写格式,并在单词之间加上下划线。
  • 中,可使用一个空行分隔方法;在模块中,可使用两个空行分隔类
  • 需要同时导入标准库中的模块和自己编写的模块时,先编写导入标准库模块的import语句,再添加一个空行,然后编写导入自己编写的模块的import语句。这种做法让人更容易明白程序使用的各个模块来自何方

文件和异常

从文件中读取数据

文本文件可存储的数据量多的难以置信,每当需要分析或修改存储在文件中的信息时,读取文件都很有用,对数据分析应用程序来说尤其如此。例如可以编写一个这样的程序:读取一个文本文件的内容,重新设置这些数据的格式并将其写入文件,让浏览器能够显示这些内容

要使用文本文件中的信息,首先需要将信息读取到内存中。为此,可以一次性读取文件的全部内容,也可以每次一行逐步读取

读取整个文件

首先创建一个文件,它包含精确到小数点后30位的圆周率值,且在小数点后每10位处都换行

3.1415926535
  8979323846
  2643383279

将上述文件保存为pi_digits.txt保存到本章程序所在的目录中

下面的程序打开并读取这个文件,再将其内容显示到屏幕上

with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents)

再这个程序中,第一行代码做了大量的工作

  • 函数open()

    • 接受一个参数——要打开的文件的名称
    • Python在当前执行的文件所在的目录中查找指定的文件
    • 返回一个表示文件的对象,Python将这个对象存储在我们将在后面使用的变量中。
  • 关键字with不需要访问文件后将其关闭

    • 也可以调用open()close()来打开和关闭文件,但这样做时,如果程序存在bug导致close()语句未执行,文件将不会关闭

      如果在程序中过早调用close(),你会发现需要使用文件时它已关闭(无法访问),这会导致更多的错误

    • 并非在任何情况下都能轻松确定关闭文件的恰当时机,但通过关键字with,可以让Python确定合适的时机自动关闭文件

  • 有了表示文件的对象后,使用方法read()读取这个文件的全部内容作为一个字符串

    • read()到达文件末尾时返回一个空字符串,这个空字符串显示出来就是一个空行
    • 删除末尾的空行,可在print语句中使用rstrip()print(contents.rstrip())

文件路径

Python默认在当前执行的文件所在的目录中查找指定的文件,但有时可能要打开不在程序文件所属目录中的文件。要让Python打开不与程序文件位于同一个目录中的文件,需要提供文件路径

  • 相对文件路径:

    相对于当前运行的程序所在目录的路径。

    • 在 Linux 和 OS X 中,可以这样编写代码:

      with open('text_files/filename.txt') as file_object:

      这行代码让Python到当前文件夹下的text_files文件夹中寻找指定的.txt文件。

    • 在 Windows 系统中,在文件路径中使用反斜杠(\):

      with open('text_files\filename.txt') as file_object:

  • 绝对文件路径:

    将文件在计算机中的准确位置告诉Python。

    绝对路径通常比相对路径更长,因此将其存储在一个变量中,再将该变量传递给open()会有所帮助。

    • 在 Linux 和 OS X 中,可以这样编写代码:

      file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
      with open(file_path) as file_object:
    • 在 Windows 系统中,它们类似于下面这样:

      file_path = 'C:\Ysers\ehmatthes\other_files\text_files\filename.txt'
      with open(file_path) as file_object:

    注意:

    • Windows系统有时能够正确解读文件路径中的斜杠。如果使用Windows系统,且结果不符合预期,请确保在文件路径中使用的是反斜杠
    • 反斜杠在Python中被视为转义标记,为在Windows中确保万无一失,应以原始字符串的方式指定路径,即在开头的单引号前加上r(以r开头,那么说明后面的字符,都是普通的字符了,即如果是\n将表示一个反斜杠字符,一个字母n,而不是表示换行了)。

逐行读取

读取文件时,常常需要检查其中的每一行:你可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。例如,你可能要遍历一个包含天气数据的文件,并使用天气描述中包含字样sunny的行;在新闻报道中,你可能会查找包含标签<headline>的行,并按特定的格式设置它。

要以每次一行的方式检查文件,可对文件对象使用for循环

filename = 'pi_digits.txt'

with open(filename) as file_object:
    for line in file_object:
        print(line)

我们打印每一行时,发现空白行更多了:

3.1415926535

  8979323846
  
  2643383279

为什么会出现这些空白行呢?因为在这个文件中每行的末尾都有一个看不见的换行符,而print语句也会加上一个换行符。要消除这些多余的空白行,可在print语句中使用rstrip()print(line.rstrip())


创建一个包含文件各行内容的列表

使用关键字with时,open()返回的文件对象只在with代码块内可用。如果要在with代码块外访问文件的内容,可在with代码块内将文件的各行存储在一个列表中,并在with代码块外使用该列表:

filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()
    
for line in lines:
    print(line.rstrip())
  • 方法readlines()从文件中读取每一行,并将其存储在一个列表中

使用文件的内容

首先创建一个字符串,它包含文件中存储的所有数字,且没有任何空格

filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()
    
pi_string = ''
for line in lines:
    pi_string += line.rstrip()
    
print(pi_string)
print(len(pi_string))

打印结果:

3.1415926535  8979323846  2643383279
36

在变量pi_string存储的字符串中,包含原来位于左边的空格,为删除这些空格,**可使用strip()**而不是rstrip()

注意:

读取文本文件时,Python将其中的文本都解读为字符串。如果读取的是数字,并要将其作为数值使用,就必须使用函数int()float()转换为数字。


包含小数点后一百万位的大型文件

只要系统内存足够多,想处理多少数据都可以。


圆周率值中包含你的生日吗

为确认某个人的生日是否包含在圆周率值得前1 000 000位中,可将生日表示为一个由数字组成得字符串,再检查这个字符串是否包含在pi_string中:

filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()
    
pi_string = ''
for line in lines:
    pi_string += line.strip()
    
birthday = input("Enter your birthday, in thje form mmddyy: ")
if birthday in pi_string:
    print("Your birthday appears in the first million digits of pi!")
else:
    print("Your birthday does not appear in the first million digits of pi.")

写入文件

保存数据最简单的方式之一是将其写入到文件中

写入空文件

要将文本写入文件,在调用open()时需要提供另一个实参,告诉Python要写入打开的文件

filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.")
  • 实参'w'告诉Python,我们要以写入模式打开这个文件。

  • 打开文件时,可指定模式

    • 读取模式(‘r’
    • 写入模式(‘w’
    • 附加模式(‘a’)
    • 读写模式(‘r+’

    如果省略了模式实参,默认以只读模式打开文件。

  • 如果要写入的文件不存在,函数open()自动创建它。然而,以写入(‘w’)模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件

  • 文件对象的方法write()将一个字符串写入文件

  • Python只能将字符串写入文本文件,要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。


写入多行

函数write()不会在写入的文本末尾添加换行符,要让每个字符串都单独占一行,需要在write()语句中包含换行符。


附加到文件

如果要给文件添加内容,而不是覆盖原有内容,可以**附加模式(‘a’)**打开文件。如果指定的文件不存在,Python会创建一个空文件。

filename = 'programming.txt'

with open(filename, 'a') as file_object:
    file_object.write("I also love finding meaning in large datasets.\n")
    file_object.write("I love creating apps that can run in a browser.\n")

异常

Python使用被称为异常的特殊对象管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果编写了处理该异常的代码,程序将继续运行;如果未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告

异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示编写的友好的错误消息,而不是令用户迷惑的traceback

  • ZeroDivisionError 异常

    ZeroDivisionError就是一个异常对象

  • ValueError 异常

    尝试将非数字文本转换为数字时,将引发ValueError

  • FileNotFoundError 异常


使用 try-except 代码块

当你认为可能发生了错误时,可编写一个try-except代码块来处理可能引发的异常。

处理ZeroDivisionError异常的try-except代码块类似于下面这样:

try:
    print(5/0)
except ZeroDivisionError:
    print("You can't divide by zero!")

使用异常避免崩溃

发生错误时,如果程序还有工作没有完成妥善地处理错误就尤其重要。这种情况经常会出现在要求用户提供输入的程序中;如果程序能够妥善地处理无效输入就能再提示用户提供有效输入,而不至于崩溃


else 代码块

将可能引发错误地代码放在try-except代码块中,可提高这个程序抵御错误的能力。依赖于try代码块成功执行的代码都应放到else代码块中:

try:
    answer = int(first_number) / int(second_number)
except ZeroDivisionError:
    print("You can't divide by 0!")
else:
    print(answer)

处理 FileNotFoundError 异常

filename = 'alice.txt'

try:
    with open(filename) as f_obj:
        contents = f_obj.read()
except FileNotFoundError:
    msg = "Sorry, the file " + filename + " does not exist."
    print(msg)

如果文件不存在,这个程序什么都不做,因此错误处理代码的意义不大。


分析文本

下面来提取童话Alice in Wonderland的文本,并尝试计算它包含多少个单词

我们将使用方法split(),它根据一个字符串创建一个单词列表。方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储在一个列表中

filename = 'alice.txt'

try:
    with open(filename) as f_obj:
        contents = f_obj.read()
except FileNotFoundError:
    msg = "Sorry, the file " + filename + " does not exist."
    print(msg)
else:
    # 计算文件大致包含多少个单词
    words = contents.split()
    num_words = len(words)
    print("The file " + filename + " has about " + str(num_words) + " words.")

使用多个文件

下面多分析几本书。我们先将这个程序的大部分代码移到一个名为count_words()的函数中,这样对多本书进行分析时将更容易:

def count_words(filename):
    """计算一个文件大致包含多少个单词"""
    try:
    	with open(filename) as f_obj:
        	contents = f_obj.read()
	except FileNotFoundError:
        msg = "Sorry, the file " + filename + " does not exist."
        print(msg)
    else:
        # 计算文件大致包含多少个单词
        words = contents.split()
        num_words = len(words)
        print("The file " + filename + " has about " + str(num_words) + " words.")
filename = 'alice.txt'
count_words(filename)

失败时一声不吭

要让程序在失败时一声不吭,可通过pass语句,在except代码块中明确表明什么都不做

def count_words(filename):
    """计算一个文件大致包含多少个单词"""
    try:
        --snip--

	except FileNotFoundError:
        pass
    else:
        --snip--

pass语句还充当了占位符。它提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。在这个程序中,我们可能决定将找不到的文件的名称写入到文件missing_files.txt中。


使用 模块json 存储数据

很多程序要求用户输入某种信息,如让用户存储游戏首选项提供可视化的数据。不管专注的是什么,程序都把用户提供的信息存储在列表和字典等数据结构中。用户关闭程序时,几乎总是要保存他们提供的信息,一种简单的方式是使用模块json来存储数据

  • 模块json能将简单的Python数据结构转储到文件中并在程序再次运行时加载该文件中的数据
  • 还可以使用json在Python程序之间分享数据
  • JSON数据格式不是Python专用,因此能够将以JSON格式储的数据与使用其他编程语言的人分享
  • JSON是一种轻便格式,很有用,也易于学习
  • **JSON(JavaScript Object Notation)**格式最初是为JavaScript开发的,但随后成了一种常见格式,被众多语言采用

我们来编写程序,使用json.dump()存储一组数字,使用json.load()将这些数字读取到内存中

使用 json.dump()

  • 函数json.dump()接受两个实参:
    1. 要存储的数据
    2. 可用于存储数据的文件对象
import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'
with open(filename, 'w') as f_obj:
    json.dump(numbers, f_obj)
  • 先导入模块json,再创建一个数字列表
  • 通常使用文件扩展名.json指出文件存储的数据为JSON格式
  • 使用函数json.dump()将数字列表存储到文件numbers.json中

使用 json.load()

import json

filename = 'numbers.json'
with open(filename) as f_obj:
    numbers = json.load(f_obj)
    
print(numbers)

保存和读取用户生成的数据

来看这样一个例子:用户首次运行程序时被提示输入自己的名字,这样再次运行程序时就记住他了

  • 先存储用户的名字:

    import json
    
    username = input("What is your name? ")
    
    filename = 'username.json'
    with open(filename, 'w') as f_obj:
        json.dump(username, f_obj)
        print("We'll remember you when you come back, " + username + "!")
  • 再编写一个程序,向其名字被存储的用户发出问候:

    import json
    
    filename = 'username.json'
    
    with open(filename) as f_obj:
        username = json.load(f_obj)
        print("Welcome back, " + username + "!")

我们需要将这两个程序合并到一个程序中。这个程序运行时,我们将尝试从文件username.json中获取用户名。因此首先编写一个尝试恢复用户名的try代码块。如果这个文件不存在,我们就在except代码块中提示用户输入用户名,并将其存储在username.json中,以便程序再次运行时能够获取它:

import json

# 如果以前存储了用户名,就加载它
# 否则,就提示用户输入用户名并存储它
filename = 'username.json'
try:
    with open(filename) as f_obj"
    username = json.load(f_obj)
except FileNotFoundError:
    username = input("What is your name? ")
    with open(filename, 'w') as f_obj:
        json.dump(username, f_obj)
        print("We'll remember you when you come back, " + username + "!")
else:
    print("Welcome back, " + username + "!")

重构

你经常会遇到这样的情况:代码能够正确地运行,但可做进一步的改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构重构让代码更清晰、更易于理解、更容易扩展

要重构上述程序,可将大部分逻辑放到一个或多个函数中。

import json

def greet_user():
    """问候用户,并指出其名字"""
    filename = 'username.json'
    try:
        with open(filename) as f_obj:
            username = json.load(f_obj)
    except FileNotFoundError:
        username = input("What is your name? ")
        with open(filename, 'w') as f_obj:
            json.dump(username, f_obj)
            print("We'll remember you when you come back" + username + "!")
    else:
        print("Welcome back, " + username + "!")
        
greet_user()

下面来重构greet_user(),让它不执行这么多任务:

import json

def get_stored_username():
    """如果存储了用户名,就获取它"""
    filename = 'username.json'
    try:
        with open(filename) as f_obj:
            username = json.load(f_obj)
    except FileNotFoundError:
        return None
    else:
        return username
    
def get_new_username():
    """提示用户输入用户名"""
    username = input("What is your name? ")
    filename = 'username.json'
    with open(filename. 'w') as f_obj:
        json.dump(username, f_obj)
    
def greet_user():
    """问候用户,并指出其名字"""
    username = get_stored_username()
    if username:
        print("Welcom back, " + username + "!")
    else:
        username = get_new_username()
        print("We'll remember you when you come back" + username + "!")

greet_user()

在这个版本中,每个函数都执行单一而清晰的任务


测试代码

测试函数

下面是一个简单的函数,它接受名和姓并返回整洁的姓名:

def get_formatted_name(first, last):
    """生成整洁的姓名"""
    full_name = first + ' ' + last
    return full_name.title()

为核实函数像期望的那样工作,来编写一个使用这个函数的程序:

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
    first = input("\nPlease give me a forst name: ")
    if first == 'q':
	    break
    last = input("Please give me a last time: ")
    if last == 'q':
        break
        
    formatted_name = get_formatted_name(first, last)
    print("\tNeatly formatted name: " + formatted_name + '.')

现在假设我们要修改get_formatted_name(),使其还能处理中间名。这样做时,我们要确保不破坏这个函数处理只有名和姓的姓名的方式。为此,我们可以在每次修改get_formatted_name()后都进行测试:运行程序names.py,并输入像Janis Joplin这样的姓名,但这太繁琐了。所幸Python提供了一种自动测试函数输出的高效方式。

单元测试和测试用例

Python标准库中的模块unittest提供了代码测试工具

  • 单元测试:用于核实函数的某个方面没有问题
  • 测试用例:是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。

良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖


可通过的测试

创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。

下面是一个只包含一个方法的测试用例,它检查函数get_formatted_name()在给定名和姓时能否正确地工作:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """测试name_function.py"""
    
    def test_first_last_name(self):
        """能够正确地处理像Janis Joplin 这样的姓名吗? """
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')
        
unittest.main()
  • 代码行unittest.main()让Python运行这个文件中的测试

    .
    ---------------------------------------------------------
    Ran 1 test in 0.000s
    
    OK
    • 第一行的句点.表明有一个测试通过了
    • 最后的OK表明该测试用例中的所有单元测试都通过了
  • 测试类的命名最好让它看起来与要测试的函数相关,并包含字样Test。这个类必须继承unittest.TestCase类这样Python才知道如何运行你编写的测试

  • 我们运行上述程序时,所有以test_打头的方法都会自动运行

  • unittest最有用的功能之一:一个断言方法。

    断言方法用来核实得到的结果是否与期望的结果一致(应该满足的条件是否确实满足)。上述代码通过调用unittest的方法assertEqual(),并向它传递formatted_name'Janis Joplin'


不能通过的测试

测试未通过时结果是什么样的呢?我们来修改get_formatted_name(),使其能够处理中间名,但这样做时,故意让这个函数无法正确地处理像Janis Joplin这样只有名和姓的姓名。

下面是函数get_formatted_name()的新版本,它要求通过一个实参指定中间名

def get_formatted_name(first, middle, last):
    """生成整洁的姓名"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

这次运行测试代码,将会得到如下输出:

E
=======================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
------------------------------------------------------
...
...
...
------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
  • 第一行字母E指出测试用例中有一个单元测试导致了错误
  • 最后一行指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误

添加新测试

我们再编写一个测试,用于测试包含中间名的姓名。为此,在NamesTestCase类中再添加一个方法:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """测试name_function.py"""
    
    def test_first_last_name(self):
        """能够正确地处理像Janis Joplin 这样的姓名吗? """
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')
        
    def test_first_last_middle_name(self):
        """能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗? """
        formatted_name = get_formatted_name(
        	'wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')
        
unittest.main()
  • 测试方法名必须以test_打头,这样它才会在我们运行test_name_function.py时自动运行。
  • 可以在TestCase类中使用很长的方法名,这些方法名必须是描述性的,这样才能让你明白测试未通过时的输出。

两个测试都通过的输出:

..
-------------------------------------------------
Ran 2 tests in 0.000s

OK

测试类

各种断言方法

Python在unittest.TestCase类中提供了很多断言方法

6个常用的断言方法:

方法 用途
assertEqual(a, b) 核实a == b
assertNotEqual(a, b) 核实a != b
assertTrue(x) 核实x为True
assertFalse(x) 核实x为False
assertIn(item, list) 核实item在list中
assertNotIn(item, list) 核实item不在list中

一个要测试的类

类的测试与函数的测试相似——所做的大部分工作都是测试类中方法的行为,但存在一些不同之处,下面来编写一个类进行测试。

来看一个帮助管理匿名调查的类:

class AnonymousSurvey():
    """收集匿名调查问卷的答案"""
    
    def __init__(self, question):
        """存储一个问题,并为存储答案做准备"""
        self.question = question
        self.responses = []
        
    def show_questinon(self):
        """显示调查问卷"""
        print(self.question)
        
    def store_response(self, new_response):
        """存储单份调查答卷"""
        self.responses.append(new_response)
        
    def show_results(self):
        """显示收集到的所有答卷"""
        print("Survey results:")
        for response in self.responses:
            print('- ' + response)

为证明AnonymousSurvey类能够正确地工作,我们来编写一个使用它地程序:

from survey import AnonymousSurvey

# 定义一个问题,并创建一个表示调查地AnonymousSurvey对象
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# 显示问题并存储答案
my_survey。show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)
    
# 显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

AnonymousSurvey类可用于进行简单的匿名调查。假设我们将它放在了模块survey中,并想进行改进让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次再编写一个类,用于管理非匿名调查

进行上述修改存在风险,可能会影响AnonymousSurvey类的当前行为。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。


测试 AnonymousSurvey 类

下面来编写一个测试,对AnonymouSurvey类的行为进行验证:如果用户面对调查问题时只提供了一个答案,这个答案也能被妥善地存储;用户提供三个答案时,也将被妥善地存储:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""
    
    def test_store_single_response(self):
        """测试单个答案会被妥善地存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')
        
        self.assertIn('English', my_survey.responses)
        
    def test_store_three_responses(self):
        """测试三个答案会被妥善地存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        for response in responses:
            my_survey.store_response(response)
            
        for response in responses:
            self.assertIn(response, my_survey.responses)
        
unittest.main()

上述做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。


方法 setUp()

unittest.TestCase类中包含了方法setUp()让我们只需创建这些对象一次,并在每个测试方法中使用它们。如果在TestCase类章包含了方法setUp(),Python将先运行它,再运行各个以test_打头的方法。这样,在每个测试方法中都可使用在方法setUp()中创建的对象了

下面使用setUp()来创建一个调查对象和一组答案,供方法test_store_single_response()test_store_three_responses()使用:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""
    
    def setUp(self):
        """
        创建一个调查对象和一组答案,供使用的测试方法使用
        """
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']
        
    def test_store_single_response(self):
        """测试单个答案会被妥善地存储"""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)
        
    def test_store_three_responses(self):
        """测试三个答案会被妥善地存储"""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)
        
unittest.main()

方法setUp()做了两件事:

  1. 创建一个调查对象
  2. 创建一个答案列表

存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用

测试自己编写的类时,方法SetUp()让测试方法编写起来更容易:可setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例

注意:

运行测试用例时,每完成一个单元测试,Python都打印一个字符测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F


Python项目

数据可视化

0%