[学习笔记]Python for Data Analysis, 3E-3.内置数据结构,函数和文件

3.1数据结构和序列

元组

元组是一个固定长度的、元素不可变的Python对象序列。

# 创建元组最简单方法是用括号括起来的逗号分隔序列(括号可以省略)
tup = (4, 5, 6)
tup = 4, 5, 6

# 通过调用tuple可以将任何序列和迭代器转化为元组。
tuple([4, 0, 2]) # (4, 0, 2)
tuple('string')  # ('s', 't', 'r', 'i', 'n', 'g')

# 元组可以通过中括号[]来访问其中的元素。
tup[0] # 4

# 元组的元素可以是元组。
nested_tup = (4, 5, 6), (7, 8)

# 虽然存储在元组中的对象可能是可变的,但是一旦元组被创建,就不能将这个对象更改为别的对象。但是如果它可变,可以修改这个对象。
tup = tuple(['foo', [1, 2], True])
tup[2] = False # 无法赋值,因为tuple不可变
tup[1].append(3) # tuple中的对象是list,可变,可以修改

# 元组可以通过+运算符进行拼接
(4, None, 'foo') + (6, 0) + ('bar', ) # (4, None, 'foo', 6, 0, 'bar')

# 元组可以通过*运算符进行复制和拼接
('foo', 'bar') * 4 # ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

解包元组

# 如果尝试将类似元组的表达式赋值给变量,Python会尝试解压缩右侧的值(甚至具有嵌套元组的序列也可以解包)
a, b, c = (4, 5, 6)
a, b, (c, d) = 4, 5, (6, 7)

# 变量解包的一个常见用途是迭代元组或列表序列
seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
for a, b, c in seq:
    print(f'a={a}, b={b}, c={c}')
    
# 在变量解包中,利用*rest获得任意长度元组元素,以达到提取元组开头元素的目的
values = 1, 2, 3, 4, 5
a, b, *rest = values # a=1, b=2, rest=[3, 4, 5]
# rest部分有时候是不需要的,所以常常也用_代替,即上一行代码可改为
a, b, *_ = values

元组方法

# 元组方法中一个特别有用的方法是'count'方法,它也可以用于列表
a = (1, 2, 2, 2, 3, 4, 2)
a.count(2) # 返回4

列表

列表是一个可变长的、元素可变的Python对象序列。

# 使用[]或使用list函数创建列表
a_list = [2, 3, 7, None]
b_list = list(('foo', 'bar', 'baz'))

# 修改列表元素
b_list[1] = 'peekaboo'

# list内置函数在数据处理中经常被用于具像化迭代器或生成器表达式
gen = range(10)

# 连接和合并列表
[4, None, 'foo'] + [7, 8, (2, 3)] # 通过+运算符进行列表串联
x = [4, None, 'foo']
x.extend([7, 8, (2, 3)]) # 通过extend函数追加列表(比串联方案更快)

字典

字典可能是最重要的内置Python数据结构。在其他编程语言中,字典有时候称为哈希映射或关联数组。字典存储键值对的集合,其中键和值是Python对象。

# 创建字典的方法是用大括号和冒号来分隔键和值
empty_dict = {} # 创建空字典
d1 = {'a': 'some value', 'b': [1, 2, 3, 4]}

# 插入、访问元素
d1[7] = 'an integer
d1['b']

# 检查字典中是否包含某个键
'b' in d1

# 通过del关键字或pop方法(会返回值)来删除键值对
del d1['a'] # 删除d1的键'a'和其对应的值
ret = d1.pop('b') # pop方法在删除键值对的同时还会返回值,这里ret的值是[1, 2, 3, 4]

# keys()和values()方法可以分别提供键和值的迭代器,items()方法可以迭代访问键值对组成的二元组
list(d1.keys()) # 获得键组成的列表
list(d1.values())  # 获得值组成的列表
list(d1.items()) # 获得列表,其元素为键值对组成的元组

# 使用update()方法可以将一个字典合并到另一个字典中
d1.update({'b': 'foo', 'c': 12}) # 如果有重复的键,则对应的旧值会被丢弃,更新为新值

# 从序列创建字典
mapping = {}
for key, value in zip(key_list, value_list): # zip()函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象
    mapping[key] = value
# 由于字典本质上是二元组的集合,因此可以通过dict()函数接受一个二元组的列表来生成字典
tuples = zip(range(5), reversed(range(5)))
mapping = dict(tuples)

