第 3 章:内置数据结构、函数和文件

本章深入探讨 Python 中对数据分析至关重要的基本构建块。我们将探索 Python 的内置数据结构,如何创建可重用的函数,以及如何与文件交互。

Note

虽然像 pandas 和 NumPy 这样的库为更大的数据集提供了高级功能,但它们的设计目的是与 Python 的核心数据操作工具协同工作。掌握这些基础知识至关重要!🛠️

数据结构和序列

Python 提供了几种通用的数据结构。我们将从以下几个开始:

  • 元组 (Tuples)
  • 列表 (Lists)
  • 字典 (Dictionaries)

理解这些是掌握 Python 的关键一步。🐍

元组:定义

元组是固定长度不可变的 Python 对象序列。一旦创建,就不能更改其元素或大小。

不可变性

不可变性意味着内容在创建后不能更改。这确保了数据的完整性。可以把它想象成一个密封的容器📦——你可以看到里面的东西,但你不能交换东西。

元组:创建

使用逗号分隔的值创建元组,通常在括号中:

tup = (4, 5, 6)  # 使用圆括号创建元组
print(tup)      # 输出: (4, 5, 6)

括号通常是可选的:

tup = 4, 5, 6  # 不使用括号创建元组
print(tup)    # 输出: (4, 5, 6)

元组:转换和访问

使用 tuple() 将序列/迭代器转换为元组:

my_list = [4, 0, 2]           # 创建一个列表
my_tuple = tuple(my_list)     # 将列表转换为元组
print(my_tuple)              # 输出: (4, 0, 2)

string_tuple = tuple('string') # 将字符串转换为元组
print(string_tuple)           # 输出: ('s', 't', 'r', 'i', 'n', 'g')

使用 [] 访问元素(从 0 开始索引):

print(string_tuple[0])  # 访问元组的第一个元素,输出: 's'

元组:嵌套元组

元组可以包含其他元组:

nested_tup = (4, 5, 6), (7, 8) # 创建一个嵌套元组
print(nested_tup)             # 输出: ((4, 5, 6), (7, 8))
print(nested_tup[0])          # 访问第一个元组,输出: (4, 5, 6)
print(nested_tup[1][0])       # 访问第二个元组的第一个元素,输出: 7

元组:不可变性(详细示例)

虽然元组的对象可能是可变的,但元组本身是不可变的:

tup = tuple(['foo', [1, 2], True]) # 创建一个包含列表的元组
# tup[2] = False  # 这行代码会引发 TypeError,因为元组不可变!

# 但是,可以*在原处*修改可变元素:
tup[1].append(3)  # 向列表 [1, 2] 中追加元素 3
print(tup)       # 输出: ('foo', [1, 2, 3], True)

Caution

你不能将对象分配给元组中的位置,但是你可以修改元组中可变对象的内容

元组:连接和重复

使用 + 连接:

tuple1 = (4, None, 'foo')          # 创建元组 tuple1
tuple2 = (6, 0)                   # 创建元组 tuple2
tuple3 = ('bar',)                 # 创建单元素元组 tuple3(注意逗号)
combined_tuple = tuple1 + tuple2 + tuple3 # 连接三个元组
print(combined_tuple)             # 输出: (4, None, 'foo', 6, 0, 'bar')

使用 * 重复:

repeated_tuple = ('foo', 'bar') * 4 # 将元组重复 4 次
print(repeated_tuple)             # 输出: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

Note

只会复制对象的引用,而不是对象本身。

元组:解包

将元组解包到变量中:

tup = (4, 5, 6) # 创建一个元组
a, b, c = tup    # 将元组解包到变量 a, b, c 中
print(b)        # 输出: 5

嵌套元组也可以:

tup = 4, 5, (6, 7) # 创建一个嵌套元组
a, b, (c, d) = tup  # 解包嵌套元组
print(d)         # 输出: 7

元组:交换变量

优雅的变量交换:

