Python_tutorial
Carpe Tu Black Whistle

写在前面

  • 课程网站:廖雪峰_Python教程

  • 主要内容分为: Python基础、函数编程、高级语法特性、面向对象 ## 都不会太深入(开发人员可能需要很懂

  • 内容很多,其实这可能是我,第四次点开廖老师的网站了。因为想把机器学习的作业做好一点。先前做到数据挖掘可视化的地方,人直接尬住。就很吃瘪。
    这里主要是,为了给学习使用 NumPy、Pandas还有Matplotlib打下基础。

真正开始做AI相关的科研学习过程中,一定会遇到一些自己以前从来没见过的,函数跟库。不同科研人员编写代码的习惯也不同。
但是问题不大,到时候看文献查文档就是了。

这里再推一波自己去北大软微神仙室友的Python教程:
https://blog.csdn.net/zimuzi2019/article/details/127195751?spm=1001.2014.3001.5502

具体学习方法

  1. 自己配置开发环境
  2. 看着我这篇的大纲还有廖老师的博客学习
  3. 可以不看我写了什么,这篇tutorial其实就是一个语言学习划的提纲
  4. 一定要自己写代码

面向AI的极简Python学习,我们开始吧!

安装

任何Python的教程都会教,配置开发环境,我就不做。给三种Python环境配置。

Python官网法

https://www.python.org/

Pycharm环境配置

https://www.jetbrains.com/pycharm/

Anaconda环境配置

https://www.anaconda.com/
一般我们安装Anaconda两个方法

  1. 挂梯子上官网
  2. 上清华源

vscode

我是用VScode作为开发环境的,其实vscode只是一个文本编辑器,安装插件后,既可以跑ipynb的文件(像jupyter notebook一样)也能跑py文件(其实就是调用CPython解释器)

第三方库

见后文的 安装第三方库

第一个程序

image

输入与输出 I/O

Output

1
2
3
4
5
6
'''用 print() 在括号中加上字符串,就可以向屏幕上输出指定的文字。 比如输出 'hello_world', 用代码实现如下:'''
>>> print('hello, world')
'''print()函数可以接受多个字符串,用逗号","隔开,就可以连成一串输出:'''
print('The quick brown fox', 'jumps over','the lazy dog')
'''print()还可以打印整数'''
>>> print('100 + 200 =', 100 + 200)

以上接受过个字符串,遇到逗号”,”会输出一个空格

image

Input

#input 函数的输入,读取到的数据类型为 字符串,要使得其用于其他用途,要进行类型转换。

1
2
3
4
5
6
7
'''Python 提供了一个 input(),可以让用户输入字符串,并存放到一个变量里'''
>>> name = input()
# 输入 name = input() 并按下回车后,Python交互式命令行就在等待输入,可以输入任何字符,然后按回车后,完成输入。
Michael
>>> name
'Michael'
>>>

image
一个交互性比较强的输入输出代码

1
2
name = input('please enter your name:')
print('hello', name)

image

格式化输出

学好格式化输出,可以帮你避免 看得懂别人写的代码,自己写不出来的尴尬
一般深度学习打表用的是第三种,f-string 格式化输出

格式化方法1

在Python中,采用的格式化方式和C语言是一致的,用 % 实现,举例如下:

1
2
3
4
>>> 'Hello,%s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' %('Michael', 1000000)
'Hi, Michael, you have $1000000.'

%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略

常见占位符表

整体格式化的形式和C语言相同

占位符替换内容
%d整数
%f浮点数
%s字符串
%x十六进制整数

格式化方法2

另一种 格式化字符串输出 的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}…,不过这种方式写起来比%要麻烦得多:

1
2
>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'

格式化方法3

以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:

1
2
3
4
>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62。

比较常见的,打印训练信息的格式化输出。

Python基础

  1. Python作为计算机编程语言,不能有歧义
  2. 以#开头的语句是注释,内容任意,解释器会忽略注释。
  3. 当语句以冒号:结尾时,缩进的语句视为代码块。

数据类型