# 字典的get()方法、pop()方法、setdefault()方法支持默认值
value = some_dict.get(key, default_value) # 如果key不在字典的键中,则返回default_value值
value = some_dict.pop(key, default_value) # 如果key不在字典的键中,则返回default_value值
# 利用setdefault()方法将单词列表按照首字母分类为列表字典
words = ['apple', 'bat', 'bar', 'atom', 'book']
by_letter = {}
for word in words:
    letter = word[0]
    by_letter.setdefault(letter, []).append(word) # setdefault()方法会返回letter键对应的值,如果没有对应的键则创建,同时值设置为[]
# 内置的collections模块有一个有用的类defaultdict。通过传递一个类型或者函数,字典会为后续添加的每个键创建默认值(上面的例子可改为)
from collections import defaultdict
by_letter = defaultdict(list)
for word in words:
    by_letter[word[0]].append(word)

# 字典的键通常要求是不可变对象,如标量类型(int, float, 字符串)或元组(元组中所有对象也要求不可变),这样才能保证哈希可处理。通过hash()函数可以检查对象是否可哈希
hash('string') # 可哈希
hash((1, 2, (2, 3))) # 可哈希
hash((1, 2, [2, 3])) # 不可哈希,因为元组中的对象[2, 3]为列表,它是可变的
# 注意:要将列表作为键,一般选择是将其转化为元组

集合

集合是唯一元素的无序集合。

# 创建集合可以通过set()函数或者带有大括号的集合文本
set([2, 2, 2, 1, 3, 3])
{2, 2, 2, 1, 3, 3}

# 集合支持包括并集(union)、交集、差分和对称差分等集合运算
a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7, 8}
a.union(b) # 并集, {1, 2, 3, 4, 5, 6, 7, 8}
a | b      # 并集, {1, 2, 3, 4, 5, 6, 7, 8}
a.intersection(b) # 交集
a & b      # 交集
# 如果将不是集合的输入传递给union或intersection,Python会在执行操作前将输入转化为集合。但使用二元运算符'|'或'&'时,两个对象必须是集合。

[表]Python集合操作

# 与字典键一样,集合元素通常是不可变的,并且它们必须是可哈希的。为了在集合中存储类似列表的元素(或其他可变序列),可以将它们转换为元组。

内置序列函数

enumerate

# enumerate()函数可以返回collection序列的一系列的(index, value)元组组成的序列
for index, value in enumerate(collection):
    # do something with value

sorted

# sorted()函数可以返回一个排好序的列表
sorted('horse race')

zip

# zip()函数将多个列表、元组或其他序列的元素配对以创建元组组成的zip对象,再通过list()函数可以将其转化为列表
seq1 = ['foo', 'bar', 'baz']
seq2 = ['one', 'two', 'three']
zipped = zip(seq1, seq2)
list(zipped) # [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

# zip可以接受任意数量的序列,并且产生的元素数量由最短序列的元素数量决定:
seq3 = [False, True]
list(zip(seq1, seq2, seq3)) # [('foo', 'one', False), ('bar', 'two', True)]

# zip的常见用途是同时迭代多个序列,甚至可能结合enumerate()函数:
for index, (a, b) in enumerate(zip(seq1, seq2)):
    print(f'{index}: {a}, {b}')

reversed

# reversed会逆序迭代序列元素
list(reversed(range(10)))
# 注意:reversed是一个生成器,它在利用list()函数或for循环实现之前不会创建反向序列

列表、集合以及字典推导式

[expr for value in collection if condition]# 列表推导式的一般形式
{key-expr: value-expr for value in collection if condition} # 字典推导式的一般形式
{expr for value in collection if condition} # 集合推导式的一般形式

# 嵌套列表推导式
all_data = [["John", "Emily", "Michael", "Mary", "Steven"], ["Maria", "Juan", "Javier", "Natalia", "Pilar"]]
result = [name for names in all_data for name in names if name.count('a') >= 2] # 有点像嵌套循环,大循环在前,小循环在后,过滤条件放在末尾

3.2函数

函数是Python中代码组织和重用的主要和最重要方法。

# 每个函数都可以有位置参数和关键字参数。关键字参数常用于指定默认值或可选参数(这里定义了一个函数,其中可选参数z的默认值为1.5):
def my_function2(x, y, z=1.5):
    if z > 1:
        return z*(x+y)
    else:
        return z/(x+y)
