第 3 章:内置数据结构、函数和文件
本章深入探讨 Python 中对数据分析至关重要的基本构建块。我们将探索 Python 的内置数据结构,如何创建可重用的函数,以及如何与文件交互。
Note
虽然像 pandas 和 NumPy 这样的库为更大的数据集提供了高级功能,但它们的设计目的是与 Python 的核心数据操作工具协同工作。掌握这些基础知识至关重要!🛠️
Python 提供了几种通用的数据结构。我们将从以下几个开始:
理解这些是掌握 Python 的关键一步。🐍
元组是固定长度、不可变的 Python 对象序列。一旦创建,就不能更改其元素或大小。
不可变性
不可变性意味着内容在创建后不能更改。这确保了数据的完整性。可以把它想象成一个密封的容器📦——你可以看到里面的东西,但你不能交换东西。
使用逗号分隔的值创建元组,通常在括号中:
括号通常是可选的:
使用 tuple()
将序列/迭代器转换为元组:
使用 []
访问元素(从 0 开始索引):
元组可以包含其他元组:
虽然元组内的对象可能是可变的,但元组本身是不可变的:
Caution
你不能将新对象分配给元组中的位置,但是你可以修改元组中可变对象的内容。
使用 +
连接:
使用 *
重复:
Note
只会复制对象的引用,而不是对象本身。
将元组解包到变量中:
嵌套元组也可以:
优雅的变量交换:
*rest
捕获剩余的元素:
_
用于不需要的变量:
由于不可变性,元组的方法很少。count()
很有用:
列表是可变长度且可变的。你可以在创建后更改其内容和大小。
可变性
可变性意味着你可以在创建后更改元素、添加新元素或删除现有元素。列表非常灵活!🤸♀️
使用 []
或 list()
创建列表:
修改元素:
list()
实体化迭代器/生成器:
append()
: 添加到末尾。insert()
: 在特定位置插入。Caution
insert
比 append
开销更大(它会移动元素)。
pop()
: 删除并返回指定索引处的元素。remove()
: 删除第一个出现的指定值。Note
对于列表,in
/ not in
速度很慢(线性扫描)。字典和集合快得多(哈希表,常数时间)。
使用 +
连接:
extend()
追加多个元素:
Tip
extend()
通常比 +
快(不创建新列表)。
sort()
: 就地排序。key
参数: 提供自定义排序方法。使用 start:stop
选择部分:
start
: 默认为开头。stop
: 默认为结尾。使用步长选择每 n 个元素:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
H | E | L | L | O | ! |
0 | 1 | 2 | 3 | 4 | 5 |
-6 | -5 | -4 | -3 | -2 | -1 |
此图说明了在字符串“HELLO!”上的切片。索引显示在“格子边缘”,以帮助显示使用正索引或负索引时切片选择的开始和停止位置。
字典(或 dict
)至关重要。它们存储键值对(类似于哈希映射)。
键值对
每个键都与一个值相关联。键是唯一且不可变的(字符串、数字、元组)。值可以是任何东西。
使用 {}
和 :
:
del
:d1[5] = 'some value' # 添加键值对 5: 'some value'
d1['dummy'] = 'another value' # 添加键值对 'dummy': 'another value'
print(d1) # 输出: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer', 5: 'some value', 'dummy': 'another value'}
del d1[5] # 删除键 5 及其对应的值
print(d1) # 输出: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer', 'dummy': 'another value'}
pop()
: 删除并返回。keys()
: 键的迭代器。values()
: 值的迭代器。items()
: 键值对的迭代器。Note
键的顺序取决于插入顺序。keys
和 values
以相同的顺序返回迭代器。
update()
合并字典:
Caution
update()
是就地操作。现有键的值会被丢弃。
key_list = ['a', 'b', 'c'] # 创建一个键列表
value_list = [1, 2, 3] # 创建一个值列表
mapping = {} # 创建一个空字典
for key, value in zip(key_list, value_list): # 使用 zip 将键列表和值列表配对
mapping[key] = value # 将键值对添加到字典中
print(mapping) # 输出: {'a': 1, 'b': 2, 'c': 3}
# 简洁写法:dict() 和 zip()
mapping = dict(zip(range(5), reversed(range(5)))) # 使用 zip 和 reversed 创建字典
print(mapping) # 输出: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
zip
函数可以将多个序列的元素配对。dict
接受一个由 2 元组组成的列表。
如果键不存在,get
返回 None
(或指定的默认值),而 pop
会引发异常。
setdefault(key, default)
: 如果 key
存在,则返回其值。如果不存在,则插入 key
并设置值为 default
,然后返回 default
。
collections.defaultdict
简化了初始化:
hash()
检查:集合是无序的唯一元素集合(类似于只有键的字典)。
a = {1, 2, 3, 4, 5} # 创建集合 a
b = {3, 4, 5, 6, 7, 8} # 创建集合 b
# 并集 (| 或 union())
print(a.union(b)) # 输出: {1, 2, 3, 4, 5, 6, 7, 8}
print(a | b) # 输出: {1, 2, 3, 4, 5, 6, 7, 8}
# 交集 (& 或 intersection())
print(a.intersection(b)) # 输出: {3, 4, 5}
print(a & b) # 输出: {3, 4, 5}
# 差集 (- 或 difference())
print(a.difference(b)) # 输出: {1, 2}
print(a - b) #输出: {1, 2}
函数 | 替代语法 | 描述 |
---|---|---|
a.add(x) |
N/A | 将元素 x 添加到集合 a |
a.clear() |
N/A | 将集合 a 重置为空集,丢弃所有元素 |
a.remove(x) |
N/A | 从集合 a 中移除元素 x |
a.pop() |
N/A | 从集合 a 中移除并返回一个任意元素,如果集合为空则引发 KeyError |
a.union(b) |
a \| b |
a 和 b 中的所有唯一元素 |
a.update(b) |
a \|= b |
将 a 的内容设置为 a 和 b 中元素的并集 |
a.intersection(b) |
a & b |
a 和 b 中的所有元素 |
a.intersection_update(b) |
a &= b |
将 a 的内容设置为 a 和 b 中元素的交集 |
a.difference(b) |
a - b |
a 中不在 b 中的元素 |
a.difference_update(b) |
a -= b |
将 a 设置为 a 中不在 b 中的元素 |
a.symmetric_difference(b) |
a ^ b |
a 或 b 中的所有元素,但不同时在两者中 |
a.symmetric_difference_update(b) |
a ^= b |
将 a 设置为包含 a 或 b 中的元素,但不同时在两者中 |
a.issubset(b) |
<= |
如果 a 的所有元素都包含在 b 中,则为 True |
a.issuperset(b) |
>= |
如果 b 的所有元素都包含在 a 中,则为 True |
a.isdisjoint(b) |
N/A | 如果 a 和 b 没有共同元素,则为 True |
存在就地版本(例如,a |= b
):
Tip
对于大型集合,就地运算更有效。
与字典键一样,集合元素必须是不可变且可哈希的。
如果内容相等,则集合相等:
enumerate
在迭代期间跟踪索引:
enumerate
返回 (index, value)
元组。
sorted
返回一个新的排序列表:
Note
sorted()
返回一个新列表。list.sort()
就地排序。
zip
将元素“配对”:
zip
可以接受任意数量的序列。输出长度由最短的序列决定:
使用 enumerate
迭代多个序列:
reversed
以相反的顺序迭代:
Note
reversed
是一个生成器(在实体化之前不会创建反向序列)。
推导式可以简洁地创建新集合。
示例:
类似于列表推导式,但使用 {}
(创建一个集合)。
# {key_expr: value_expr for val in collection if condition}
strings = ['a', 'as', 'bat', 'car', 'dove', 'python'] # 创建一个字符串列表
# 使用字典推导式创建一个字典,其中键是字符串,值是字符串在列表中的索引
loc_mapping = {val: index for index, val in enumerate(strings)}
print(loc_mapping) # 输出: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}
all_data = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]
# 使用嵌套列表推导式找出名字中包含2个以上a的名字。
names_of_interest = [name for names in all_data for name in names
if name.count('a') >= 2]
print(names_of_interest) # 输出: ['Maria', 'Natalia']
some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)] # 创建一个元组列表
flattened = [x for tup in some_tuples for x in tup] # 使用嵌套列表推导式展开元组
print(flattened) # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 列表推导式中的列表推导式
flattened = [[x for x in tup] for tup in some_tuples] # 两层列表推导
print(flattened) # 输出: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Note
for
的顺序与嵌套 for
循环中的顺序相同。
函数组织和重用代码。
return
返回一个值。return
语句,则隐式返回 None
。def my_function2(x, y, z=1.5): # 定义函数,z 是关键字参数,默认值为 1.5
if z > 1: # 如果 z 大于 1
return z * (x + y) # 返回 z 乘以 x 和 y 的和
else: # 否则
return z / (x + y) # 返回 z 除以 x 和 y 的和
print(my_function2(5, 6, z=0.7)) # 使用关键字参数调用函数,输出: 0.06363636363636363
print(my_function2(3.14, 7, 3.5)) # 使用位置参数调用函数,输出: 35.49
print(my_function2(10, 20)) # 使用默认参数调用函数,输出: 45.0
global
或 nonlocal
。Caution
尽量减少 global
的使用。更好的设计可以减少对全局状态的依赖。
函数是一等公民:
import re # 导入正则表达式模块
states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
'south carolina##', 'West virginia?'] # 列表
def clean_strings(strings): # 定义一个函数,用于清理字符串列表
result = [] # 创建一个空列表
for value in strings: # 遍历列表中的每个字符串
value = value.strip() # 去除字符串两端的空格
value = re.sub('[!#?]', '', value) # 使用正则表达式去除标点符号
value = value.title() # 将字符串转换为标题格式
result.append(value) # 将处理后的字符串添加到列表中
return result # 返回处理后的列表
print(clean_strings(states)) #输出: ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'South Carolina', 'West Virginia']
# 替代方案:函数列表
def remove_punctuation(value): # 定义一个函数,用于去除标点符号
return re.sub('[!#?]', '', value) # 使用正则表达式去除标点符号
clean_ops = [str.strip, remove_punctuation, str.title] # 定义一个函数列表
def clean_strings_functional(strings, ops): # 定义一个函数,接受字符串列表和操作列表
result = [] # 创建一个空列表
for value in strings: # 遍历字符串列表中的每个字符串
for function in ops: # 遍历操作列表中的每个函数
value = function(value) # 对字符串应用函数
result.append(value) # 将处理后的字符串添加到列表中
return result # 返回处理后的列表
print(clean_strings_functional(states, clean_ops)) #输出: ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'South Carolina', 'West Virginia']
# 将函数与 map 结合使用:
for x in map(remove_punctuation, states): # 使用 map 函数对列表中的每个字符串应用 remove_punctuation 函数
print(x)
这种方式很灵活,而且可以重用。
用于传递短函数很方便:
def apply_to_list(some_list, f): # 定义一个函数,接受一个列表和一个函数作为参数
return [f(x) for x in some_list] # 对列表中的每个元素应用函数 f,并返回结果列表
ints = [4, 0, 1, 5, 6] # 创建一个整数列表
result = apply_to_list(ints, lambda x: x * 2) # 将 lambda 函数作为参数传递
print(result) # 输出: [8, 0, 2, 10, 12]
strings = ['foo', 'card', 'bar', 'aaaa', 'abab'] # 创建一个字符串列表
strings.sort(key=lambda x: len(set(x))) # 使用 lambda 函数作为 key,按照字符串中不同字符的数量排序
print(strings) # 输出: ['aaaa', 'foo', 'abab', 'bar', 'card']
生成器按需生成值(节省内存)。
def squares(n=10): # 定义一个生成器函数,生成 1 到 n^2 的平方数
print('Generating squares from 1 to %d' % n ** 2) # 打印一条消息
for i in range(1, n + 1): # 循环 1 到 n
yield i ** 2 # 使用 yield 产生 i 的平方
gen = squares() # 创建一个生成器对象
print(gen) # 输出: <generator object squares at 0x...> (生成器对象)
for x in gen: # 请求值
print(x, end=' ') # 输出: 1 4 9 16 25 36 49 64 81 100
yield
而不是 return
。简洁的生成器(类似于推导式):
# 列表推导式:[x ** 2 for x in range(100)]
gen = (x ** 2 for x in range(100)) # 使用圆括号创建生成器表达式
print(gen) # 输出: <generator object <genexpr> at 0x...> (生成器对象)
print(sum(gen)) # 输出: 328350
# 作为函数参数:
print(sum(x ** 2 for x in range(100))) # 将生成器表达式作为参数传递给 sum 函数,输出: 328350
print(dict((i, i **2) for i in range(5))) # 将生成器表达式作为参数传递给 dict 函数,输出: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
itertools
提供了有用的生成器:
import itertools # 导入 itertools 模块
def first_letter(x): # 定义一个函数,返回字符串的第一个字母
return x[0]
names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven'] # 创建一个名字列表
for letter, names_iter in itertools.groupby(names, first_letter): # 使用 itertools.groupby 按首字母分组
print(letter, list(names_iter)) # 输出每组的首字母和名字列表
itertools.groupby
对连续的元素进行分组。
函数 | 描述 |
---|---|
chain(*iterables) |
链接迭代器。 |
combinations(iterable, k) |
生成长度为 k 的组合,忽略顺序,不放回。 |
permutations(iterable, k) |
生成长度为 k 的排列,考虑顺序。 |
groupby(iterable[, keyfunc]) |
为每个唯一键生成 (key, sub-iterator)。 |
product(*iterables, repeat=1) |
生成笛卡尔积(类似于嵌套 for 循环)。 |
Note
查看 itertools
文档!这是一个宝库!💎
处理错误对于健壮的代码至关重要。
try
: 可能引发异常的代码。except
: 如果发生异常,则执行。指定异常类型(首选)。
使用元组捕获多种类型的异常。
finally
: 始终执行(清理)。else
: 如果 try
块没有引发异常,则执行。IPython 提供了有用的回溯信息。使用 %xmode
控制详细程度(Plain、Context、Verbose)。
open(path, mode='r', encoding=None)
path
: 文件路径。mode
: ‘r’ (读取), ‘w’ (写入), ‘a’ (追加), ‘x’ (创建), ‘rb’, ‘wb’ 等。encoding
: 文件编码 (例如, ‘utf-8’)。f.close()
。with
语句会自动关闭文件:
Tip
使用 with
!即使出现错误,它也能确保清理。
模式 | 描述 |
---|---|
r | 只读 |
w | 只写;创建新文件(擦除现有文件) |
x | 只写;创建新文件 |
a | 追加到现有文件(如果需要,创建新文件) |
r+ | 读写 |
b | 二进制模式(添加到模式:‘rb’, ‘wb’) |
t | 文本模式(解码字节);默认 |
f1 = open(path, encoding = "utf-8") # 以文本模式打开文件,指定编码为 utf-8
print(f1.read(10)) # 读取 10 个*字符*
f2 = open(path, mode='rb') # 以二进制模式打开文件
print(f2.read(10)) # 读取 10 个*字节*
print(f1.tell()) # 获取当前位置(字符)
print(f2.tell()) # 获取当前位置(字节)
import sys
print(sys.getdefaultencoding()) # 获取默认编码
f1.seek(3) # 移动到第 3 个字节/字符
print(f1.read(1)) # 读取 1 个字符
f1.close() # 关闭文件
f2.close() # 关闭文件
read(n)
: 读取 n
个字符/字节。tell()
: 当前位置。seek(position)
: 移动指针。seek
和 UTF-8(多字节字符)。with open('tmp.txt', 'w', encoding = "utf-8") as handle: # 以写入模式打开一个临时文件,指定编码为 utf-8
# 将 segismundo.txt 文件中长度大于 1 的行写入 tmp.txt
handle.writelines(x for x in open(path, encoding = "utf-8") if len(x) > 1)
with open('tmp.txt', encoding = "utf-8") as f: # 以读取模式打开临时文件,指定编码为 utf-8
lines = f.readlines() # 读取所有行
print(lines) # 打印所有行
import os
os.remove("tmp.txt") # 删除临时文件
write(string)
: 写入字符串。writelines(list_of_strings)
: 写入字符串列表。##重要的文件方法/属性
方法/属性 | 描述 |
---|---|
read([size]) |
返回数据(字节/字符串)。 |
readable() |
如果可读,则返回 True 。 |
readlines([size]) |
返回行的列表。 |
write(string) |
写入字符串。 |
writable() |
如果可写,则返回 True 。 |
writelines(strings) |
写入字符串序列。 |
close() |
关闭文件。 |
flush() |
将缓冲区刷新到磁盘。 |
seek(pos) |
移动到指定位置。 |
seekable() |
如果可寻址,则返回 True 。 |
tell() |
返回当前位置。 |
closed |
如果已关闭,则返回 True 。 |
encoding |
返回文件编码 (例如, UTF-8)。 |
mode='rb'
/'wb'
: 二进制模式(字节)。with open(path, encoding = "utf-8") as f: # 以文本模式打开文件
chars = f.read(10) # 读取前10个字符
print(len(chars)) #输出字符数
with open(path, 'rb') as f: # 以二进制模式打开文件
data = f.read(10) # 读取前10个字节
print(data)
print(data.decode('utf-8')) # 将字节解码为字符串
# print(data[:4].decode('utf-8')) # 这行代码可能会引发 UnicodeDecodeError(不完整的多字节字符)
read(n)
个字符 ≠ read(n)
个字节。read
返回确切的字节。sink_path = 'sink.txt' # 设置一个用于演示的文件名
with open(path, encoding = "utf-8") as source: # 以utf-8编码读取
with open(sink_path, 'x', encoding='iso-8859-1') as sink: # 以iso-8859-1编码写入
sink.write(source.read())
with open(sink_path, encoding='iso-8859-1') as f: # 以iso-8859-1编码读取
print(f.read(10))
import os
os.remove(sink_path) # 删除临时文件
# 注意:
f = open(path, encoding = "utf-8") # 以utf-8编码读取
print(f.read(5))
f.seek(4) # 移动到第4个字节位置
# print(f.read(1)) # 这行代码可能会引发 UnicodeDecodeError,因为可能在多字节字符的中间
f.close()
Caution
在非二进制模式下使用 seek
时要非常小心。
本章涵盖了:
enumerate
、sorted
、zip
、reversed
。itertools
:有用的迭代器工具。try
、except
、finally
、else
。这些是 Python 数据分析的基础!🧱
with
语句: 文件处理的优势?邱飞 💌 [email protected]