a, b = 1, 2        # 将 a 赋值为 1,b 赋值为 2
print(f"a: {a}, b: {b}") # 输出: a: 1, b: 2
b, a = a, b        # 交换 a 和 b 的值
print(f"a: {a}, b: {b}") # 输出: a: 2, b: 1

元组:*rest (和 _)

*rest 捕获剩余的元素:

values = 1, 2, 3, 4, 5 # 创建一个元组
a, b, *rest = values   # 将前两个元素赋值给 a 和 b,其余赋值给 rest
print(a)             # 输出: 1
print(b)             # 输出: 2
print(rest)          # 输出: [3, 4, 5]

_ 用于不需要的变量:

a, b, *_ = values  # 忽略剩余的元素

元组:方法 (count)

由于不可变性,元组的方法很少。count() 很有用:

a = (1, 2, 2, 2, 3, 4, 2) # 创建一个元组
count_of_2 = a.count(2)   # 计算元组中 2 出现的次数
print(count_of_2)         # 输出: 4
# `count` 方法返回指定值出现的次数。

列表:定义和可变性

列表是可变长度可变的。你可以在创建后更改其内容和大小。

可变性

可变性意味着你可以在创建后更改元素、添加新元素或删除现有元素。列表非常灵活!🤸‍♀️

列表:创建

使用 []list() 创建列表:

a_list = [2, 3, 7, None]     # 使用方括号创建列表
tup = ('foo', 'bar', 'baz')  # 创建一个元组
b_list = list(tup)          # 将元组转换为列表
print(b_list)               # 输出: ['foo', 'bar', 'baz']

修改元素:

b_list[1] = 'peekaboo' # 将列表的第二个元素修改为 'peekaboo'
print(b_list)          # 输出: ['foo', 'peekaboo', 'baz']

列表:实体化迭代器

list() 实体化迭代器/生成器:

gen = range(10)     # 创建一个 range 对象(迭代器)
print(gen)          # 输出: range(0, 10)
my_list = list(gen) # 将 range 对象转换为列表
print(my_list)      # 输出: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

列表:添加元素

  • append(): 添加到末尾
b_list.append('dwarf') # 在列表末尾添加 'dwarf'
print(b_list)         # 输出: ['foo', 'peekaboo', 'baz', 'dwarf']
  • insert(): 在特定位置插入。
b_list.insert(1, 'red') # 在索引 1 处插入 'red'
print(b_list)           # 输出: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']

Caution

insertappend 开销更大(它会移动元素)。

列表:删除元素 (pop 和 remove)

  • pop(): 删除并返回指定索引处的元素。
removed_element = b_list.pop(2) # 删除索引 2 处的元素,并将其赋值给 removed_element
print(removed_element)         # 输出: peekaboo
print(b_list)                   # 输出: ['foo', 'red', 'baz', 'dwarf']
  • remove(): 删除第一个出现的指定值。
b_list.append('foo')         # 再次在列表末尾添加 'foo'
b_list.remove('foo')        # 删除第一个出现的 'foo'
print(b_list)               # 输出: ['red', 'baz', 'dwarf', 'foo']

列表:检查成员资格 (in 和 not in)

print('dwarf' in b_list)     # 检查 'dwarf' 是否在列表中,输出: True
print('dwarf' not in b_list) # 检查 'dwarf' 是否不在列表中,输出: False

Note

对于列表,in / not in 速度很(线性扫描)。字典和集合快得多(哈希表,常数时间)。

列表:连接和合并

使用 + 连接:

list1 = [4, None, 'foo']      # 创建列表 list1
list2 = [7, 8, (2, 3)]      # 创建列表 list2
combined_list = list1 + list2 # 连接两个列表
print(combined_list)         # 输出: [4, None, 'foo', 7, 8, (2, 3)]

extend() 追加多个元素:

x = [4, None, 'foo']       # 创建列表 x
x.extend([7, 8, (2, 3)]) # 使用 extend 方法追加多个元素
print(x)                   # 输出: [4, None, 'foo', 7, 8, (2, 3)]

Tip

extend() 通常比 + 快(不创建新列表)。