# 虽然关键字参数可选,但调用函数时,位置参数必须指定。无论是否提供关键字都可以将值传递给关键字参数z,但是鼓励使用关键字
my_functon2(5, 6, z=0.7)   # 使用关键字传参
my_function2(3.14, 7, 3.5) # 不使用关键字传参
# 函数参数中的主要限制是关键字参数必须跟在位置参数之后。并且可以以任意顺序指定关键字参数(这使你不必记住函数参数的指定顺序,只需要记住它们的名字是什么)

命名空间、作用域和本地函数

# 函数可以访问在函数内部创建的变量,也可以访问函数外部更高(甚至全局)作用域中的变量。用来描述Python中变量范围的术语称为命名空间。
# 默认情况下,在函数中分配的任何变量都将分配给本地命名空间。本地命名空间是在调用函数时创建的,并立即由函数的参数填充。函数完成后,本地命名空间被销毁。
# 可以在函数范围之外分配变量,但这些变量必须使用global或nonlocal关键字显式声明:
a = None
def bind_a_variable():
    global a # 不声明代码也可通过,但是鼓励声明
    a = []
bind_a_variable()
print(a)
# nonlocal允许函数修改在非全局的更高级别作用域中定义的变量(可以参考Python文档来了解)

不鼓励使用global关键字。通常,全局变量用于在系统中存储某种状态。如果你发现自己使用了很多全局变量,则表明你需要面向对象的编程(使用类)。

返回多个值

# Python中返回多个值实际上是返回一个对象(元组或字典等)
def f():
    a = 5
    b = 6
    c = 7
    return {'a': a, 'b': b, 'c': c} # 返回一个字典对象

函数是对象

# 数据处理,将一堆转换(去除空格、删除标点符号、规范大小写)应用于以下字符串列表
import re # 为了使用re模块内置的字符串方法以及用于正则表达式的标准化库
states = ["   Alabama ", "Georgia!", "Georgia", "georgia", "FlOrIda", "south   carolina##", "West virginia?"]
def remove_punctuation(value): # 移除标点符号
    return re.sub('[!#?]', '', value) # 将字符串value中的!#?替换为空字符
    
clean_ops = [str.strip, remove_punctuation, str.title] # 函数是对象,str.strip是可以去掉空格,str.title可以将首字母大写
def clean_strings(strings, ops):
    result = []
    for value in strings:
        for func in ops:
            value = func(value)
        result.append(value)
    return result
    
clean_strings(states, clean_ops)

# 你可以用函数作为别的函数(如内置函数map)的参数,它会将这个函数应用到序列的每一个元素(map可以用作没有过滤器的列表生成式的替代方法)
for x in map(remove_punctuation, states):
    print(x)

匿名(Lambda)函数

# Python支持所谓的匿名或Lambda函数,这是一种编写包含单个语句的函数的方法。它由关键字lambda定义,lambda除了表示“我们正在声明一个匿名函数”之外,没有任何含义
# 匿名函数更加简洁,因为他不需要编写函数声明。
equiv_anon = lambda x: x*2 # 匿名函数可以像正常函数一样使用equiv_anon(5)
# 匿名函数的一个示例:根据每个字符串中不同字母的数量对字符串集排序
strings = ["foo", "card", "bar", "aaaa", "abab"]
strings.sort(key=lambda x: len(set(x)))

生成器

Python中的许多对象都支持迭代,如列表中的对象或文件中的行。这是通过迭代器协议实现的,迭代器协议是使对象可迭代的通用方法

# 迭代字典会产生字典键
some_dict = {'a': 1, 'b': 2, 'c': 3}
for key in some_dict: # Python会首先尝试从字典中创建一个迭代器iter(some_dict)
    print(key)

迭代器是被用于如for循环的环境中才会生成对象给Python解释器的对象。大多数需要列表或类似列表的对象的方法也接受任何可迭代对象。这些方法包括如min,max的内置方法,以及如list和tuple的类型构造方法

list(dict_iterator)

生成器函数

类似于编写普通函数,生成器是一种方便的方法用于构造新的可迭代对象。不同于普通函数一次执行返回单个结果,生成器可以通过暂停和恢复执行来返回多个值的序列。为了创建一个生成器,在函数中需要使用yield关键字而不是return关键字。

# 构造生成器函数squares
def squares(n=10):
    print(f'Generating squares from 1 to {n ** 2}')
    for i in range(1, n+1):
        yield i ** 2
