Python数据处理
¶

03. 文件,类和函数式编程
¶

主讲人:丁平尖

持久化数据¶

  • 到目前为止,我们只知道如何编写“临时”程序,

    • 程序停止运行后数据就会消失。
  • 文件允许数据持久化,

    • 可以将程序的工作保存到磁盘上,
    • 以便之后再次使用。
  • 持久程序的例子

    • 操作系统
    • 数据库
    • 服务器

读文件¶

  • 使用open()函数打开文件,并通过readline()方法逐行读取文件内容。
In [1]:
f = open('baseball.csv')
f.readline()
Out[1]:
'"id","year","stint","team","lg","g","ab","r","h","X2b","X3b","hr","rbi","sb","cs","bb","so","ibb","hbp","sh","sf","gidp"\n'
In [2]:
f.readline()
print(f.closed)
False

读文件¶

  • with关键字用于处理文件时,可以自动进行错误检查和清理。
    • 自动关闭资源:当 with 语句结束时,Python 会自动关闭资源,如文件、连接等。
    • 处理异常:如果在 代码块 中发生异常,Python 会自动调用上下文管理器的 exit() 方法,确保资源被正确关闭。
In [6]:
with open('baseball.csv') as f:
    lines = f.readlines()
    for line in lines:
        #print(line)
        pass
print(f.closed)
True

写文件¶

  • 以写入模式打开文件,如果文件已存在,则会创建新文件并删除旧内容。写入文件时,\n代表新行,并且每次写入操作都会追加到文件末尾。
In [130]:
with open('demo.txt', 'w') as f:
    f.write('cat\n')
    f.write('dog\n')
print(f.closed)
True

字符串格式化¶

  • Python提供了格式化字符串的工具,例如使用%d、%s、%f来格式化整数、字符串和浮点数。
In [131]:
# 使用操作符 “%” 格式化字符串
name = "John"
age = 30
print("My name is %s and I am %d years old." % (name, age))
My name is John and I am 30 years old.
In [132]:
# 使用str.format()方法格式化字符串
pi = 3.14159
print("The value of pi is {:.2f}".format(pi))
The value of pi is 3.14

对象持久化:pickle¶

  • pickle.dumps()可以将对象转换为二进制字符串,这个字符串可以存储到文件中以供后续使用。
In [133]:
import pickle
t1 = [1, 'two', 3.0]
s = pickle.dumps(t1)
print(s)
with open('demo.pkl', 'wb') as f:
    f.write(s)

with open('demo2.pkl', 'wb') as f:
    pickle.dump(t1, f)
b'\x80\x04\x95\x16\x00\x00\x00\x00\x00\x00\x00]\x94(K\x01\x8c\x03two\x94G@\x08\x00\x00\x00\x00\x00\x00e.'
  • pickle.loads()可以将这个字符串转换回对象。
In [134]:
t2 = pickle.loads(s)
t1 == t2, t2 is t1
Out[134]:
(True, False)

定位文件:os模块¶

  • os模块允许我们与操作系统交互,例如获取当前工作目录、列出目录内容、更改工作目录等。路径可以是绝对路径或相对路径。
In [135]:
import os
os.getcwd(), os.path.exists('demo.pkl')
Out[135]:
('/mnt/c/Users/ASUS/Desktop/PPT', True)

错误处理:try/catch语句¶

  • 使用try/except语句可以在发生错误时尝试恢复,而不是直接失败。
    • 例如,尝试打开一个文件,如果失败(例如文件不存在),则在其他地方寻找文件。
In [136]:
import os
try:
    t = pickle.load(open('demo.pkl', 'rb'))
    print(t)
except:
    print('File not found')
[1, 'two', 3.0]

错误处理:try/catch语句¶

In [137]:
import math
def my_sqrt(x):
    try:
        return math.sqrt(x)
    except TypeError:
        print('x is not a number')
        return None
    except ValueError:
        print('x is not a positive number')
        return None
    except:
        print('Something else went wrong')
        return None
x = my_sqrt('cat')
print(x)
x is not a number
None

编写模块¶

  • 我们可以编写自己的模块,并使用相同的语法导入它们。
In [138]:
import os
print(os.path.isfile('prime.py'))
True
In [139]:
import subprocess
cmd = 'cat prime.py'
subprocess.run(cmd, shell=True)
import math

def is_prime(n):
    if n <= 1:
        return False
    elif n == 2:
        return True
    else:
        ulim = math.ceil(math.sqrt(n))
        for k in range(2, ulim + 1):
            if n % k == 0:
                return False
        return True
Out[139]:
CompletedProcess(args='cat prime.py', returncode=0)

编写模块¶

In [140]:
import prime
prime.is_prime(1)
Out[140]:
False
In [141]:
from prime import is_prime
is_prime(2)
Out[141]:
True

类:程序员定义的类型¶

  • 有时我们使用一组变量来表示一个特定的对象。类可以将数据类型封装起来,例如表示二维空间中的点。
    • 定义一个类后,我们可以创建该类型的新变量。
