数据整形(结构化)手术刀——Python语言精要。
参考:《利用Python进行数据分析》
Contents:
Python为一种解释型语言,Python解释器通过“一次执行一条语句”的方式运行程序,可以直接在终端用python
启动标准交互式Python解释器,如果要运行Python程序,只需调用python并将.py
文件作为其第一个参数即可。
通常Python科学计算程序员更趋向于使用IPython(一种加强的交互式Python解释器)。通过使用%run
命令,IPython会在同一个进程中执行指定文件中的代码,在代码执行完毕后,就可以通过交互的方式研究其结果。可以用exit
退出IPython。
Python语言的设计特点是重视可读性、简洁性及明确性。
Python是通过空白符(制表符或空格)来组织代码的,不像其它语言(如R、C++、Java、Perl等)用的是大括号。
以for循环为例,实现快速排序算法:
for x in array:
if x < pivot:
less.append(x)
else:
greater.append(x)
冒号表示一段缩进代码块的开始,其后的所有代码都必须缩进相同的量,直到代码块结束。在其它语言中则可能是:
for x in array{
if x < pivot{
less.append(x)
} else{
greater.append(x)
}
}
使用空白符的主要好处是,他能使大部分Python代码在外观上看起来差不多。在阅读某段别人写的(或者自己很久之前写的)代码时不容易出现“认知失调”。而那些空白符无实际意义的语言中,则可能出现格式不统一的代码,例如:
for x in array
{
if x < pivot
{
less.append(x)
}
else
{
greater.append(x)
}
}
使用4个空格作为默认缩进量的好处在于,这样编辑器可以将制表符替换为4个空格。
分号——Python中的语句不以分号结束,但是分号可以用来在一行上分隔多条语句:
a = 5; b = 6; c = 7
但是Python中通常不推荐在一行中放置多条语句,因为这样往往会使代码可读性变差。
Python语言的一个重要特点就是其对象模型的一致性。Python解释器中的任何数值、字符串、数据结构、函数、类、模块等都待在它们自己的“盒子”里,这个“盒子”就是Python对象。
每一个对象都有一个与之相关的类型(比如字符串或函数)以及内部数据。在实际工作中,这使得Python语言变得非常灵活,因为即使是函数也能被当做其它对象那样处理。
用#
:
a = 1 # 这里是注释
注释代码块: Python中没有块注释,要么每行前面加#
,要么用"""
和"""
将代码块当做字符串,或者用if 0:
(这两个主要在测试的时候用用)。
中文编码: 代码首行加:
# -- coding: utf-8 --
好像有的说要在第二行加,或者是别的形式的,不过这个我每次用,活得好好的。
函数调用需要用到圆括号以及0个或多个参数,此外还可以将返回值赋值给一个变量:
result = f(x, y, z)
g()
几乎所有的Python对象都有一些附属函数(也就是方法),它们可以访问该对象的内部数据。方法的调用如下:
obj.some_method(x, y, z)
函数既可以接受位置参数,也可以接受关键字参数:
result = f(a, b, c, d=5, e='foo')
Python中对变量赋值时,其实是在创建等号右侧对象的一个引用。例如,看下面这个整数列表:
In [1]: a = [1, 2, 3]
假如将a
赋值给一个新变量b
:
In [2]: b = a
在某些语言中,该赋值过程会导致数据[1, 2, 3]
被复制。而在Python中,a
和b
现在都指向同一个对象,即原始列表[1, 2, 3]
。可以自己验证一下:对a
添加一个元素,然后查看b
的情况:
In [3]: a.append(4)
In [4]: b
Out[4]: [1, 2, 3, 4]
理解Python 引用的语义以及数据复制的条件、方式、原因等知识对于在Python中处理大数据集非常重要。
注:赋值(assignment)操作也叫做绑定(binding),因为我们其实是将一个名称和一个对象绑定到一起。已经赋过值的变量名有时也被称为已绑定变量(bound variable)。
当你将对象以参数的形式传入函数时,其实只是传入了一个引用而已,不会发生任何复制。因此,Python被称为是按引用传递的,而其他的语言既支持按值传递(创建副本)又支持按引用传递。也就是说,Python函数可以修改其参数的内容。假设我们有下面这样一个函数:
def append_element(some_list, element):
some_list.append(element)
根据刚才所说,则下面的结果应该是在意料之中的:
In [5]: def append_element(some_list, element):
...: some_list.append(element)
...:
In [6]: data = [1, 2, 3]
In [7]: append_element(data, 4)
In [8]: data
Out[8]: [1, 2, 3, 4]
和许多编译型语言(如Java和C++)相反,Python中的对象引用没有与之关联的类型信息。下面这些代码不会有什么问题:
In [9]: a = 5
In [10]: type(a)
Out[10]: int
In [11]: a = 'foo'
In [12]: type(a)
Out[12]: str
变量其实就是对象在特定命名空间中的名称而已。对象的类型信息是保存在它自己内部的。有人可能会轻率地认为Python不是一种“类型化语言”。其实不是这样的。例如:
In [13]: '5' + 5
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
13-f9dbf5f0b234> in ()
----> 1 '5' + 5
TypeError: cannot concatenate 'str' and 'int' objects
在有些语言中(比如Visual Basic),字符串'5'
可能会被隐式地转换为整数,于是就会得到10。而在另一些语言中(比如JavaScript),整数5可能会被转换为字符串,于是就会得到'55'
。而在这一点上,Python可以被认为是一种强类型语言,也就是说,所有对象都有一个特定的类型(或类),隐式转换只在很明显的情况下才会发生,比如下面这样:
In [14]: a = 4.5
In [15]: b = 2
# %s表示将参数格式化为字符串
In [16]: print 'a is %s, b is %s' % (type(a), type(b))
a is 'float'>, b is 'int'>
In [17]: a / b
Out[17]: 2.25
了解对象的类型是很重要的。要想编写能够处理多个不同类型输入的函数就必须了解有关类型的知识。通过isinstance
函数,你可以检查一个对象是否是某个特定类型的实例:
In [18]: a = 5
In [19]: isinstance(a, int)
Out[19]: True
isinstance
可以接受由类型组成的元组。如果想检查某个对象的类型是否属于元组中所指定的那些:
In [20]: a = 5; b = 4.5
In [21]: isinstance(a, (int, float))
Out[21]: True
In [22]: isinstance(b, (int, float))
Out[22]: True
Python中的对象通常都既有属性(attribute,即存储在该对象“内部”的其他Python对象)又有方法(method,与该对象有关的能够访问其内部数据的函数)。它们都能通过obj.attribute_name
这样的语法进行访问:
In [23]: a = 'foo'
In [24]: a.
a.capitalize a.format a.isupper a.rindex a.strip
a.center a.index a.join a.rjust a.swapcase
a.count a.isalnum a.ljust a.rpartition a.title
a.decode a.isalpha a.lower a.rsplit a.translate
a.encode a.isdigit a.lstrip a.rstrip a.upper
a.endswith a.islower a.partition a.split a.zfill
a.expandtabs a.isspace a.replace a.splitlines
a.find a.istitle a.rfind a.startswith
属性和方法还可以利用getattr
函数通过名称进行访问:
In [24]: getattr(a, 'split')
Out[24]:
与之相关的还有hasattr
和stator
函数,它们在编写通用的、可复用的代码时都很实用。
一般来说,你可能不会关心对象的类型,而只是想知道它到底有没有某些方法或行为。比如说,只要一个对象实现了迭代器协议(iterator protocol),你就可以确认它是可迭代的。对于大部分对象而已,这就意味着它拥有一个__iter__
魔术方法。当然,还有一个更好一些的验证办法,即尝试使用iter
函数:
In [25]: def isiterable(obj):
....: try:
....: iter(obj)
....: return True
....: except TypeError:
....: return False
....:
对于字符串以及大部分Python集合类型,该函数会返回True
:
In [26]: isiterable('a string')
Out[26]: True
In [27]: isiterable([1, 2, 3])
Out[27]: True
In [28]: isiterable(5)
Out[28]: False
在编写需要处理多类型输入的函数时可能会用到这个功能。还有一种常见的应用场景:编写可以接受任何序列(列表、元组、ndarray)或迭代器的函数。你可以先检查对象是不是列表(或NumPy数组),如果不是,就将其转换成是:
if not isinstance(x, list) and isiterable(x):
x = list(x)
在Python中,模块(module)就是一个含有函数和变量定义以及从其他.py
文件引入的此类东西的.py
文件。假设我们有下面这样的一个模块:
# some_module.py
PI = 3.14159
def f(x):
return x + 2
def g(a, b):
return a + b
如果想要引入some_module.py
中定义的变量和函数,我们可以在同一个目录下创建另一个文件:
import some_module
result = some_module.f(5)
pi = some_module.PI
还可以写成这样:
from some_module import f, g, PI
result = g(5, PI)
通过as
关键字,你可以引入不同的变量名(定义别名):
import some_module as sm
from some_module import PI as pi, g as gf
r1 = sm.f(pi)
r2 = gf(6, pi)
大部分二元数学运算和比较运算都跟我们想象中的一样:
In [1]: 5 - 7
Out[1]: -2
In [2]: 12 + 12.5
Out[2]: 24.5
In [3]: 1 >=5
Out[3]: False
下表为所有可用的二元运算符:
运算 | 说明 |
---|---|
a + b | - |
a - b | - |
a * b | - |
a / b | - |
a % b | 取余 |
a // b | a除以b后向下圆整(不进位),丢弃小数部分 |
a ** b | a的b次方 |
a & b | 布尔值相与。对于整数,执行按位与操作 |
a | b | 布尔值相或。对于整数,执行按位或操作 |
a ^ b | 布尔值相异或。对于整数,执行按位异或操作 |
a == b | 如果a等于b,则结果为True |
a != b | 如果a不等于b,则结果为True |
a <= b、a < b | 如果a小于等于(或小于)b,则结果为True |
a > b、a >= b | 如果a大于(或大于等于)b,则结果为True |
a is b | 如果引用a和b指向同一个Python对象,则结果为True |
a is not b | 如果引用a和b指向不同的Python对象,则结果为True |
如上表,要判断两个引用是否指向同一个对象,可以使用is
关键字。如果想判断两个引用是否不是指向同一个对象,则可以使用is not
:
In [4]: a = [1, 2, 3]
In [5]: b = a
# 注意,list函数始终会创建新列表
In [6]: c = list(a)
In [7]: a is b
Out[7]: True
In [8]: a is not c
Out[8]: True
由于list
函数始终会创建新列表,也就是说它不是对a的引用。
同时要注意,这跟比较运算“==”不是一回事,因为由上面的情况可以得到:
In [9]: a == c
Out[9]: True
is
和is not
常常用于判断变量是否为None
,因为None
的实例只有一个:
In [10]: a = None
In [11]: a is None
Out[11]: True
严格意味着立即,懒惰意指延迟。
无论使用什么编程语言,都必须了解表达式是何时被求值的。以下面两个简单的表达式为例:
a = b = c = 5
d = a + b * c
在Python中,只要这些语句被求值,相关计算就会立即(也就是严格)发生,d的值会被设置为30。而在另一种编程范式中(比如Haskell这样的纯函数编程语言),d的值在被使用之前是不会被计算出来的。这种将计算推迟的思想通常称为延迟计算(lazy evaluation)。而Python是一种非常严格的(急性子)语言。几乎在任何时候,计算过程和表达式都是立即求值的。即使是在上面那个简单的例子中,也是先计算b * c
的结果然后再将其与a加起来的。
有一些Python技术(尤其是用到迭代器和生成器的那些)可以用于实现延迟计算。在数据密集型应用中,当执行一些负荷非常高的计算时(这种情况不太多),这些技术就能派上用场了。
大部分Python对象是可变的(mutable),比如列表、字典、NumPy数组以及大部分用户自定义类型(类)。也就是说,它们所包含的对象或值是可以被修改的。
In [12]: a_list = ['foo', 2, [4, 5]]
In [13]: a_list[2] = (3, 4)
In [14]: a_list
Out[14]: ['foo', 2, (3, 4)]
而其他的(如字符串和元组等),则是不可变的(immutable):
In [15]: a_tuple = (3, 5, (4, 5))
In [16]: a_tuple[1] = 'four'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
16-b7966a9ae0f1> in ()
----> 1 a_tuple[1] = 'four'
TypeError: 'tuple' object does not support item assignment
不可变是指——“不能修改原内存块的数据”。也就是说,即使修改操作成功了,也只是创建了一个新对象并将其引用赋值给原变量而已。
注意,仅仅因为“可以修改某个对象”并不代表“就该那么做”。这种行为在编程中也叫做副作用(side effect)。例如,在编写一个函数时,任何副作用都应该通过该函数的文档或注释明确地告知用户。即使可以使用可变对象,也建议尽量避免副作用且注重不变性。