Python有以下几种,可以直接处理的数据类型

整数

  • 程序上的表示方法和数学上的写法一摸一样: 1, 100, -8080, 0
  • 十六进制表示法,0x前缀和0-9,a-f表示: 0xff00, 0xa5b4c3d2
  • 很大的数字Python允许下划线_分隔。

浮点数

浮点数在计算机内部存储方式与整数不同,不同于整数,浮点数有舍入误差
很大或很小的浮点数,就必须用 科学计数法 表示,把10用e替代: 1.23e9, 1.2e-5 这样

字符串

单引号‘或者双引号“括起来的版本,比如’abc’,’xyz’,如果字符串中有一些特殊字符,用转义字符\来标识

1
2
>>> 'I\'m \"OK\"!'
# 输入的字符串为 I'm "OK"!

转义字符 \ 可以转义很多字符,比如 \n 表示换行,\t 表示制表符,字符 \ 本身也要转义,所以 \ 表示的字符就是 \,可以在Python的交互式命令行用 print() 打印字符串看看:

1
2
3
4
5
6
7
8
>>> print('I\'m ok.')
I'm ok.
>>> print('I\'m learning\nPython.')
I'm learning
Python.
>>> print('\\\n\\')
\
\

若字符串中有大量的字符需要转义, 为了方便,Python允许用 r’’表示’’内部的字符串默认不转义,r:raw

1
2
3
4
>>> print('\\\t\\')
\ \
>>> print(r'\\\t\\')
\\\t\\

字符串内部有大量换行,用 \n 写在一行内不好阅读,为了简化,Python允许用’’’…’’’的格式表示多行内容

1
2
3
4
5
6
>>> print('''line1
... line2
... line3''')
line1
line2
line3

在写py文件的时候,不需要写…,这是交互命令行的解释器生成的。

Bool

python的Bool值只有俩,True 和 False,两者都是首字母大写
条件判断的结果也是 Bool值

Bool值的与或非

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# and
>>> True and True
True
>>> True and False
False
>>> False and False
False
>>> 5 > 3 and 3 > 1
True

# or
>>> True or True
True
>>> True or False
True
>>> False or False
False
>>> 5 > 3 or 1 > 3
True

# not
>>> not True
False
>>> not False
True
>>> not 1 > 2
True

空值

空值是Python里的一个特殊的值,用 None 表示。None只是一种特殊的空值,其余的空值还包括 列表、字典等数据类型的空

变量

  • 变量名必须是 大小写英文、数字和下划线_的组合,且不用能数字开头
  • 在Python语法中 等号 = 是赋值语句,可以把任意数据类型赋值给变量,同一变量可以反复赋值。
  • 变量本身类型不固定的语言称之为 动态语言,与之相对应的叫 静态语言

常量

不能改变值的变量,但是 Python没有机制保证常量不被改变。

基本运算:除法&阶乘