# 创建生成器对象gen,此时不会执行任何代码
gen = squares()
# 从生成器请求元素,它才开始执行代码
for x in gen:
    print(x, end = ' ')

注意:由于生成器一次生成一个元素的输出,而不是一次生成整个列表,因此它可以帮你的程序使用更好内存

生成器表达式

创建生成器的另一种方法是使用生成器表达式。这是一个类似于列表、字典和集合推导式的生成器。为了创建它,只需要将列表推导式的’[]‘改为’()'。

# 创建生成器表达式
gen = (x ** 2 for x in range(100))
# 这等价于以下更详细的生成器函数
def _make_gen():
    for x in range(100):
        yield x ** 2
gen = _make_gen()
# 在某些情况下,可以用生成器表达式代替列表推导式作为函数参数:
sum(x ** 2 for x in range(100))
dict((i, i ** 2) for i in range(5))

itertools模块

标准库itertools模块有许多常见数据算法的生成器集合。

# group函数以任何序列和一个函数为输入,按照函数返回值对序列中的元素进行分组
import itertools
def first_letter(x):
    return x[0]
names = ["Alan", "Adam", "Wes", "Will", "Albert", "Steven"]
for letter, names in itertools.groupby(names, first_letter):
    print(letter, list(names)) # names是一个生成器

下表是一些好用的itertools模块中的函数。

[表]一些有用的itertools函数

错误和异常处理

优雅地处理Python错误或异常是构建健壮程序的重要组成部分。在数据分析应用程序中,许多函数仅适用于某些类型的输入。例如,Python的float函数能够将字符串转换为浮点数,但是对不合适的输入会报ValueError:

float('1.2345') # 正确转化为1.2345
float('something') # 报错:ValueError

# 通过将函数包含在try/except块中进行异常处理
def attempt_float(x):
    try:
        return float(x)
    except:      # 可以只指定ValueError,也可以设置异常类型的元组(TypeError, ValueError)来捕获多个异常类型,注意括号是必须的
        return x # 仅在引发异常时,才会执行except块中的代码
        
# 使用finally语句,可以保证无论try块中的代码成功执行或异常,finally块中的语句都能执行
f = open(pah, mode='w')
try:
    write_to_file(f)
finally:
    f.close() # 一定会执行
   
# 在try...except后,使用else语句,可以在try块成功时执行else里面的代码
f = open(path, mode='w')
try:
    write_to_file(f)
except:
    print('Failed')    # try块中的内容没有被正确执行,则执行except内的语句
else:
    print('Succeeded') # try块中的内容被正确执行,则执行else内的语句
finally:
    f.close()    

Ipython中的异常

当你在使用"%run"运行脚本或执行任何语句时发生异常,则IPython将默认打印一个完整的调用堆栈跟踪(traceback),并在堆栈中的每个点的周围显示几行上下文。

与标准Python解释器(不提供任何额外的上下文)相比,拥有额外的上下文具有很大的优势。你可以使用magic命令控制显示的上下文量。在“附录B:更多关于Ipython系统”中可以看到,在错误发生后,可以通过%debug或%pdb魔术方法来单步执行堆栈。

3.3文件和操作系统

本书的大部分内容都使用高级工具如pandas.read_csv来从磁盘读取数据到Python数据结构中。但是,了解如何在Python中使用文件的基础知识非常重要。

# 要打开文件进行读取或写入,可以使用内置的open函数,同时设置文件的相对路径或者绝对路径,以及一个可选的文件编码:
path = 'examples/segismundo.txt'
f = open(path, encoding='utf-8') # 默认情况下,文件以只读模式打开
for line in f: # 将文件对象视为列表,通过for循环访问这些行
    print(line)
f.close() # 当使用open创建文件对象时,建议处理完文件后通过f.close()将其关闭,关闭文件会将其资源释放回操作系统

# 通过with语句可以更方便清理打开的文件,在退出with块之后文件f会自动被关闭
with open(path, encoding='utf-8') as f:
    lines = [x.rstrip() for x in f]
# 注意:不能确保文件已关闭在许多小程序和小脚本中不会有问题,但是在需要与大量文件交互的程序中,可能会是一个问题

下表是所有有效的文件读/写模式的列表:

[表]Python文件模式

# 对于可读文件,一些最常用的方法是:read, seek和tell. 