列表:排序 (sort 和 sorted)

  • sort(): 就地排序。
a = [7, 2, 5, 1, 3] # 创建一个列表
a.sort()            # 对列表进行就地排序
print(a)            # 输出: [1, 2, 3, 5, 7]
  • key 参数: 提供自定义排序方法。
b = ['saw', 'small', 'He', 'foxes', 'six'] # 创建一个字符串列表
b.sort(key=len)   # 按照字符串长度对列表进行排序
print(b)            # 输出: ['He', 'saw', 'six', 'small', 'foxes']

列表:切片

使用 start:stop 选择部分:

seq = [7, 2, 3, 7, 5, 6, 0, 1] # 创建一个列表
sub_list = seq[1:5]          # 从索引 1(包含)到 5(不包含)进行切片
print(sub_list)              # 输出: [2, 3, 7, 5]

seq[3:5] = [6,3] # 使用切片替换元素
print(seq) # 输出: [7, 2, 3, 6, 3, 6, 0, 1]

列表:切片(省略 start/stop 和负索引)

  • 省略 start: 默认为开头。
  • 省略 stop: 默认为结尾。
  • 负索引:从末尾开始计数。
print(seq[:5])    # 获取前 5 个元素,输出: [7, 2, 3, 6, 3]
print(seq[3:])    # 从索引 3 开始到结尾,输出: [6, 3, 6, 0, 1]
print(seq[-4:])   # 获取最后 4 个元素,输出: [3, 6, 0, 1]
print(seq[-6:-2]) # 获取倒数第 6 个到倒数第 2 个元素(不包含),输出: [3, 6, 3, 6]

列表:切片(步长)

使用步长选择每 n 个元素:

print(seq[::2])   # 每隔一个元素取一个,输出: [7, 3, 3, 0]
print(seq[::-1])  # 反转列表,输出: [1, 0, 6, 3, 6, 3, 2, 7]

列表切片图示

0 1 2 3 4 5
H E L L O !
0 1 2 3 4 5
-6 -5 -4 -3 -2 -1
string = "HELLO!"
print(string[2:4]) # 使用正索引进行切片,输出: LL
print(string[-5:-2]) # 使用负索引进行切片, 输出: ELL

此图说明了在字符串“HELLO!”上的切片。索引显示在“格子边缘”,以帮助显示使用正索引或负索引时切片选择的开始和停止位置。

字典 (dict):定义

字典(或 dict)至关重要。它们存储键值对(类似于哈希映射)。

键值对

每个都与一个相关联。键是唯一不可变的(字符串、数字、元组)。值可以是任何东西。

字典:创建

使用 {}:

empty_dict = {} # 创建一个空字典
d1 = {'a': 'some value', 'b': [1, 2, 3, 4]} # 创建一个字典
print(d1) # 输出: {'a': 'some value', 'b': [1, 2, 3, 4]}

字典:访问、插入、设置

d1[7] = 'an integer'  # 添加键值对 7: 'an integer'
print(d1)            # 输出: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
print(d1['b'])       # 访问键 'b' 对应的值,输出: [1, 2, 3, 4]

字典:检查键 (in)

print('b' in d1)  # 检查键 'b' 是否存在于字典中,输出: True

字典:删除 (del 和 pop)

  • 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(): 删除并返回
ret = d1.pop('dummy') # 删除键 'dummy' 及其对应的值,并将值赋给 ret
print(ret)            # 输出: another value
print(d1)            # 输出: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}

字典:keys(), values() 和 items() 方法

  • keys(): 键的迭代器。
  • values(): 值的迭代器。
  • items(): 键值对的迭代器。
print(list(d1.keys()))   # 获取字典中所有键的列表,输出: ['a', 'b', 7]
print(list(d1.values())) # 获取字典中所有值的列表,输出: ['some value', [1, 2, 3, 4], 'an integer']
print(list(d1.items()))  # 获取字典中所有键值对的列表,输出: [('a', 'some value'), ('b', [1, 2, 3, 4]), (7, 'an integer')]