Python有三种除法,阶乘的写法也很特别(指与 Matlab 不用,很容易写错

1
2
3
4
5
6
7
8
9
10
11
12
# 浮点除法
>>> 10/3
3.33333333
# 地板除法
>>> 10//3
3
# 余除法
>>> 10%3
1
# 阶乘 **
>>> 10**2
100

此外 Python 的不等于写法与C相同

数据结构:List and Tuple

List

List是Python内置的一种数据类型。List是一种有序的集合,可以随时添加和删除其中的元素

创建列表的方法[]

1
2
3
>>> classmates = ['Michael', 'Bob','Tracy']
>> classmates
['Michael','Bob','Tracy']
  • 使用len()函数,就可以获得list元素的个数

index

List 的索引是用[],跟其他编程语言一样(除了matlab),顺序都是从0开始count,所以最后一个元素的index是 len-1

List可以倒序索引[-1],[-2],…倒数第一第二…这样,但是要注意越界的报错。

List方法

  1. append()

#在List的末尾插入一个元素,List 是一个有序的列表

1
2
3
>>> classmates.append('Adam')
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']
  1. insert()

#在指定位置插入元素,比如索引号为 1 的位置:

1
2
3
>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
  1. pop()

#取出(有调出值的过程,然后删除)list末尾元素,用pop()方法,删除指定索引位置的元素,pop(i)

1
2
3
4
5
6
7
8
9
10
# 就跟 stack 的用法一样
>>> classmates.pop()
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']
# 还可以用pop(i)
>>> classmates.pop(1)
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']

可以直接利用索引和赋值来改变列表元素的值

  1. sort()

#list 可以排序,如果都是字符的话,是字典序从小到大

tuple

tuple 相较 list 唯一的区别在于,无法改变数据元素
tuple 相较 list 更加安全
创建方式()

tuple引用

一样用[]来取引用

tuple方法

因为元素无法更改,没有list所拥有的方法,但是如果元素是list的话,list元素中的元素可以更改

数据结构:dict and set

dict

在一些其他的语言中,叫作map,就是键值对
字典的内部存放顺序与key的放入方式无关
创建方法

1
2
3
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95

dict 键的查找

  1. ’key’ in d: 返回一个条件判断结果
  2. 方法get()
    1
    2
    3
    4
    >>> d.get('Thomas')
    >>> d.get('Thomas', -1)
    -1
    # 如果查找的键,在字典中,会返回 键所对应的值,如果,不存在,要么不输出,要么输出预设的值
    dict的pop()方法
    1
    2
    3
    >>> d['Adam'] = 67
    >>> d.pop('Adam')
    # 再次查找的时候,字典中就不再会有Adam这个键-值对了

与list相比较,dict有几个特点:

  1. 查找和插入的速度极速,不会随着key的增加而变慢;
  2. 需要占用大量的内存,内存浪费多。
    而list相反。
    dict 是用空间来换取时间的一种方法。dict的key必须是不可变对象

set

set与dict类似,也是一组key的集合,但不存储value。
创建set

1
2
3
>>> s = set([1, 2, 3])
>>> s
{1,2,3}

集合是,元素是无序的且不重复,重复输入,会被语言自动过滤

集合方法

  1. add(key)

    1
    2
    >>> s.add(4)
    # 集合s加入一个元素 4
  2. remove(key)

    1
    2
    >>> s.remove(4)
    # 4被拿走
  3. 集合的交、并

    1
    2
    3
    >>> s1 & s2

    >>> s1 | s2

control flow

if&elif

1
2
3
4
5
6
7
8
9
# 注意冒号部分 以及 缩进
if <条件判断1>:
<执行1>
elif <条件判断2>:
<执行2>
elif <条件判断3>:
<执行3>
else:
<执行4>

循环

Python的循环有两种,一种是for … in 一种是 while
break与continue跟c中的一样

for … in

1
2
for x in list:
'''将list中的元素,按顺序依次带入x进行循环中的运算'''

有时候,我们只需要一个自然数序列有函数 range(n) 生成一个长度为 n 的自然数递增序列(0-> n-1)

while

1
2
while <条件1>:
执行<语句1>

函数

调用函数

调用函数,只需要函数的名称参数
如果传入的参数数量不对,会报错 TypeError

数据类型转换

数据类型转换是 Python 内置的常用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

函数的引用

拿函数值做赋值,会生成对函数对引用,就跟 Cpp 中的语法一样,直接“起一个别名”

1
2
3
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

定义函数

Python的函数定义

1
2
3
4
5
6
def my_abs(x):
if x >= 0:
return x
else:
return -x
# quite easy

如果函数体内,没有 return 语句,函数执行完毕,也会返回结果,只是结果为 None

import

如果上看的 my_abs()函数定义保存为了 abstest.py 文件,可以在当前目录下启用Python解释器,用 from abstest import my_abs 来导入 my_abs() 函数

pass

定义一个函数,什么也不做,函数体用 pass

1
2
def nop():
pass

参数检查

*这个section是进阶内容
如何编写报错提示

1
2
3
4
5
6
7
8
def my_abs(x):
# 这个函数会检测 x 的数据类型,如果不属于 int 和 float,会TypeError报错,字符串提示
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x

多值返回

  • return后面多值用,分隔,返回值是一个 tuple
  • 取用函数返回值时,多变量同时接收,并且按位置赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import math

def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny

>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

函数的参数

这个section比较重要, 之前看一些源码会因为,一些参数设置不熟悉而犯懵逼。

定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。

位置参数

就是我们平时最常见的参数,函数输入必须有对应位置参数的输入。
如果缺少,位置参数的输入,系统会报错。

默认参数

在函数设定的时候,就已经设定好默认值的参数。
如果,额外输入该参数值,参数值会被替代。

1
2
3
4
5
6
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

设定默认参数的注意事项

  1. 位置参数必须在前
  2. 如何设置参数

传入多个参数时,将变化大的参数设置在前面,将变化小的(甚至大多数可以用默认值替代的参数放在后面)

默认参数一定要指向不变对象!!!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 以下为没有将 默认参数设定为不可改变对象 的一个漏洞例子
def add_end(L=[]):
L.append('END')
return L
# 没有出bug的函数操作
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
# 出bug的函数操作
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

# 正确的例子
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L

可变参数

顾名思义:传入参数个数是可变的,允许传入0个参数值
一般输入一个List或者Tuple,而函数体内会有for…in循环对变量进行一个遍历

1
2
3
4
5
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

设置方式是,在变量名前加上*,可变参数会自动组装成一个 Tuple

  • 可以先将参数值组装成List或Tuple
  • 但是在输入参数的时候,要在那个可迭代参数前加上*
    1
    2
    3
    >>> nums = [1, 2, 3]
    >>> calc(*nums)
    14

关键字参数

关键字参数,会在函数内部自动组装称一个dict

1
2
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

一般会有一个大杂烩输出(大字典,键值对)

同理,可以先组装称字典再将dict变量传入
传指定参数前,要加上两个**

1
2
3
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

命名关键字参数

关键字参数可以接收任意不受限制的关键字参数。这导致,传入了哪儿些,查找很麻烦。

命名关键字参数,会限制关键字参数的名字“key”

1
2
3
# 限制方式为,在*号之后的都为命名关键字参数
def person(name, age, *, city, job):
print(name, age, city, job)
  1. 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错
  2. 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
    1
    2
    def person(name, age, *args, city, job):
    print(name, age, args, city, job)

参数组合

参数定义的顺序必须是:位置(必选)参数、默认参数、可变参数、命名关键字参数和关键字参数。
命名关键字在关键字前面 这两者跟文档编写顺序相反

递归函数

因为这是所有语言都有的特性,所以不特别说明。
递归就是:函数调用函数。

高级特性

后补: 我又看了一下,挺重要的。。。挺重要的(打脸)

这个part的语法特性在一些数据科学的工业级教科书上出现过。
还是可以学一下的,但是感觉,不是非常重要,就AI科研学习而言(之前不懂事写的

但是在Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。
基于这一思想,我们来介绍Python中非常有用的高级特性,1行代码能实现的功能,决不写5行代码。请始终牢记,代码越少,开发效率越高。

切片slice

经常取指定索引范围的操作,用循环就非常繁琐,利用切片就能大大简化

1
2
3
4
5
6
7
8
9
10
11
12
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
### 引用是,左边闭区间,右边是开区间
### 负数索引也是一样,右边仍然是开区间
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
### 加上步长的slice 跟matlab的语法一样,步长放在第三个地方
>>> L[:10:2]
[0, 2, 4, 6, 8]

一些小trick

1
2
3
4
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

迭代

若给定一个list或者tuple,我们可以用 for…in
Python 的 for 循环抽象程度要高于 C 的 for 循环,因为 Python 的 for 循环不仅可以用在 list 或 tuple 上,还可以作用在其他可迭代对象上

1
2
3
4
5
6
7
8
# 字典也可以进行迭代
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b

因为 dict 的存储不是按照 list 的方式顺序排列,所以,迭代出的结果顺序很可能不一样。

默认情况下,dict 迭代的是key。如果要迭代value,可以用 for value in d.values(),如果要同时迭代 key 和 value,可以用 for k,v in d.items()。

由于字符串也是可迭代对象,因此,也可以作用于for循环

1
2
3
4
5
6
>>> for ch in 'ABC':
... print(ch)
...
A
B
C

判断一个对象是否可迭代

方法是通过 collections.abc 模块的 Iterable 类型判断:

1
2
3
4
5
6
7
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

下标循环

这个part太重要了,因为不熟悉,之前看到代码都感觉见过,又想不起来具体是干什么的

1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C

for循环其实可以同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:

具体做法就是,List中的元素依次与index绑定生成二元素tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 二元 for...in
例子1
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9
# 例子2
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
... print(k, '=', v)
...
y = B
x = A
z = C

列表生成式

List Comprehensions 是 Python 内置的简单且强大的可以用来创建 list 的生成式。

  • 生成 1->10 的整数序列,list(range(1,11))

  • 生成 1->10 各个数值的平方

    1
    >>> [x * x for x in range(1, 11)]
  • for 循环后还能加上 if 条件判断

    1
    2
    >>> [x * x for x in range(1, 11) if x % 2 == 0]
    [4, 16, 36, 64, 100]

列表生成式进阶应用

  1. 进阶版,两层循环生成全排列

    1
    2
    >>> [m + n for m in 'ABC' for n in 'XYZ']
    ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
  2. 生成当前目录下的所有文件和目录名

    1
    2
    3
    >>> import os # 导入os模块,模块的概念后面讲到
    >>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
    ['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
  3. 使用两变量生成list

    1
    2
    3
    >>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
    >>> [k + '=' + v for k, v in d.items()]
    ['y=B', 'x=A', 'z=C']
  4. 把一个list中所有的字符串变成小写

    1
    2
    3
    >>> L = ['Hello', 'World', 'IBM', 'Apple']
    >>> [s.lower() for s in L]
    ['hello', 'world', 'ibm', 'apple']

列表生成式的if…else

1
2
3
4
5
6
7
8
9
10
11
12
13
# if 的列表生成式
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
# 错误的if-else
>>> [x for x in range(1, 11) if x % 2 == 0 else 0]
File "<stdin>", line 1
[x for x in range(1, 11) if x % 2 == 0 else 0]
^
SyntaxError: invalid syntax

# 正确的if-else
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]

生成器generator

generator 能够“记住” 生成序列的算法,在需要调用的时候,根据算法依次推导各个值,是一种 用时间换空间 的设计

如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。

创建生成器的两种方法

  1. 把一个列表生成式的[]改成(),就创建了一个generator
    1
    2
    3
    4
    5
    6
    >>> L = [x * x for x in range(10)]
    >>> L
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> g = (x * x for x in range(10))
    >>> g
    <generator object <genexpr> at 0x1022ef630>
  • L 与 g 的区别
    • 最外层的 [] 和 ()
    • L 是一个 list,而 g 是一个generator

可以通过 next() 函数获得 generator 的下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

generator 保存的是算法,每次调用 next(g),就计算出 g 的下一个元素的值,直到最后一个元素,此时再次调用会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81

如果推算算法比较复杂,用类似列表生成式的 for 循环无法实现: 还可以用
2. 函数实现
Fibonacci 就是无法用列表生成式表示的。

1
2
3
4
5
6
7
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

相比函数的打印,函数实现generator,只需要将 print(b) -> yield b

这里,最难理解的就是generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

请务必注意调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。

generator也是可迭代的

1
2
3
4
5
6
7
8
9
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8

迭代器

可迭代数据类型: list, tuple, dict, set, str
一类是 generator, 包括 生成器带 yield 的 generator function

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。可以使用isinstance()判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

迭代器(Iterator):可以被 next()函数不断调用并返回下一个值

可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
6
7
8
9
>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

你可能会问,为什么list、dict、str等数据类型不是Iterator?
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

模块

强烈建议还是看一下 廖老师的网站

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块Module)。

使用模块的好处

  1. 提高代码的可维护性。
  2. 避免函数名和变量名的冲突,但要注意不要与Python内置模块命名冲突

为避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(package)

image

廖老师这里写的太好了,强烈建议去看看这篇。

使用模块

以内建的 sys 模块为例,编写一个 hello 模块(廖老师写的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

# 导入变量sys后,就可以利用变量sys指向该模块,并且访问sys模块的所有功能。
import sys

def test():
# sys模块有个argv变量,用list存储命令行中的所有参数(至少有一个参数,因为第一个参数永远是.py文件名
'''运行python3 hello.py获得的sys.argv就是['hello.py'];'''
'''运行python3 hello.py Michael获得的sys.argv就是['hello.py', 'Michael']。'''

args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')

if __name__=='__main__':
test()

第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;

name & main trick

1
2
if __name__=='__main__':
test()

这个技巧非常重要

当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

作用域

我们可能会定义许多 变量函数,在一个模块中。有的变量,我们只希望在模块内部被调用,这时我们对变量名前后加上 _ 来表示为 private
一般的变量,我们认为是 public的

如模块中的__name__,__auther__就是特殊变量(private),理论上这些不该被调用,但是Python没有防止其被调用的硬性手段。

一个private函数的小例子:

1
2
3
4
5
6
7
8
9
10
11
def _private_1(name):
return 'Hello, %s' % name

def _private_2(name):
return 'Hi, %s' % name

def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)

我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。

安装第三方模块

pip

  • 在Python中,安装第三方模块,是通过包管理工具pip完成的。
  • 如果你正在使用Mac或Linux,安装pip本身这个步骤就可以跳过。Windows用户需要在安装Python的时候就勾选pip的安装。

例如,我们要安装一个第三方库——Pillow,在cmd或者bash或者各种shell这些终端

1
pip install Pillow

conda

在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。

可以从Anaconda官网下载GUI安装包,安装包有500~600M,所以需要耐心等待下载。下载后直接安装,Anaconda会把系统Path中的python指向自己自带的Python,并且,Anaconda安装的第三方模块会安装在Anaconda自己的路径下,不影响系统已安装的Python目录。

安装好Anaconda后,重新打开命令行窗口,输入python,可以看到Anaconda的信息:

image

可以尝试直接 import numpy 等已安装的第三方模块。

模块搜索路径

当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错

默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:

1
2
3
>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']

添加路径

  1. 直接修改sys.path,添加要搜索的目录:

    1
    2
    >>> import sys
    >>> sys.path.append('/Users/michael/my_py_scripts')

    这种方法是在运行时修改,运行结束后失效。

  2. 第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。

面向对象

这个章节非常重要,因为,用 PyTorch 框架设置模型的时候,我们需要运用一些面向对象的知识。

面向对象编程 – Object Oriented Programming 是一种 程序设计思想。
把 对象 作为程序的基本单元,对象 包含了 数据操作数据 的函数。

  • 面向过程: 计算机程序视为一系列的命令合集,即一组函数的顺序执行。函数->切分的子函数,通过将大块函数切割成小块函数,来降低系统的复杂度
  • 面向对象: 把计算机程序视为一组对象的集合。

自定义对象: 类(Class)

  • 采用面向对象的程序设计,首先考虑的是将 数据类型 视为一个对象。
    • 对象拥有若干个 属性
    • 还要内置 方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class Student(object):

      def __init__(self, name, score):
      self.name = name
      self.score = score

      def print_score(self):
      print('%s: %s' % (self.name, self.score))
      # 与 对象交互 实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:

      bart = Student('Bart Simpson', 59)
      lisa = Student('Lisa Simpson', 87)
      bart.print_score()
      lisa.print_score()
  1. 类(Class)是一个抽象的概念
  2. 实例(Instance)是一个具体的概念
  3. 方法(Method)是与类相关联的函数

类与实例

实例(Instance)是根据类创建出来的一个个具体的“对象”

1
2
class Student(object):
pass

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

创建实例

1
2
3
4
5
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

实例的初始化

通过定义一个特殊的__init__方法,在创建实例的时候,就把 name, score 等属性绑上去:

1
2
3
4
5
class Student(object):

def __init__(self, name, score):
self.name = name
self.score = score

这个函数定义后,在创建实例的时候就得,Student后的括号中传入 namescore的参数了

  • 方法的第一个参数永远是 self,表示实例本身。在方法内部,可以通过 self 指向创建的实例本身。
  • 有了初始化方法后,就不能换入空的参数了。

限制访问

函数内部有些属性,不想被访问。就在命名的时候,命名前加上双下划线 __

优点: 隐藏了内部的复杂逻辑

继承与多态

当定义一个 Class 的时候,可以从某个现有的 Class 继承,新的 Class 称为 子类(Subclass),而被继承的 Class 称为基类、父类或超类(Base class, Super class)

继承

继承: 子类可以使用父类定义的方法,获得父类所有的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定义 Animal 作为父类
class Animal(object):
def run(self):
print('Animal is running...')

# 定义两个子类
class Dog(Animal):
pass

class Cat(Animal):
pass

>>> dog = Dog()
>>> dog.run()

>>> cat = Cat()
>>> cat.run()

Output:
Animal is running...
Animal is running...
  • 如果在子类中,定义了与父类同名的方法,会覆盖父类方法。

判断类的类型的方法:

1
2
3
4
5
6
7
8
9
10
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

继承关系中,实例即被视为子类,又被视为父类,但是,作为最小的类,范围最短。

多态

方法的传参中,传入类的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 输入参数为数据的类
def run_twice(animal):
animal.run()
animal.run()

# 输入类后,会调用那个类中定义的方法
>>> run_twice(Animal())
Animal is running...
Animal is running...

>>> run_twice(Dog())
Dog is running...
Dog is running...

>>> run_twice(Cat())
Cat is running...
Cat is running...

任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处: 需要传入Dog、Cat、Tortoise,只需要接收Animal类型,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:

对于一个变量,我们只需要知道它的类型,无需确切地知道它的子类型,就可以放心地调用父类方法,而具体调用的方法是作用在本身还是子类对象上,由运行时该对象的确切类型决定。
多态真正的威力:调用方只管调用,不管细节,而当新增一种子类时,只要确保方法编写正确,不用管原来的代码是如何调用的。

  • 著名的“开闭”原则:
    • 对扩展开放:允许新增子类
    • 对修改封闭:不需要修改依赖类型的方法函数。
    • 继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。

image

获取对象信息

type()

可以判断 基本数据类型类的类别,还可以判断 函数的类型

1
2
3
4
5
6
7
8
9
10
11
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>

>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>

*不要求掌握的方法:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

instance()

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
object -> Animal -> Dog -> Husky

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()

>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog)
True
>>> isinstance(h, Animal)
True
>>> isinstance(d, Dog) and isinstance(d, Animal)
True

总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

dir: 获取类所有属性与方法

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

1
2
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

仅仅是列出 属性 与 方法 是不够的,配合 getattr(), setattr() 以及 hasattr(),我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19

如果试图获取不存在的属性,会抛出AttributeError的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
# getattr()的勘误机制
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404
# 方法对象一同处理
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

实例属性、类属性

给实例绑定属性的方法是通过实例变量,或者通过 self 变量:

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name

s = Student('Bob')
s.score = 90
  • 定义在类中的属性,类属性。定义后,所有实例都可以访问到。
  • 实例属性,属于实例的属性。
  • 实例与类属性重名,会覆盖类属性。

第三方模块

  1. 基本所有第三方模块都会在 https://pypi.org/ 注册,找到模块名 pip 安装。什么?你问我 pip 怎么用? 自己不会上网学啊?
  2. conda install

虚拟环境

虚拟环境就是,重新开个新号,可以在一个完全干净的python环境里,配置自己需要的开发环境

  1. virtualenv 配置
  2. conda 配置

这个一定要去学!!! 看网上的一些 虚拟环境 配置的教程,或者别的,一般conda配置环境比较方便