Python学习
一、基础篇
01 学习方法
- 大厦之基,勤加练习
- 代码规范,必不可少
02 jupyter notebook
到底什么是 Jupyter Notebook?按照 Jupyter 创始人 Fernando Pérez 的说法,他最初的梦想是做一个综合 Ju (Julia)、Py (Python)和 R 三种科学运算语言的计算工具平台,所以将其命名为 Ju-Py-te-R。发展到现在,Jupyter 已经成为一个几乎支持所有语言,能够把软件代码、计算输出、解释文档、多媒体资源整合在一起的多功能科学运算平台。
- Jupyter 的优点
- 整合所有的资源
- 交互性编程体验
- 零成本重现结果
03 列表和元组
- 列表(list)和元组(tuple),都是一个可以放置任意数据类型的有序集合。
l = [1, 2, 'hello', 'world'] # 列表中同时含有 int 和 string 类型的元素
l
[1, 2, 'hello', 'world']
tup = ('jason', 22) # 元组中同时含有 int 和 string 类型的元素
tup
('jason', 22)
-
列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。
-
而元组是静态的,长度大小固定,无法增加删减或者改变(immutable)。
l = [1, 2, 3, 4]
l[3] = 40 # 和很多语言类似,python 中索引同样从 0 开始,l[3] 表示访问列表的第四个元素
l
[1, 2, 3, 40]
tup = (1, 2, 3, 4)
tup[3] = 40
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
如果你想对已有的元组做任何"改变",该怎么办呢?那就只能重新开辟一块内存,创建新的元组了。
比如下面的例子,我们想增加一个元素 5 给元组,实际上就是创建了一个新的元组,然后把原来两个元组的值依次填充进去。
而对于列表来说,由于其是动态的,我们只需简单地在列表末尾,加入对应元素就可以了。如下操作后,会修改原来列表中的元素,而不会创建新的列表。
tup = (1, 2, 3, 4)
new_tup = tup + (5, ) # 创建新的元组 new_tup,并依次填充原元组的值
new _tup
(1, 2, 3, 4, 5)
l = [1, 2, 3, 4]
l.append(5) # 添加元素 5 到原列表的末尾
l
[1, 2, 3, 4, 5]
- 和其他语言不同,Python 中的列表和元组都支持负数索引,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。
l = [1, 2, 3, 4]
l[-1]
4
tup = (1, 2, 3, 4)
tup[-1]
4
- 除了基本的初始化,索引外,列表和元组都支持切片操作,区间左闭右开:
l = [1, 2, 3, 4]
l[1:3] # 返回列表中索引从 1 到 2 的子列表
[2, 3]
tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从 1 到 2 的子元组
(2, 3)
- 列表和元组都可以随意嵌套:
l = [[1, 2, 3], [4, 5]] # 列表的每一个元素也是一个列表
tup = ((1, 2, 3), (4, 5, 6)) # 元组的每一个元素也是一元组
- 两者也可以通过 list() 和 tuple() 函数相互转换:
list((1, 2, 3))
[1, 2, 3]
tuple([1, 2, 3])
(1, 2, 3)
- 列表和元组的内置函数
l = [3, 2, 3, 7, 8, 1]
l.count(3)
2
l.index(7)
3
l.reverse()
l
[1, 8, 7, 3, 2, 3]
l.sort()
l
[1, 2, 3, 3, 7, 8]
tup = (3, 2, 3, 7, 8, 1)
tup.count(3)
2
tup.index(7)
3
list(reversed(tup))
[1, 8, 7, 3, 2, 3]
sorted(tup)
[1, 2, 3, 3, 7, 8]
-
count(item)
表示统计列表 / 元组中 item 出现的次数。 -
index(item)
表示返回列表 / 元组中 item 第一次出现的索引。 -
list.reverse()
和 list.sort() 分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)。 -
reversed()
和sorted()
同样表示对列表 / 元组进行倒转和排序,但是会返回一个倒转后或者排好序的新的列表 / 元组。 -
列表和元组存储方式的差异 前面说了,列表和元组最重要的区别就是,列表是动态的、可变的,而元组是静态的、不可变的。这样的差异,势必会影响两者存储方式:
l = [1, 2, 3]
l.__sizeof__()
64
tup = (1, 2, 3)
tup.__sizeof__()
48
对列表和元组,我们放置了相同的元素,但是元组的存储空间,却比列表要少 16 字节。这是为什么呢?
事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(上述例子中,对于 int 型,8 字节)。另外,由于列表可变,所以需要额外存储已经分配的长度大小(8 字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。
l = []
l.__sizeof__() // 空列表的存储空间为 40 字节
40
l.append(1)
l.__sizeof__()
72 // 加入了元素 1 之后,列表为其分配了可以存储 4 个元素的空间 (72 - 40)/8 = 4
l.append(2)
l.__sizeof__()
72 // 由于之前分配了空间,所以加入元素 2,列表空间不变
l.append(3)
l.__sizeof__()
72 // 同上
l.append(4)
l.__sizeof__()
72 // 同上
l.append(5)
l.__sizeof__()
104 // 加入元素 5 之后,列表的空间不足,所以又额外分配了可以存储 4 个元素的空间
元组要比列表更加轻量级一些,所以总体上来说,元组的性能速度要略优于列表。
元组可以对静态数据做一些资源缓存(resource caching)。
元组初始化速度更快,索引速度与列表相差不大,增加、删减或者改变元素,那么列表显然更优。
# 创建空列表
# option A
empty_list = list()
# option B
empty_list = []
- []比list()更快,因为调用list函数有一定的开销,Python的function call会创建stack,并且进行一系列参数检查的操作,比较expensive,而[]却没有.
- []是一个内置的C函数,可以直接被调用
03 字典和集合
字典是一系列由键(key)和值(value)配对组成的元素的集合,在 Python3.7+,字典被确定为有序(注意:在 3.6 中,字典有序是一个 implementation detail,在 3.7 才正式成为语言特性,因此 3.6 中无法 100% 确保其有序性),而 3.6 之前是无序的,其长度大小可变,元素可以任意地删减和改变。
字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。
而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。
d1 = {'name': 'jason', 'age': 20, 'gender': 'male'}
d2 = dict({'name': 'jason', 'age': 20, 'gender': 'male'})
d3 = dict([('name', 'jason'), ('age', 20), ('gender', 'male')])
d4 = dict(name='jason', age=20, gender='male')
d1 == d2 == d3 ==d4
True
s1 = {1, 2, 3}
s2 = set([1, 2, 3])
s1 == s2
True
Python 中字典和集合,无论是键还是值,都可以是混合类型。比如下面这个例子,我创建了一个元素为1,'hello',5.0的集合:s = {1, 'hello', 5.0}
字典访问可以直接索引键,如果不存在,就会抛出异常:
d = {'name': 'jason', 'age': 20}
d['name']
'jason'
d['location']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'location'
也可以使用 get(key, default) 函数来进行索引。如果键不存在,调用 get() 函数可以返回一个默认值。比如下面这个示例,返回了'null'。
d = {'name': 'jason', 'age': 20}
d.get('name')
'jason'
d.get('location', 'null')
'null'
集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一样。所以,下面这样的操作是错误的,Python 会抛出异常:
s = {1, 2, 3}
1 in s
True
10 in s
False
d = {'name': 'jason', 'age': 20}
'name' in d
True
'location' in d
False
字典和集合也同样支持增加、删除、更新等操作。
d = {'name': 'jason', 'age': 20}
d['gender'] = 'male' # 增加元素对'gender': 'male'
d['dob'] = '1999-02-01' # 增加元素对'dob': '1999-02-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male', 'dob': '1999-02-01'}
d['dob'] = '1998-01-01' # 更新键'dob'对应的值
d.pop('dob') # 删除键为'dob'的元素对
'1998-01-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male'}
s = {1, 2, 3}
s.add(4) # 增加元素 4 到集合
s
{1, 2, 3, 4}
s.remove(4) # 从集合中删除元素 4
s
{1, 2, 3}
集合的 pop() 操作是删除集合中最后一个元素,可是集合本身是无序的,你无法知道会删除哪个元素,因此这个操作得谨慎使用。
我们需要对字典或集合进行排序,对于字典,我们通常会根据键或值,进行升序或降序排序:
d = {'b': 1, 'a': 2, 'c': 10}
d_sorted_by_key = sorted(d.items(), key=lambda x: x[0]) # 根据字典键的升序排序
d_sorted_by_value = sorted(d.items(), key=lambda x: x[1]) # 根据字典值的升序排序
d_sorted_by_key
[('a', 2), ('b', 1), ('c', 10)]
d_sorted_by_value
[('b', 1), ('a', 2), ('c', 10)]
这里返回了一个列表。列表中的每个元素,是由原字典的键和值组成的元组。
而对于集合,其排序和前面讲过的列表、元组很类似,直接调用 sorted(set) 即可,结果会返回一个排好序的列表。
s = {3, 4, 2, 1}
sorted(s) # 对集合的元素进行升序排序
[1, 2, 3, 4]
集合这个数据结构,由于集合是高度优化的哈希表,里面元素不能重复,并且其添加和查找操作只需 O(1) 的复杂度
- 字典和集合的内部结构都是一张哈希表。
- 对于字典而言,这张表存储了哈希值(hash)、键和值这 3 个元素。
- 而对集合来说,区别就是哈希表内没有键和值的配对,只有单一的元素了
为了提高存储空间的利用率,现在的哈希表除了字典本身的结构,会把索引和哈希值、键、值单独分开,也就是下面这样新的结构:
每次向字典或集合插入一个元素时,Python 会首先计算键的哈希值(hash(key)),再和 mask = PyDicMinSize - 1 做与操作,计算这个元素应该插入哈希表的位置 index = hash(key) & mask。如果哈希表中此位置是空的,那么这个元素就会被插入其中。
-
而如果此位置已被占用,Python 便会比较两个元素的哈希值和键是否相等。
-
若两者都相等,则表明这个元素已经存在,如果值不同,则更新值。
若两者中有一个不相等,这种情况我们通常称为哈希冲突(hash collision),意思是两个元素的键不相等,但是哈希值相等。这种情况下,Python 便会继续寻找表中空余的位置,直到找到位置为止。
-
查找操作 和前面的插入操作类似,Python 会根据哈希值,找到其应该处于的位置;然后,比较哈希表这个位置中元素的哈希值和键,与需要查找的元素是否相等。如果相等,则直接返回;如果不等,则继续查找,直到找到空位或者抛出异常为止。
-
删除操作 对于删除操作,Python 会暂时对这个位置的元素,赋于一个特殊的值,等到重新调整哈希表的大小时,再将其删除。
不难理解,哈希冲突的发生,往往会降低字典和集合操作的速度。因此,为了保证其高效性,字典和集合内的哈希表,通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插入,当剩余空间小于 1/3 时,Python 会重新获取更大的内存空间,扩充哈希表。不过,这种情况下,表内所有的元素位置都会被重新排放。
虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所以,平均情况下,这仍能保证插入、查找和删除的时间复杂度为 O(1)。
05 字符串
单引号('')双引号("")或者三引号('''''')或("""""")中,方便内嵌带引号的字符串
字符串内部支持转义字符。
字符串同样支持索引、切片和遍历等操作。
name = 'jason'
name[0]
'j'
name[1:3]
'as'
遍历字符串
for char in name:
print(char)
j
a
s
o
n
python中字符串是不可变的也即以下操作是错误的:s = 'hello'; s[0] = 'H'
python中字符串的改变只能通过创建新的字符串完成。
s = 'H' + s[1:] # 拼接
s = s.replace('h', 'H'); # 函数
字符串可以使用+=
进行拼接
s = ''
for n in range(0, 1000): # 这里范围是[0,1000)
s += str(n)
append
相当于C++中的push_back。str.join(iterable)
可以实现按照指定格式链接字符串:iterable中的元素之间使用str进行分隔。
l = []
for n in range(0, 10):
l.append(str(n))
l = ' '.join(l) # '0 1 2 3 4 5 6 7 8 9'
字符串分割splice函数
path = 'hive://ads/training_table'
namespace = path.split('//')[1].split('/')[0] # 'ads'
table = path.split('//')[1].split('/')[1] # 'training_table'
其他字符串函数
- string.strip(str) : 表示去掉首尾的str字符串
- string.lstrip(str) : 表示只去掉开头的str字符串
- string.rstrip(str) : 表示字去掉末尾的str字符串 去除首尾空格:
s = ' my name is jason '
s.strip() # 这里会生成一个新的字符串'my name is jason',源字符串s不变
格式化输出str.format()
print('id:{}, name:{}'.format(id, name))
print('id:%s, name:%s'%(id, name))
06 输入与输出
input()用于从键盘读取字符串,阻塞。
name = input('your name:')
gender = input('you are a boy ?(y/n)')
type(var)
可以把var
变量的值得类型显示出来。
强制转换int(),float()。python没有int型得最大限制。
- 文本处理
open()函数拿到文件的指针,第一个参数为文件路径,第二个参数是权限:
r
、w
或rw
、a
(追加append)。
test = fin.read()打开的指针fin对应的文件使用read函数赋值给test
- JSON序列化 json.dumps()接受基本数据类型,将其序列化为string类型。
json.loads()接受一个合法的字符串,将其反序列化为基本数据类型
import json
params = {
'symbol':'123456',
'type':'limit',
'price':123.4,
'amount':23
}
params_str = json.dumps(params)
#'{"symbol": "123456", "type": "limit", "price": 123.4, "amount": 23}'
original_params = json.loads(params_str)
# {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23}
print('params_str type:{},original_params:{}'.format(type(params_str), type(original_params)))
# params_str type:<class 'str'>,original_params:<class 'dict'>
文本处理都需要加上异常处理。
07 条件与循环
if id == 0:
print('red')
elif id == 1:
print('yellow')
else:
print('green')
注意:没有括号,有冒号,elif.
省略判断写法
if s: # s是字符串
...
if l: # l是list
...
if i: # i是int
- 循环语句
l = [1, 2, 3, 4]
for item in l:
print(item)
- 遍历字典
d = {'name':'jason', 'dob':'2000-01-01','gender':'male'}
# 遍历key
for key in d:
print(key)
# 遍历val
for val in d.values():
print(val)
for key, val in d.items():
print('key:{}, val:{}'.format(key, val))
- 遍历list
l = [1,2,3,4,5,6,7]
for idx in range(0, len(l)):
if (idx < 5):
print(l[idx])
for idx, item in enumerate(l):
if idx < 5:
print(item)
continue和break分别是跳过该层循环和结束循环
- while循环
l = [1,2,3,4,5,6,7]
idx = 0
while idx < len(l):
print(l[idx])
idx += 1
条件与循环合并
exp1 if condition else exp2 for item in iter
# 相当于:
for item in iter:
if condition:
exp1
else:
exp2
绘制y = 2 * |x| + 5
的函数图像,已知x的集合
y = [val * 2 + 5 if val > 0 else -val * 2 + 5 for val in x]
字符串按照逗号分割单词,去掉首位空字符,并过滤掉长度小于3的单词
text = ' Today, is, Sunday'
text_list = [s.strip() for s in text.split(',') if len(s.strip()) > 3]
print(text_list)
# ['Today', 'Sunday']
给定两个列表x和y要求返回x和y自称所有元素对组成元组,想等除外。
[(xx, yy) for xx in x for yy in y if xx != yy]
l = []
for xx in x:
for yy in y:
if xx != yy:
l.append((xx, yy))
08 异常处理
常见的三种异常类型:ZeroDivisionError
、Nameerror
和TypeError
.
try-except
try:
s = input('please enter two numbers separated by comma: ')
num1 = int(s.split(',')[0].strip()) # 以逗号相隔的两个整形数字,将其提取
num2 = int(s.split(',')[1].strip())
...
except ValueError as err:
print('Value Error: {}'.format(err))
print('continue')
...
except block 只接受与它相匹配的异常类型并执行,如果程序抛出的异常并不匹配,那么程序照样会终止并退出。continue 不会被打印。
其中一种解决方案,是在 except block 中加入多种异常的类型。
try:
s = input('please enter two numbers separated by comma: ')
num1 = int(s.split(',')[0].strip())
num2 = int(s.split(',')[1].strip())
...
except (ValueError, IndexError) as err:
print('Error: {}'.format(err))
print('continue')
...
Exception 是其他所有非系统异常的基类,能够匹配任意非系统异常。那么这段代码就可以写成下面这样:
try:
s = input('please enter two numbers separated by comma: ')
num1 = int(s.split(',')[0].strip())
num2 = int(s.split(',')[1].strip())
...
except ValueError as err:
print('Value Error: {}'.format(err))
except IndexError as err:
print('Index Error: {}'.format(err))
except Exception as err:
print('Other error: {}'.format(err))
print('continue')
...
import sys
try:
f = open('file.txt', 'r')
.... # some data processing
except OSError as err:
print('OS error: {}'.format(err))
except:
print('Unexpected error:', sys.exc_info()[0])
finally:
f.close()
09 函数
def my_func(message):
print('Got a message: {}'.format(message))
# 调用函数 my_func()
my_func('Hello World')
# 输出
Got a message: Hello World
- def 是函数声名
- my_func是函数得名称
- message函数参数
- print是函数主体 必须先声明再使用
设定默认参数
def func(param = 0):
...
多态:同样的一个函数(比如这边的相加函数 my_sum()),可以同时应用在整型、列表、字符串等等的操作中。
Python 支持函数的嵌套。所谓的函数嵌套,就是指函数里面又有函数,比如:
def f1():
print('hello')
def f2():
print('world')
f2()
f1()
# 输出
hello
world
- 函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。
- 合理的使用函数嵌套,能够提高程序的运行效率。
def factorial(input):
# validation check
if not isinstance(input, int):
raise Exception('input must be an integer.')
if input < 0:
raise Exception('input must be greater or equal to 0' )
...
def inner_factorial(input):
if input <= 1:
return 1
return input * inner_factorial(input-1)
return inner_factorial(input)
print(factorial(5))
不能在函数内随便改变全局变量的值,会报错。Python 的解释器会默认函数内部的变量为局部变量,但是又发现局部变量 MIN_VALUE 并没有声明,因此就无法执行相关操作。
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
global MIN_VALUE
...
MIN_VALUE += 1
...
validation_check(5)
global 关键字,并不表示重新创建了一个全局变量 MIN_VALUE,而是告诉 Python 解释器,函数内部的变量 MIN_VALUE,就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。
对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上 nonlocal 这个关键字:
def outer():
x = "local"
def inner():
nonlocal x # nonlocal 关键字表示这里的 x 就是外部函数 outer 定义的变量 x
x = 'nonlocal'
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: nonlocal
如果不加上 nonlocal 这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。
def outer():
x = "local"
def inner():
x = 'nonlocal' # 这里的 x 是 inner 这个函数的局部变量
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: local
- 闭包(closure) 闭包其实和刚刚讲的嵌套函数类似,不同的是,这里外部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋于一个变量,这个变量可以在后面被继续执行调用。
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
square
# 输出
<function __main__.nth_power.<locals>.exponent(base)>
cube
# 输出
<function __main__.nth_power.<locals>.exponent(base)>
print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
# 输出
4 # 2^2
8 # 2^3
这里外部函数 nth_power() 返回值,是函数 exponent_of(),而不是一个具体的数值。需要注意的是,在执行完square = nth_power(2)和cube = nth_power(3)后,外部函数 nth_power() 的参数 exponent,仍然会被内部函数 exponent_of() 记住。这样,之后我们调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义了。
上面的闭包也可以使用以下形式:
def nth_power_rewrite(base, exponent):
return base ** exponent
# 不适用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...
# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...