Note

键的顺序取决于插入顺序。keysvalues相同的顺序返回迭代器。

字典:使用 update() 合并

update() 合并字典:

d1.update({'b': 'foo', 'c': 12})  # 更新键 'b' 的值,并添加键值对 'c': 12
print(d1)                       # 输出: {'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12}

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 和 setdefault)

# 冗长写法:
if 'some_key' in some_dict:   # 检查键是否存在
    value = some_dict['some_key'] # 如果存在,获取值
else:
    value = default_value     # 如果不存在,使用默认值

# 简洁写法:get()
value = some_dict.get('some_key', default_value) # 如果键存在,返回值;否则返回默认值

如果键不存在,get 返回 None(或指定的默认值),而 pop 会引发异常。

words = ['apple', 'bat', 'bar', 'atom', 'book'] # 创建一个字符串列表
by_letter = {}                                  # 创建一个空字典
for word in words:                             # 遍历列表中的每个单词
    letter = word[0]                          # 获取单词的首字母
    by_letter.setdefault(letter, []).append(word) # 使用 setdefault 设置默认值,并追加单词
print(by_letter)                                # 输出: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

setdefault(key, default): 如果 key 存在,则返回其值。如果不存在,则插入 key 并设置值为 default,然后返回 default

字典:defaultdict

collections.defaultdict 简化了初始化:

from collections import defaultdict # 导入 defaultdict

by_letter = defaultdict(list)      # 创建一个 defaultdict,默认值为列表
for word in words:                # 遍历列表中的每个单词
    by_letter[word[0]].append(word) # 自动创建列表并追加单词
print(by_letter)                   # 输出: defaultdict(<class 'list'>, {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']})

字典:有效的键类型(哈希性)

  • 键必须是不可变的。
  • 使用 hash() 检查:
print(hash('string'))        # 字符串可以哈希,输出一个哈希值
print(hash((1, 2, (2, 3)))) # 元组可以哈希,输出一个哈希值
# print(hash((1, 2, [2, 3])))  # 这行代码会引发 TypeError: unhashable type: 'list',因为列表不可哈希

# 列表作为键?转换为元组:
d = {}                      # 创建一个空字典
d[tuple([1, 2, 3])] = 5    # 将列表转换为元组作为键
print(d)                    # 输出: {(1, 2, 3): 5}

集合:定义和唯一性

集合是无序唯一元素集合(类似于只有键的字典)。

集合:创建

set1 = {2, 2, 2, 1, 3, 3}  # 使用 {} 创建集合
print(set1)               # 输出: {1, 2, 3} (自动去重)
set2 = set([2, 2, 2, 1, 3, 3])  # 使用 set() 创建集合
print(set2)                      # 输出: {1, 2, 3}

集合:运算(并集、交集等)

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):

c = a.copy() # 创建 a 的副本
c |= b       # 就地并集
print(c)     # 输出: {1, 2, 3, 4, 5, 6, 7, 8}

d = a.copy()  # 创建 a 的副本
d &= b        # 就地交集
print(d)      # 输出: {3, 4, 5}

Tip

对于大型集合,就地运算更有效。

集合:元素哈希性

与字典键一样,集合元素必须是不可变可哈希的。

my_data = [1, 2, 3, 4]   # 创建一个列表
# my_set = {my_data}  # 这行代码会引发 TypeError,因为列表不可哈希
my_set = {tuple(my_data)} # 将列表转换为元组
print(my_set)            # 输出: {(1, 2, 3, 4)}

集合:子集和超集

a_set = {1, 2, 3, 4, 5} # 创建一个集合
print({1, 2, 3}.issubset(a_set))   # 检查 {1, 2, 3} 是否是 a_set 的子集,输出: True
print(a_set.issuperset({1, 2, 3})) # 检查 a_set 是否是 {1, 2, 3} 的超集,输出: True

集合:相等性

如果内容相等,则集合相等:

print({1, 2, 3} == {3, 2, 1}) # 输出: True