In [142]:
class Point:
    '''Represent a 2-d point.'''
    pass

p = Point()
p
Out[142]:
<__main__.Point at 0x7faf8c12d030>
In [143]:
p.x = 3.0
p.y = 4.0
print(p.x)
3.0

嵌套¶

  • 一个对象作为另一个对象的属性存在,叫做嵌套。
In [144]:
class Rectangle:
    '''Represent a rectangle.
    attribute: width, height, corner
    '''

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

复制¶

  • 别名的使用会让程序变得复杂, 因为一处改动可能会影响到其他地方.
In [145]:
import copy
q1 = copy.copy(p)
q1, p
Out[145]:
(<__main__.Point at 0x7faf8c12fd90>, <__main__.Point at 0x7faf8c12d030>)

复制¶

  • 浅拷贝:仅仅复制对象和其内部引用,而不会复制内嵌对象;
  • 深拷贝:不仅复制对象,同时也会复制其引用的对象,以及其引用对象内部引用的对象等等。
In [146]:
print(box, box.corner)
box2 = copy.copy(box)
print(box2, box2.corner)
box3 = copy.deepcopy(box)
print(box3, box3.corner)
<__main__.Rectangle object at 0x7faf8c12ef80> <__main__.Point object at 0x7faf8c12f400>
<__main__.Rectangle object at 0x7faf8c12dde0> <__main__.Point object at 0x7faf8c12f400>
<__main__.Rectangle object at 0x7faf8c12e530> <__main__.Point object at 0x7faf8c12dd80>

纯函数和修改器¶

  • 纯函数返回一个对象,并且不修改其任何参数。
  • 修改器函数会修改其获取的参数对象。
In [147]:
# 纯函数
def double_sides(r):
    rdouble = Rectangle()
    rdouble.corner = r.corner
    rdouble.height = 2.0 * r.height
    rdouble.width = 2.0 * r.width
    return(rdouble)

# 修改器
def shift_rectangle(rec, dx, dy):
    rec.corner.x = rec.corner.x + dx
    rec.corner.y = rec.corner.y + dy

方法¶

  • 方法是一种特殊类型的函数,它由对象提供。每个方法必须包含self作为其第一个参数。
In [148]:
class Time:    
    def print_time(self):
        print(self.hour, ':', self.minute, ':', self.second)

t = Time()
t.hour, t.minute, t.second = 11, 59, 30
t.print_time()
11 : 59 : 30

创建对象:__init__方法¶

  • __init__是一个特殊方法,在我们实例化对象时被调用。如果向__init__提供少于三个参数,它将默认填充其余参数。
In [149]:
class Time:
    def __init__(self, h=None, m=None, s=None):
        self.hour = h
        self.minute = m
        self.second = s

    def print_time(self):
        print(self.hour, ':', self.minute, ':', self.second)

t = Time(11, 59, 30)
t.print_time()
11 : 59 : 30

继承¶

In [150]:
class Card:
    """"Represents a playing card."""
    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank
 
    # 类属性
    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King']
    # __str__ 是 Python 中的一个特殊方法,用于返回对象的字符串表示形式。
    # 当你使用 print() 函数或 str() 函数来打印一个对象时,Python 会自动调用该对象的 __str__ 方法来获取其字符串表示形式
    def __str__(self):
        return '%s of %s' % (self.rank_names[self.rank], self.suit_names[self.suit])
    # 在Python中,__lt__是特殊方法之一,用于定义对象之间的比较行为。具体来说,__lt__方法用于实现小于(less than)比较运算符<。
    # 当你使用 print() 函数或 str() 函数来打印一个对象时,Python 会自动调用该对象的 __str__ 方法来获取其字符串表示形式。
    def __lt__(self, other):
        if self.suit < other.suit: return True
        if self.suit > other.suit: return False
        return self.rank < other.rank
card1 = Card(2, 11)
card2 = Card(3, 10)
print(card1, card1<card2)
Jack of Hearts True
In [151]:
# 整幅牌
class Deck:
    def __init__(self):
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)

    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)
    
    def pop_card(self):
        return self.cards.pop()
    
    def add_card(self, card):
        self.cards.append(card)
    
    def shuffle(self):
        import random
        random.shuffle(self.cards)

    def move_card(self, hand, num):
        for i in range(num):
            hand.add_card(self.pop_card())

继承¶

  • 继承是一种基于已有类,进行修改,从而定义新的类的能力。
  • 我们想要一个类来表示 ‘手牌’, 即玩家手中持有的牌. 一副手牌和整副牌相差无几: 都是由卡牌构成, 并且都需要支持添加和移除牌等操作.
In [152]:
class Hand(Deck):
    """Represents a hand of playing cards."""
    def __init__(self, label=''):
        self.cards = []
        self.label = label

    def __str__(self):
        res = []
        res.append(self.label)
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)
    
    

