[学习笔记]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和面向数组的计算了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这本书主要是用 pandas 连接 SciPy 和 NumPy,用pandas做数据处理是Pycon2012上一个很热门的话题。另一个功能强大的东西是Sage,它将很多开源的软件集成到统一的 Python 接口。, Python for Data Analysis is concerned with the nuts and bolts of manipulating, processing, cleaning, and crunching data in Python. It is also a practical, modern introduction to scientific computing in Python, tailored for data-intensive applications. This is a book about the parts of the Python language and libraries you’ll need to effectively solve a broad set of data analysis problems. This book is not an exposition on analytical methods using Python as the implementation language., Written by Wes McKinney, the main author of the pandas library, this hands-on book is packed with practical cases studies. It’s ideal for analysts new to Python and for Python programmers new to scientific computing., Use the IPython interactive shell as your primary development environment, Learn basic and advanced NumPy (Numerical Python) features, Get started with data analysis tools in the pandas library, Use high-performance tools to load, clean, transform, merge, and reshape data, Create scatter plots and static or interactive visualizations with matplotlib, Apply the pandas groupby facility to slice, dice, and summarize datasets, Measure data by points in time, whether it’s specific instances, fixed periods, or intervals, Learn how to solve problems in web analytics, social sciences, finance, and economics, through detailed examples
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值