内置序列函数:enumerate

enumerate 在迭代期间跟踪索引:

collection = ['foo', 'bar', 'baz'] # 创建一个列表

# 不使用 enumerate 的写法:
i = 0                             # 初始化索引
for value in collection:         # 遍历列表
    print(f"Index: {i}, Value: {value}") # 打印索引和值
    i += 1                        # 索引递增

# 使用 enumerate:
for i, value in enumerate(collection): # 同时获取索引和值
    print(f"Index: {i}, Value: {value}") # 打印索引和值

enumerate 返回 (index, value) 元组。

内置序列函数:sorted

sorted 返回一个新的排序列表:

print(sorted([7, 1, 2, 6, 0, 3, 2]))  # 对列表进行排序,输出: [0, 1, 2, 2, 3, 6, 7]
print(sorted('horse race'))          # 对字符串进行排序,输出: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']

Note

sorted() 返回一个列表。list.sort() 就地排序。

内置序列函数:zip

zip 将元素“配对”:

seq1 = ['foo', 'bar', 'baz']   # 创建列表 seq1
seq2 = ['one', 'two', 'three'] # 创建列表 seq2
zipped = zip(seq1, seq2)       # 将 seq1 和 seq2 配对
print(list(zipped))           # 输出: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

zip 可以接受任意数量的序列。输出长度由最短的序列决定:

seq3 = [False, True]           # 创建列表 seq3
print(list(zip(seq1, seq2, seq3))) # 输出: [('foo', 'one', False), ('bar', 'two', True)]

内置序列函数:zip (与 enumerate 结合)

使用 enumerate 迭代多个序列:

for i, (a, b) in enumerate(zip(seq1, seq2)): # 同时迭代多个序列,并获取索引
    print(f'{i}: {a}, {b}')                  # 输出: 0: foo, one \n 1: bar, two \n 2: baz, three

内置序列函数:reversed

reversed相反的顺序迭代:

print(list(reversed(range(10))))  # 反向迭代 range(10),输出: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Note

reversed 是一个生成器(在实体化之前不会创建反向序列)。

列表、集合和字典推导式

推导式可以简洁地创建新集合。

列表推导式:基本形式

# 通用形式:[expr for val in collection if condition]

# 等效的 for 循环:
result = []                # 创建一个空列表
for val in collection:     # 遍历集合中的每个元素
    if condition:          # 如果条件为真
        result.append(expr) # 将表达式的结果添加到列表中

示例:

strings = ['a', 'as', 'bat', 'car', 'dove', 'python'] # 创建一个字符串列表
# 使用列表推导式将长度大于 2 的字符串转换为大写
upper_case_long_strings = [x.upper() for x in strings if len(x) > 2]
print(upper_case_long_strings)  # 输出: ['BAT', 'CAR', 'DOVE', 'PYTHON']

集合推导式

# {expr for val in collection if condition}
strings = ['a', 'as', 'bat', 'car', 'dove', 'python'] # 创建一个字符串列表
unique_lengths = {len(x) for x in strings}   # 使用集合推导式获取列表中字符串的唯一长度
print(unique_lengths)                       # 输出: {1, 2, 3, 4, 6}

# 使用 map 函数
print(set(map(len, strings))) # 使用map函数和set达到同样效果

类似于列表推导式,但使用 {}(创建一个集合)。

字典推导式

# {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 循环中的顺序相同。

3.2 函数

函数组织和重用代码。

函数声明 (def)

def my_function(x, y):                      # 定义一个函数,名为 my_function,接受两个参数 x 和 y
    """Docstring: 解释函数的功能。"""   # 函数的文档字符串
    return x + y                            # 返回 x 和 y 的和

result = my_function(1, 2)                  # 调用函数,并将返回值赋给 result
print(result)                              # 输出: 3
print(my_function.__doc__) # 访问文档字符串, 输出: Docstring: 解释函数的功能。
  • 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
  • 位置参数:按正确的顺序传递。
  • 关键字参数:可以按任意顺序传递,通常带有默认值。
  • 关键字参数必须跟在位置参数之后。