函数式编程¶

  • 面向对象编程的思想
    • 一切都是对象
    • 每个操作都是某个类/对象的责任
    • 使用副作用(例如,修改属性)为我们带来好处
  • 在函数式编程中,函数是中心概念,而不是对象
    • 一切都是函数, 数据是不可变的
    • 尽量避免副作用
    • 使用纯函数
      • 相同输入始终返回相同输出:对于相同的输入参数,纯函数总是返回相同的结果。这意味着函数的输出仅依赖于其输入,而与外部状态无关。 * HTTP协议:HTTP 是一个无状态协议,每个请求(例如网页请求)都与之前的请求无关。
      • 没有副作用:纯函数在执行过程中不会对外部状态产生任何影响。这包括不修改全局变量、不改变传入对象的状态、不进行 I/O 操作(如文件读写或打印输出)等。
    • 允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

面向对象编程和函数式编程¶

  • 函数式编程适用于计算逻辑较复杂且需要高并发的场景;

  • 而面向对象编程更适合建模现实世界的复杂系统

  • Python是一种多范式编程语言

  • Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。(尽管许多函数式编程语言(如Haskell、Lisp)可以使用变量,但它们倾向于使用不可变数据结构和函数,避免改变变量的值。)

迭代器¶

  • 函数式编程强调函数的组合性,迭代器支持对数据流的逐个处理。

  • 迭代器 (Iterator):

    • 迭代器是一个对象,它实现了两个方法:iter() 和 next()。iter() 返回自身,next() 返回序列中的下一个元素,并在元素耗尽时抛出 StopIteration 异常。
    • 迭代器的特点是:元素只能被遍历一次,且它们不会存储所有的元素,而是按需逐个生成。
  • 可迭代对象 (Iterable):

    • 一个可迭代对象是实现了 iter() 方法的对象,该方法返回一个迭代器。例如:list、tuple、str、dict 等 Python 内置容器类型都是可迭代对象。
    • 可迭代对象可以被 for 循环遍历,但它本身不是迭代器,不能直接调用 next() 方法。
In [153]:
t = [1, 2]
titer = iter(t) # 列表不是迭代器,所以我们首先必须使用函数iter()将列表t转换为迭代器。
next(titer), next(titer)
Out[153]:
(1, 2)
In [154]:
next(titer)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
/tmp/ipykernel_95373/302089432.py in <module>
----> 1 next(titer)

StopIteration: 
In [155]:
t = [1,2]
for x in t:
    print(x)
for x in iter(t):
    print(x)
# 这两个for循环是等效的。第一个隐藏了iter()的调用,而在第二个中,我们正在做Python本来会为我们做的工作,通过将t转换为迭代器。
1
2
1
2

生成器¶

  • 生成器是通过生成器函数或生成器表达式来创建的。
  • 生成器函数与普通函数的区别在于,它使用 yield 而不是 return 来返回值。每次调用生成器的 next() 方法时,函数会从上一次暂停的地方继续执行。
In [156]:
def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()

print(next(gen))  # 输出 1
print(next(gen))  # 输出 2
print(next(gen))  # 输出 3
# 再次调用 next 会引发 StopIteration 异常
print(next(gen))
1
2
3
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
/tmp/ipykernel_95373/1948705562.py in <module>
     10 print(next(gen))  # 输出 3
     11 # 再次调用 next 会引发 StopIteration 异常
---> 12 print(next(gen))

StopIteration: 
  • 生成器表达式与列表推导式类似,但生成器表达式使用圆括号 (),而列表推导式使用方括号 []。生成器表达式是按需生成元素的,不会一次性生成所有元素。
In [157]:
gen_exp = (x * x for x in range(5))  # 生成一个生成器

print(next(gen_exp))  # 输出 0
print(next(gen_exp))  # 输出 1
print(next(gen_exp))  # 输出 4
0
1
4

高阶函数:map, reduce, and filter¶

map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

In [158]:
def f(x):
    return x * x

r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
list(r)
Out[158]:
[1, 4, 9, 16, 25, 36, 49, 64, 81]

reduce¶

再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

In [159]:
from functools import reduce
def fn(x, y):
    return x * 10 + y

reduce(fn, [1, 3, 5, 7, 9])
Out[159]:
13579

lambda¶

  • lambda函数是一个可以传递给其他函数或作为参数的匿名函数。
    • lambda 参数1, 参数2, ... : 表达式
      • 匿名:lambda函数不需要名字,可以直接使用。
      • 简洁:适合定义简单的函数,只能包含一个表达式,不能包含多条语句。
      • 返回值:lambda函数会自动返回表达式的值。
In [160]:
list(map(lambda x: x * x, [1, 2, 3]))
Out[160]:
[1, 4, 9]

filter¶

  • filter()也接收一个函数和一个序列
In [161]:
list(filter(lambda x: x % 2 == 1, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
Out[161]:
[1, 3, 5, 7, 9]