# read从文件中返回一定数量的字符(由文件的编码决定)或字节(如果文件是以二进制模式打开,则返回字节)。参数未指定则返回整个文件。
f1 = open(path)
f1.read(10) # 读取10个字符:'Sueña el r'
f2 = open(path, mode='rb') # 二进制只读
f2.read(10) # 读取10个字节:b'Sue\xc3\xb1a el '
# 注意:这里字符'ñ'对应两个字节'\xc3\xb1'

# tell方法返回文件读/写指针当前的位置
f1.tell() # 返回11:当前指针指向第11个字节(从0开始记录)
f2.tell() # 返回10:当前指针指向第10个字节
# 注意:即使我们从以文本模式打开的文件f1中读取了10个字符,位置仍是11,因为使用默认编码解码10个字符需要花费很多字节。你可以在sys模块中检查默认编码:
import sys
sys.getdefaultencoding() # 返回'utf-8'
# 若想要跨平台的情况下获得一致性的行为,最好在打开文件时传递编码(如广泛使用的'utf-8'编码)

# seek方法将改变文本读/写指针到指定的位置
fileObject.seek(offset, whence=0) # 调用seek函数的一般格式:offset表示移动偏移的字节数
# whence表示从哪个位置开始,0表示从文件头开始,1表示从当前位置开始,2表示从文件末尾开始
f1.seek(3) # 当前指针移到第3个字节
f1.read(1) # 从当前指针开始读取一个字符:返回'ñ',对应两个字节,于是指针移动两个字节
f1.tell()  # 当前指针移动到第5个字节

# 关闭文件
f1.close()
f2.close()

# 若要将文本写入文件,可以使用文件的write或writelines方法。例如,我们可以创建一个没有空行的examples/segismundo.txt文件如下:
path = 'examples/segismundo.txt'
with open('tmp.txt', mode='w') as handle: # 只写模式('w')创建tmp.txt文件
    handle.writelines(x for x in open(path) if len(x)>1) # 读取'examples/segismundo.txt'的每一行,如果长度>1则写入
with open('tmp.txt') as f: # 只读模式('r')打开tmp.txt文件
    lines = f.readlines()  # 返回文件的全部行组成的列表
lines
# 注意:若readlines()指定参数size,则返回size行的列表

[表]重要的Python文件方法或属性

文件中的字节和统一码(Unicode)

Python文件的默认行为(无论是可读的还是可写的)是文本模式,这意味着你往往使用的是Python字符串(即,Unicode)。这与二进制文本模式形成鲜明对比,二进制文本模式可以通过附加’b’到文件模式中来实现。

# 重新访问上一节中的文件(它包含具有UTF-8编码的非ASCII字符)
with open(pah) as f:
    chars = f.read(10)
chars # 返回'Sueña el r'
len(chars)

UTF-8是一种可变长度的Unicode编码,因此当我们从文件中请求一定数量的字符时,Python会从文件中读取足够的字节(最少10个,最多40个字节)来解码这么多字符。

# 如果我用'rb'(二进制只读)打开文件,则read函数会请求确切的字节数
with open(path, mode='rb') as f:
    data = f.read(10)
data # 返回b'Sue\xc3\xb1a el '

根据文本编码,你能够将字节解码为str对象,但前提是每个编码的Unicdoe字符都已完全形成

data.decode('utf-8') # 能够正常进行'utf-8'解码

data[:4].decode('utf-8') # 由于第四个字节0xc3不能被'utf-8'解码称正常的字符,所以解码失败

文本模式与open函数的encoding选项结合,提供了一种从一个Unicode到另一个Unicode编码的便捷方法:

sink_path = 'sink.txt'
with open(path) as source:
    with open(sink_path, 'x', encoding='iso-8859-1') as sink:
        sink.write(source.read())
with open(sink_path, encoding='iso-8859-1') as f:
    print(f.read(10))

在以二进制文件以外的任何模式打开文件时,请注意用seek函数。如果文件读/写指针位于Unicode字符的字节中间,则后续读取将导致错误:

f = open(path, encoding='utf-8')
f.read(5)
f.seek(4)
f.read(1) # 此时落在指针落在0xb1上,读取一个字节无法构成合法的unicode字符,报错

3.4结论

有了Python环境和语言的一些基础知识,现在是时候继续学习Python中的Numpy和面向数组的计算了。

你可能感兴趣的:(python,python,学习)