命名空间、作用域和局部函数

  • 函数内的变量位于局部命名空间中(默认)。
  • 局部命名空间:在函数调用时创建,在函数结束时销毁。
def func():        # 定义一个函数
    a = []          # 'a' 是局部变量
    for i in range(5): # 循环 5 次
        a.append(i) # 将 i 追加到列表 a 中

func()             # 调用函数
# print(a)  # 这行代码会引发 NameError: name 'a' is not defined,因为 'a' 在函数外部不可见
  • 可以访问封闭作用域,但要修改它们,请使用 globalnonlocal
a = []  # 全局变量

def func2():          # 定义一个函数
    for i in range(5): # 循环 5 次
        a.append(i)  # 修改全局变量 'a'

func2()            # 调用函数
print(a)            # 输出: [0, 1, 2, 3, 4]

b = None # 全局变量
def bind_b_variable():  # 定义一个函数
    global b          # 声明 'b' 为全局变量
    b = []           # 将空列表赋值给全局变量 'b'
bind_b_variable()     # 调用函数
print(b)            # 输出: []

Caution

尽量减少 global 的使用。更好的设计可以减少对全局状态的依赖。

返回多个值

def f():             # 定义一个函数
    a = 5            # 局部变量
    b = 6            # 局部变量
    c = 7            # 局部变量
    return a, b, c  # 返回一个元组

x, y, z = f()        # 调用函数,并将返回的元组解包到变量 x, y, z 中
print(x, y, z)      # 输出: 5 6 7

# 或者返回一个字典
def f2():
  a = 5
  b = 6
  c = 7
  return {'a' : a, 'b' : b, 'c' : c} # 返回值改为字典

result = f2()
print(result) # 输出: {'a': 5, 'b': 6, 'c': 7}

函数是对象

函数是一等公民:

  • 可以作为参数传递。
  • 可以赋值给变量。
  • 可以从其他函数返回。
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)

这种方式很灵活,而且可以重用。

匿名 (Lambda) 函数

# 使用 `lambda` 定义小型匿名函数:

# 等效于:
# def short_function(x):
#     return x * 2

equiv_anon = lambda x: x * 2  # 定义一个 lambda 函数,接受一个参数 x,返回 x * 2
print(equiv_anon(4))          # 输出: 8

用于传递短函数很方便:

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 模块

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 对连续的元素进行分组。

有用的 itertools 函数

函数 描述
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 块

def attempt_float(x):  # 定义一个函数,尝试将输入转换为浮点数
    try:               # 尝试执行以下代码块
        return float(x) # 将 x 转换为浮点数并返回
    except:            # 如果发生任何异常,则执行以下代码块
        return x       # 返回原始输入

print(attempt_float('1.2345'))    # 输出: 1.2345
print(attempt_float('something')) # 输出: something
  • try: 可能引发异常的代码。
  • except: 如果发生异常,则执行。

捕获特定异常

def attempt_float2(x):         # 定义一个函数,尝试将输入转换为浮点数
    try:                      # 尝试执行以下代码块
        return float(x)        # 将 x 转换为浮点数并返回
    except ValueError:       # 如果发生 ValueError 异常,则执行以下代码块
        return x              # 返回原始输入

# print(attempt_float2((1, 2)))  # 这行代码会引发 TypeError,因为没有捕获 TypeError 异常

指定异常类型(首选)。

捕获多个异常类型

def attempt_float3(x):                 # 定义一个函数,尝试将输入转换为浮点数
    try:                              # 尝试执行以下代码块
        return float(x)                # 将 x 转换为浮点数并返回
    except (TypeError, ValueError):  # 如果发生 TypeError 或 ValueError 异常,则执行以下代码块
        return x                      # 返回原始输入

使用元组捕获多种类型的异常。

finally 和 else

  • finally: 始终执行(清理)。
  • else: 如果 try没有引发异常,则执行。
