Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(附录 C 高级 Python 概念)

Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(附录 C 高级 Python 概念) Excel Python飞速搞定数据分析与处理附录 C 高级 Python 概念本附录会更细致地研究以下 3 个主题类和对象、带时区的 datetime 对象以及可变与不可变对象。这些主题相互独立可以以任意顺序阅读。C.1 类和对象本节我们会编写自己的类以便更好地理解类和对象之间的关系。类定义了一类新的对象一个类就像是你用来烤蛋糕比如巧克力蛋糕或者起司蛋糕的模具。用模具 类制作蛋糕对象的过程就叫作实例化。这就是为什么对象也被称为类实例class instance。无论是巧克力蛋糕还是起司蛋糕它们都是一类type蛋糕类class可 以让你定义新的数据类型这些类型将数据属性和函数方法放在了一起因而可以帮助你架构和组织代码。现在回到第 3 章中赛车游戏的例子定义我们自己的类In [1]: class Car: def __init__(self, color, speed0): self.color color self.speed speed def accelerate(self, mph): self.speed mph这是一个简单的汽车类其中包含了两个方法。方法是在类定义中定义的函数该类有一个叫作 accelerate 的普通方法。该方法会更改类实例的数据speed。它还有一个叫作__init__的特殊方法方法名首尾有两个下划线。当对象被实例化之后Python 会调用这个方法为对象附加一些初始数据。每个方法的第一个参数表示的是类实例依照惯例会被命名为 self。在看到如何使用 Car 类之后你就会明白其中的道理。我们首先来实例化两辆汽车。这个过程和调用函数是一样的在类名后面加上圆括号以调用类同时提供__init__方法的参数。你不需要为 self 提供实参因为 Python 会负责这项工作。在本例中self 分别是 ZX 和 YAMAHAIn [2]: # 来实例化两个汽车对象 ZX Car(red) YAMAHA Car(colorblue)当你调用一个类时实际上调用的是__init__函数这就是为什么所有对于函数参数有效的东西也可以应用到这里对于 ZX我们以位置参数形式提供实参而对于 YAMAHA我们使用的是关键字参数形式。从 Car 类实例化两个机车对象之后来看一下它们的属性并调用其方法。我们会看到在加速 ZX 之后ZX 的速度会发生变化YAMAHA 则保持不变 原因是两个对象是相互独立的In [3]: # 在默认情况下会打印出对象的内存位置 ZX Out[3]: __main__.Car at 0x196e7d90550 In [4]: # 通过属性可以访问对象的数据 ZX.color Out[4]: red In [5]: ZX.speed Out[5]: 0 In [6]: # 在 ZX 上调用 accelerate 方法 ZX.accelerate(20) In [7]: # ZX 的 speed 属性发生了改变 ZX.speed Out[7]: 20 In [8]: # YAMAHA 的 speed 属性保持不变 YAMAHA.speed Out[8]: 0Python 也允许直接修改属性而无须使用方法In [9]: ZX.color green In [10]: ZX.color Out[10]: green In [11]: YAMAHA.color # 不变 Out[11]: blue总结一下类定义了对象的属性和方法。类将函数“方法”和数据“属性”组合到一起从而使你可以方便地利用点语法访问myobject.attribute 或 myobject.method()。C.2 使用带时区的 datetime 对象在第 3 章简单介绍过不带时区的 datetime 对象。如果需要考虑时区那么你通常都会在 UTC 时区下进行工作只有在显示时间时才将其转换为当地时区。在使用 Excel 和 Python 时你可能想要将 Excel 产生的不带时区的时间戳转换为带时区的 datetime 对象。对于 Python 中的时区支持你可以使用 dateutil 包虽然它不是标准库的一部分但是已经在 Anaconda 中预装。下面的示例展示了在处理 datetime 和时区时的一些常见操作。In [12]: import datetime as dt from dateutil import tz In [13]: # 不带时区的 datetime 对象 timestamp dt.datetime(2020, 1, 31, 14, 30) timestamp.isoformat() Out[13]: 2020-01-31T14:30:00 In [14]: # 带时区的 datetime 对象 timestamp_eastern dt.datetime(2020, 1, 31, 14, 30, tzinfotz.gettz(US/Eastern)) # 以isoformat格式打印可以更清楚地看出和UTC的差距 timestamp_eastern.isoformat() Out[14]: 2020-01-31T14:30:00-05:00 In [15]: # 为不带时区的datetime对象赋予时区 timestamp_eastern timestamp.replace(tzinfotz.gettz(US/Eastern)) timestamp_eastern.isoformat() Out[15]: 2020-01-31T14:30:00-05:00 In [16]: # 转换时区 # 由于 UTC 时区很常用因此可以简写为 tz.UTC timestamp_utc timestamp_eastern.astimezone(tz.UTC) timestamp_utc.isoformat() Out[16]: 2020-01-31T19:30:0000:00 In [17]: # 带时区转换为不带时区 timestamp_eastern.replace(tzinfoNone) Out[17]: datetime.datetime(2020, 1, 31, 14, 30) In [18]: # 不带时区的当前时间 dt.datetime.now() Out[18]: datetime.datetime(2021, 1, 3, 11, 18, 37, 172170) In [19]: # UTC时区中的当前时间 dt.datetime.now(tz.UTC) Out[19]: datetime.datetime(2021, 1, 3, 10, 18, 37, 176299, tzinfotzutc())提示Python 3.9 中的时区处理Python 3.9 通过 timezone 模块为标准库添加了对时区的完全支持。可以用它来代替 dateutil 的 tz.gettz 调用。from zoneinfo import ZoneInfo timestamp_eastern dt.datetime(2020, 1, 31, 14, 30, tzinfoZoneInfo(US/Eastern))C.3 可变和不可变的 Python 对象在 Python 中可以修改其值的对象称为可变的mutable而不能修改的就称为不可变的 immutable。表 C-1 展示了各个数据类型属于哪一类。了解两者之间的差别是很重要的因为可变对象可能和你在其他语言包括 VBA中习以为常的东西有不一样的行为。来看看下面这段 VBA 代码Dim a As Variant, b As Variant a Array(1, 2, 3) b a a(1) 22 Debug.Print a(0) , a(1) , a(2) Debug.Print b(0) , b(1) , b(2)打印的结果如下1, 22, 3 1, 2, 3现在在 Python 中用列表完成同样的操作In [20]: a [1, 2, 3] b a a[1] 22 print(a) print(b) [1, 22, 3] [1, 22, 3]在 Python 中变量是你 “赋予” 对象的名称。b a 将两个名称赋予了同一个对象即 list[1, 2, 3]。因此所有指向该对象的变量都会体现出列表的变化。不过这只对可变对象有用如果你将列表替换成不可变对象比如元组那么修改 a 并不会影响 b。如果想让可变对象 b 不受 a 的改变的影响则必须显式地复制列表In [21]: a [1, 2, 3] b a.copy() In [22]: a Out[22]: [1, 2, 3] In [23]: b Out[23]: [1, 2, 3] In [24]: a[1] 22 # 修改a…… In [25]: a Out[25]: [1, 22, 3] In [26]: b # ……不影响b Out[26]: [1, 2, 3]列表的copy 方法创建的是一份浅复制shallow copy你确实会得到一份列表的副本 但是如果列表中包含可变元素那么这些元素仍然是共享的。如果你想递归复制所有的元素则需要利用标准库中的 copy 模块来进行深复制deep copyIn [27]: import copy b copy.deepcopy(a)C.3.1 以可变对象为参数调用函数如果你是从 VBA 转到 Python 的那么可能已经习惯于将函数参数标记为按引用传递 ByRef或按值传递ByVal当你将一个变量作为参数传递给函数时函数要么拥有改变这个变量的能力ByRef要么就是在处理值的副本ByVal因此原变量不会发生变化。VBA 中默认按引用传递参数ByRef。考虑如下 VBA 函数Function increment(ByRef x As Integer) As Integer x x 1 increment x End Function然后像下面这样调用这个函数Sub call_increment() Dim a As Integer a 1 Debug.Print increment(a) Debug.Print a End Sub上述代码会打印如下内容2 2然而如果你将 increment 函数中的 ByRef 改成 ByVal则会打印出如下内容2 1在 Python 中又是怎样呢当你把变量四处传递时实际上传递的是指向对象的名称。也就是说具体行为取决于对象是可变的还是不可变的。先使用一个不可变对象来进行测试In [28]: def increment(x): x x 1 return x In [29]: a 1 print(increment(a)) print(a) 2 1然后使用可变对象重复上面的例子In [28]: def increment1(x): x[0] x[0] 1 return x In [29]: a [1] print(increment1(a)) print(a) [2] [2]如果对象是可变的而你想要原对象保持不变那么就需要传递对象的副本In [32]: a [1] print(increment1(a.copy())) print(a) [2] [1]还有一种情况值得注意那就是定义函数时默认参数中对可变对象的使用。下面来看看为什么值得注意。C.3.2 使用可变对象作为默认参数的函数在编写函数时一般来说不应该使用可变对象作为默认参数。这是因为默认参数的值是函数定义的一部分它只会被求值一次而不会在每次调用函数时求值。因此使用可变对象作为默认参数会导致出人意料的行为In [33]: # 不要这么做 def add_one(x[]): # 这里的 x[] 是赋值给了一个列表而列表是可变量。 x.append(1) return x In [34]: add_one() Out[34]: [1] In [35]: add_one() Out[35]: [1, 1]如果你想将空列表作为默认参数则应该像下面这样做。In [36]: def add_one1(xNone): if x is None: x [] x.append(1) return x In [37]: add_one1() Out[37]: [1] In [38]: add_one1() Out[38]: [1]