f = open("tempfile.txt", mode = "w") # 创建一个临时文件
try:
    # write_to_file(f) # 假设 write_to_file 是一个可能出错的函数
    f.write("example text")  # 写入
finally:
    f.close()  # 无论是否发生异常,都关闭文件
    import os
    os.remove("tempfile.txt") # 删除临时文件
f = open("tempfile.txt", mode = "w") # 创建一个临时文件
try:
    #write_to_file(f) # 假设 write_to_file 是一个可能出错的函数
    f.write("example text") #写入
except:
    print('Failed') # 捕获异常时输出
else:
    print('Succeeded') # 没有捕获到异常时输出
finally:
    f.close() # 无论是否有异常,都关闭文件
    import os
    os.remove("tempfile.txt") # 删除临时文件

IPython 中的异常

IPython 提供了有用的回溯信息。使用 %xmode 控制详细程度(Plain、Context、Verbose)。

3.3 文件和操作系统

打开文件 (open)

path = 'examples/segismundo.txt' # 设置文件路径(请确保此文件存在)

# 默认编码因平台而异,最好明确指定。
f = open(path, encoding='utf-8')  # 以读取模式打开文件('r' 是默认模式),指定编码为 utf-8
for line in f:                   # 逐行读取文件
    print(line.rstrip())         # 移除每行末尾的空白字符并打印

f.close()                        # 关闭文件
  • open(path, mode='r', encoding=None)
    • path: 文件路径。
    • mode: ‘r’ (读取), ‘w’ (写入), ‘a’ (追加), ‘x’ (创建), ‘rb’, ‘wb’ 等。
    • encoding: 文件编码 (例如, ‘utf-8’)。
  • 关闭文件:f.close()

从文件中读取

# 读取所有行
f = open(path, encoding="utf-8")      # 以读取模式打开文件,指定编码为 utf-8
lines = [x.rstrip() for x in f]      # 读取所有行,移除每行末尾的空白字符,并存储在列表中
print(lines)                           # 打印所有行
f.close()                            # 关闭文件

使用 with 语句自动关闭文件

with 语句会自动关闭文件:

with open(path, encoding="utf-8") as f: # 使用 with 语句打开文件,指定编码为 utf-8
    lines = [x.rstrip() for x in f]  # 读取所有行,移除每行末尾的空白字符,并存储在列表中
print(lines)                         # 打印所有行
# 'f' 在这里自动关闭

Tip

使用 with!即使出现错误,它也能确保清理。

文件模式

模式 描述
r 只读
w 只写;创建新文件(擦除现有文件)
x 只写;创建新文件
a 追加到现有文件(如果需要,创建新文件)
r+ 读写
b 二进制模式(添加到模式:‘rb’, ‘wb’)
t 文本模式(解码字节);默认

read, seek 和 tell

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)。

字节和 Unicode 与文件

  • 默认:文本模式(字符串/Unicode)。
  • 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(不完整的多字节字符)
  • UTF-8:可变长度编码。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 时要非常小心。

总结

本章涵盖了:

  • 数据结构:元组、列表、字典、集合。
  • 内置函数enumeratesortedzipreversed
  • 推导式:简洁的集合创建。
  • 函数:定义、参数、命名空间、返回值、lambda 函数。
  • 生成器:节省内存的迭代。
  • itertools:有用的迭代器工具。
  • 错误处理tryexceptfinallyelse
  • 文件处理:打开、读取、写入、模式、编码。

这些是 Python 数据分析的基础!🧱

思考和讨论 🤔

  1. 大型 CSV 文件: 你有一个大于 RAM 的 CSV 文件。你将如何使用生成器和文件处理来有效地处理它?
  2. 列表与元组: 权衡?何时选择?示例?
  3. 列表推导式与生成器表达式: 区别?何时更喜欢生成器表达式?
  4. 异常处理: 为什么重要?在数据分析上下文中的示例?
  5. with 语句: 文件处理的优势?
  6. 文本模式与二进制模式: 区别?编码的重要性?
  7. 字典 vs 集合: 如何决定使用哪个?