工具学习 -python -01 基础知识点

借着别人的文章(王圣元 [王的机器] ),复盘一下python的基础知识点。感谢原作者的分享!

一、基本数据类型:整数,浮点数,布尔
二、容器数据类型:字符,元组,列表,字典,集合
三、条件语句和迭代循环:If, If_else, Nested, While, For
四、函数:正规函数,匿名函数,偏函数,柯里化
五、解析式:列表,字典,集合解析

对于任何一种计算机语言,我觉得最重要的就是「数据类型」「条件语句 & 迭代循环」和「函数」,这三方面一定要打牢基础。此外 Python 非常简洁,一行代码 (one-liner) 就能做很多事情,很多时候都用了各种「解析式」,比如列表、字典和集合解析式。

在学习本贴前感受一下这个问题:如何把以下这个不规则的列表 a 里的所有元素一个个写好,专业术语叫打平 (flatten)?

a = [1, 2, [3, 4], [[5, 6], [7, 8]]]

魔法来了 (这一行代码有些长,用手机的建议横屏看)

fn = lambda x: [y for l in x for y in fn(l)] if type(x) is list else [x]
[1, 2, 3, 4, 5, 6, 7, 8]

这一行代码,用到了迭代、匿名函数、递推函数、解析式这些技巧。初学者一看只会说“好酷啊,但看不懂”,看完本帖和下帖后,我保证你会说“我也会这样用了,真酷!


1基本数据类型

Python 里面有自己的内置数据类型 (build-in data type),本节介绍基本的三种,分别是整型 (int),浮点型 (float),和布尔型 (bool)。

1.1 整型

整数 (integer) 是最简单的数据类型,和下面浮点数的区别就是前者小数点后没有值,后者小数点后有值。例子如下:

a = 1031                 --1031 <class 'int'>

通过 print 的可看出 a 的值,以及类 (class) 是 int。Python 里面万物皆对象(object),「整数」也不例外,只要是对象,就有相应的属性 (attributes) 和方法 (methods)。

知识点

通过 dir( X ) 和help( X ) 可看出 X 对应的对象里可用的属性和方法。

  • X 是 int,那么就是 int 的属性和方法
  • X 是 float,那么就是 float 的属性和方法
dir(int)
['__abs__','__add__',...'__xor__','bit_length','conjugate',...'real','to_bytes']

红色的是 int 对象的可用方法,蓝色的是 int 对象的可用属性。对他们你有个大概印象就可以了,具体怎么用,需要哪些参数 (argument),你还需要查文档。看个bit_length的例子

a.bit_length()              -- 11

该函数是找到一个整数的二进制表示,再返回其长度。在本例中 a = 1031, 其二进制表示为 ‘10000000111’ ,长度为 11。

1.2 浮点型

简单来说,浮点型 (float) 数就是实数, 例子如下:

print( 1, type(1) )                 -- 1 <class 'int'>1.0 <class 'float'>

加一个小数点 . 就可以创建 float,不能再简单。有时候我们想保留浮点型的小数点后 n 位。可以用 decimal 包里的 Decimal 对象和 getcontext() 方法来实现。

import decimal

Python 里面有很多用途广泛的包 (package),用什么你就引进 (import) 什么。包也是对象,也可以用上面提到的dir(decimal) 来看其属性和方法。比如 getcontext() 显示了 Decimal 对象的默认精度值是 28 位 (prec=28),展示如下:

decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999,Emax=999999, capitals=1, clamp=0, flags=[],traps=[InvalidOperation, DivisionByZero, Overflow])

让我们看看 1/3 的保留 28 位长什么样?

d = Decimal(1) / Decimal(3)
Decimal('0.3333333333333333333333333333')

那保留 4 位呢?用 getcontext().prec 来调整精度哦。

decimal.getcontext().prec = 4 
Decimal('0.3333')

高精度的 float 加上低精度的 float,保持了高精度,没毛病。

d + e
Decimal('0.6666333333333333333333333333')

1.3 布尔型

布尔 (boolean) 型变量只能取两个值,**True **和 False。当把布尔变量用在数字运算中,用 1 和 0 代表 **True **和 False

T = True
F = False
print( T + 2 )                      -- 3
print( F - 8 )                      --7

除了直接给变量赋值 **True **和 False,还可以用 bool(X) 来创建变量,其中 X 可以是

  • 基本类型:整型、浮点型、布尔型

  • 容器类型:字符、元组、列表、字典和集合

基本类型

print( type(0), bool(0), bool(1) )                       --<class 'int'> False True
print( type(10.31), bool(0.00), bool(10.31) )            --<class 'float'> False True
print( type(True), bool(False), bool(True) )             --<class 'bool'> False True

bool 作用在基本类型变量的总结:X 只要不是整型 0、浮点型 0.0,bool(X) 就是 True,其余就是 False

容器类型

print( type(''), bool( '' ), bool( 'python' ) )                --<class 'str'> False True
print( type(()), bool( () ), bool( (10,) ) )                   --<class 'tuple'> False True
print( type([]), bool( [] ), bool( [1,2] ) )                   --<class 'list'> False True
print( type({}), bool( {} ), bool( {'a':1, 'b':2} ) )          --<class 'dict'> False True
print( type(set()), bool( set() ), bool( {1,2} ) )             --<class 'set'> False True

bool 作用在容器类型变量的总结X 只要不是空的变量,bool(X) 就是 True,其余就是 False

知识点

确定bool(X) 的值是 True 还是 False,就看 X 是不是空,空的话就是 False,不空的话就是True。

  • 对于数值变量,0, 0.0 都可认为是空的。
  • 对于容器变量,里面没元素就是空的。

2 容器数据类型

上节介绍的整型、浮点型和布尔型都可以看成是单独数据,而这些数据都可以放在一个容器里得到一个「容器类型」的数据,比如:

  • 字符 (str) 是一容器的字节 char,注意 Python 里面没有 char 类型的数据,可以把单字符的 str 当做 char。
  • 元组 (tuple)、列表 (list)、字典 (dict) 和集合 (set) 是一容器的任何类型变量。

2.1 字符

字符用于处理文本 (text) 数据,用「单引号 ’」和「双引号 “」来定义都可以。
创建字符

t1 = 'i love Python!'
print( t1, type(t1) )             --i love Python! <class 'str'>
t2 = "I love Python!"
print( t2, type(t2) )             --I love Python! <class 'str'>

字符中常见的内置方法 (可以用 dir(str) 来查) 有

  • capitalize():大写句首的字母
  • split():把句子分成单词
  • find(x):找到给定词 x 在句中的索引,找不到返回 -1
  • replace(x, y):把句中 x 替代成 y
  • strip(x):删除句首或句末含 x 的部分
t1.capitalize()                                      --'I love python!'
t2.split()                                           --['I', 'love', 'Python!']
print( t1.find('love') )                             -- 2
print( t1.find('like') )                             -- 1
t2.replace( 'love Python', 'hate R' )                --'I hate R!'
print( 'http://www.python.org'.strip('htp:/') )      --www.python.org
print( 'http://www.python.org'.strip('.org') )       --http://www.python

索引和切片

s = 'Python'
print( s )                   --Python           
print( s[2:4] )              --th
print( s[-5:-2] )            --yth
print( s[2] )                --t
print( s[-1] )               --n

知识点

Python 里面索引有三个特点 (经常让人困惑):

  1. 从 0 开始 (和 C 一样),不像 Matlab 从 1 开始。
  2. 切片通常写成 start:end 这种形式,包括「start 索引」对应的元素,不包括「end索引」对应的元素。因此 s[2:4] 只获取字符串第 3 个到第 4 个元素。
  3. 索引值可正可负,正索引从 0 开始,从左往右;负索引从 -1 开始,从右往左。使用负数索引时,会从最后一个元素开始计数。最后一个元素的位置编号是 -1。

正则表达式

正则表达式 (regular expression) 主要用于识别字符串中符合某种模式的部分,什么叫模式呢?用下面一个具体例子来讲解。

input = """
'06/18/2019 13:00:00', 100, '1st';
'06/18/2019 13:30:00', 110, '2nd';
'06/18/2019 14:00:00', 120, '3rd'
"""

假如你想把上面字符串中的「时间」的模式来抽象的表示出来,对照着具体表达式 '06/18/2019 13:00:00' 来看,我们发现该字符串有以下规则:

  1. 开头和结束都有个单引号 '
  2. 里面有多个 0-9 数字
  3. 里面有多个正斜线 / 和分号 :
  4. 还有一个空格

因此我们用下面这样的模式

pattern = re.compile("'[0-9/:\s]+'")

再看这个抽象模式表达式 '[0-9/:\s]+',里面符号的意思如下:

  • 最外面的两个单引号 ' 代表该模式以它们开始和结束
  • 中括号 [] 用来概括该模式涵盖的所有类型的字节
  • 0-9 代表数字类的字节
  • / 代表正斜线
  • : 代表分号
  • \s 代表空格
  • [] 外面的加号 + 代表 [] 里面的字节出现至少 1 次

有了模式 pattern,我们来看看是否能把字符串中所有符合 pattern 的日期表达式都找出来。

pattern.findall(input)
["'06/18/2019 13:00:00'",
 "'06/18/2019 13:30:00'",
 "'06/18/2019 14:00:00'"]

结果是对的,之后你想怎么盘它就是你自己的事了,比如把 / 换成 -,比如用 datetime 里面的 striptime() 把日期里年、月、日、小时、分钟和秒都获取出来。

2.2元组

创建元组

「元组」定义语法为 : (元素1, 元素2, ..., 元素n)
关键点是「小括号 ()」和「逗号 ,」

  • 小括号把所有元素绑在一起
  • 逗号将每个元素一一分开

创建元组的例子如下:

t1 = (1, 10.31, 'python')
t2 = 1, 10.31, 'python'
print( t1, type(t1) )             --(1, 10.31, 'python') <class 'tuple'>
print( t2, type(t2) )             --(1, 10.31, 'python') <class 'tuple'>

知识点

创建元组可以用小括号 (),也可以什么都不用,为了可读性,建议还是用 ()。此外对于含单个元素的元组,务必记住要多加一个逗号,举例如下:

print( type( ('OK') ) )                 --<class 'str'>
print( type( ('OK',) )                  --<class 'tuple'>

看看,没加逗号来创建含单元素的元组,Python 认为它是字符。

当然也可以创建二维元组:

nested = (1, 10.31, 'python'), ('data', 11)
nested                                --((1, 10.31, 'python'), ('data', 11))

索引和切片

元组中可以用整数来对它进行索引 (indexing) 和切片 (slicing),不严谨的讲,前者是获取单个元素,后者是获取一组元素。接着上面二维元组的例子,先看看索引的代码:

nested[0]                                               --(1, 10.31, 'python')
print( nested[0][0], nested[0][1], nested[0][2] )       --1 10.31 python    

再看看切片的代码:

nested[0][0:2]                                           --(1, 10.31)

不可更改
元组有不可更改 (immutable) 的性质,因此不能直接给元组的元素赋值,例子如下 (注意「元组不支持元素赋值」的报错提示)。

t = ('OK', [1, 2], True) 
t[2] = False         --TypeError: 'tuple' object does not support item assignment

但是只要元组中的元素可更改 (mutable),那么我们可以直接更改其元素,注意这跟赋值其元素不同。如下例 t[1] 是列表,其内容可以更改,因此用 append 在列表后加一个值没问题。

t[1].append(3)             --('OK', [1, 2, 3], True)

内置方法

元组大小和内容都不可更改,因此只有 count 和 index 两种方法。

t = (1, 10.31, 'python')
print( t.count('python') )         --1
print( t.index(10.31) )            --1

这两个方法返回值都是 1,但意思完全不同

  • count('python') 是记录在元组 t 中该元素出现几次,显然是 1 次
  • index(10.31) 是找到该元素在元组 t 的索引,显然是 1

元组拼接

元组拼接 (concatenate) 有两种方式,用「加号 +」和「乘号 *」,前者首尾拼接,后者复制拼接。

(1, 10.31, 'python') + ('data', 11) + ('OK',)        --(1, 10.31, 'python', 'data', 11, 'OK')
(1, 10.31, 'python') * 2                   --(1, 10.31, 'python', 1, 10.31, 'python')

解压元组

解压 (unpack) 一维元组 (有几个元素左边括号定义几个变量)

t = (1, 10.31, 'python')
(a, b, c) = t
print( a, b, c )                    --1 10.31 python

解压二维元组 (按照元组里的元组结构来定义变量)

t = (1, 10.31, ('OK','python'))
(a, b, (c,d)) = t
print( a, b, c, d )                 --1 10.31 OK python

如果你只想要元组其中几个元素,用通配符「*」,英文叫 wildcard,在计算机语言中代表一个或多个元素。下例就是把多个元素丢给了 rest 变量。

t = 1, 2, 3, 4, 5
a, b, *rest, c = t
print( a, b, c )                 -- 1 2 5
print( rest )                    --[3, 4]

如果你根本不在乎 rest 变量,那么就用通配符「*」加上下划线「_」,刘例子如下:

a, b, *_ = t
print( a, b )                    --1 2

优点缺点

优点:占内存小,安全,创建遍历速度比列表快,可一赋多值。
缺点:不能添加和更改元素。

2.3列表

创建列表

「列表」定义语法为 : [元素1, 元素2, ..., 元素n]
关键点是「中括号 []」和「逗号 ,」

  • 中括号把所有元素绑在一起
  • 逗号将每个元素一一分开

创建列表的例子如下:

l = [1, 10.31,'python']
print(l, type(l))                  --[1, 10.31, 'python'] <class 'list'>

内置方法

不像元组,列表内容可更改 (mutable),因此附加 (append, extend)、插入 (insert)、删除 (remove, pop) 这些操作都可以用在它身上。

附加


l.append([4, 3])
print( l )                      --[1, 10.31, 'python', [4, 3]]
l.extend([1.5, 2.0, 'OK'])
print( l )                      --[1, 10.31, 'python', [4, 3], 1.5, 2.0, 'OK']

严格来说 append 是追加,把一个东西整体添加在列表后,而 extend 是扩展,把一个东西里的所有元素添加在列表后。对着上面结果感受一下区别。

插入

l.insert(1, 'abc') # insert object before the index position
print(l)           --[1, 'abc', 10.31, 'python', [4, 3], 1.5, 2.0, 'OK']

insert(i, x) 在编号 i 位置前插入 x。对着上面结果感受一下。

删除

l.remove('python') # remove first occurrence of object
print(I)               --[1, 'abc', 10.31, [4, 3], 1.5, 2.0, 'OK']
p = l.pop(3) # removes and returns object at index.  Only only pop 1 index position at any time.
print( p )               --[4, 3]
print( l )               --[1, 'abc', 10.31, 1.5, 2.0, 'OK']

remove 和 pop 都可以删除元素

  • 前者是指定具体要删除的元素,比如 'python'
  • 后者是指定一个编号位置,比如 3,删除 l[3] 并返回出来

对着上面结果感受一下,具体用哪个看你需求。

切片索引

l = [7, 2, 999, 1000, 1, 3, 7, 2, 0, 1]
print( l )                   --[7, 2, 999, 1000, 1, 3, 7, 2, 0, 1]
print( l[1:5:2] )            --[2, 1000]
print( l[:5:2] )             --[7, 999, 1]
print( l[1::2] )             --[2, 1000, 3, 2, 1]
print( l[::2] )              --[7, 999, 1, 7, 0]
print( l[::-1] )             --[1, 0, 2, 7, 3, 1, 1000, 999, 2, 7]

列表拼接

和元组拼接一样, 列表拼接也有两种方式,用「加号 +」和「乘号 *」,前者首尾拼接,后者复制拼接。

[1, 10.31, 'python'] + ['data', 11] + ['OK']       --[1, 10.31, 'python', 'data', 11, 'OK']
[1, 10.31, 'python'] * 2                           --[1, 10.31, 'python', 1, 10.31, 'python']

优点缺点

优点:灵活好用,可索引、可切片、可更改、可附加、可插入、可删除。
缺点:相比 tuple 创建和遍历速度慢,占内存。此外查找和插入时间较慢。

2.4字典

创建字典

「字典」定义语法为 : {元素1, 元素2, ..., 元素n}
其中每一个元素是一个「键值对」- 键:值 (key:value)

关键点是「大括号 {}」,「逗号 ,」和「分号 :」

  • 大括号把所有元素绑在一起
  • 逗号将每个键值对一一分开
  • 分号将键和值分开

创建字典的例子如下:

d = {
'Name' : 'Tencent',
'Country' : 'China',
'Industry' : 'Technology',
'Code': '00700.HK',
'Price' : '361 HKD'
}
print( d, type(d) )    
{'Name': 'Tencent', 'Country': 'China', 
'Industry': 'Technology', 'Code': '00700.HK', 
'Price': '361 HKD'} <class 'dict'>

内置方法

字典里最常用的三个内置方法就是 keys(), values() 和 items(),分别是获取字典的键、值、对。

print( list(d.keys()) )        --['Name', 'Country', 'Industry', 'Code', 'Price', 'Headquarter']
print( list(d.values()))       -- ['Tencent', 'China', 'Technology', '00700.HK', '359 HKD', 'Shen Zhen']
print( list(d.items()) )       -- [('Name', 'Tencent'), ('Country', 'China'), ('Industry', 'Technology'), ('Code', '00700.HK'), ('Price', '359 HKD'), ('Headquarter', 'Shen Zhen')]

此外在字典上也有添加、获取、更新、删除等操作。

添加

比如加一个「总部:深圳」

d['Headquarter'] = 'Shen Zhen'
d                                 --{'Name': 'Tencent', 'Country': 'China',
                                     'Industry': 'Technology', 'Code': '00700.HK',
                                     'Price': '361 HKD', 'Headquarter': 'Shen Zhen'}

获取

比如想看看腾讯的股价是多少 (两种方法都可以)

print( d['Price'] )          --359 HKD359 HKD

更新

比如更新腾讯的股价到 359 港币

d['Price'] = '359 HKD'

删除

比如去掉股票代码 (code)

del d['Code']

或像列表里的 pop() 函数,删除行业 (industry) 并返回出来。

print( d.pop('Industry') )
d
Technology

{'Name': 'Tencent',
'Country': 'China',
'Price': '359 HKD',
'Headquarter': 'Shen Zhen'}

不可更改键

字典里的键是不可更改的,因此只有那些不可更改的数据类型才能当键,比如整数 (虽然怪怪的)、浮点数 (虽然怪怪的)、布尔 (虽然怪怪的)、字符、元组 (虽然怪怪的),而列表却不行,因为它可更改。

那么如何快速判断一个数据类型 X 是不是可更改的呢?两种方法:

  1. 麻烦方法:用 id(X) 函数,对 X 进行某种操作,比较操作前后的 id,如果不一样,则 X 不可更改,如果一样,则 X 可更改。

  2. 便捷方法:用 hash(X),只要不报错,证明 X 可被哈希,即不可更改,反过来不可被哈希,即可更改。

先看用 id() 函数的在整数 i 和列表 l 上的运行结果:


i = 1
print( id(i) )               --1607630928
i = i + 2
print( id(i) )               --1607630992
l = [1, 2]
print( id(l) )               --2022027856840
l.append('Python')
print( id(l) )               --2022027856840
  • 整数 i 在加 1 之后的 id 和之前不一样,因此加完之后的这个 i (虽然名字没变),但是不是加前的那个 i 了,因此整数是不可更改的。
  • 列表 l 在附加 'Python' 之后的 id 和之前一样,因此列表是可更改的。

先看用 hash() 函数的在字符 s,元组 t 和列表 l 上的运行结果:

hash('Name')                     --7230166658767143139
hash( (1,2,'Python') )           --3642952815031607597
hash( [1,2,'Python'] )           --TypeError: unhashable type: 'list'

字符 s 和元组 t 都能被哈希,因此它们是不可更改的。列表 l 不能被哈希,因此它是可更改的。

优点缺点

优点:查找和插入速度快
缺点:占内存大

2.5集合

创建集合

「集合」有两种定义语法,第一种是: {元素1, 元素2, ..., 元素n}
关键点是「大括号 {}」和「逗号 ,」

  • 大括号把所有元素绑在一起
  • 逗号将每个元素一一分开

第二种是用 set() 函数,把列表元组转换成集合。
set( **列表 **或 元组 )

创建集合的例子如下 (用两者方法创建 A 和 B):

A = set(['u', 'd', 'ud', 'du', 'd', 'du'])
B = {'d', 'dd', 'uu', 'u'}
print( A )                       --{'d', 'du', 'u', 'ud'}
print( B )                       --{'d', 'du', 'u', 'ud'}

从 A 的结果发现集合的两个特点:无序 (unordered) 和唯一 (unique)。由于 set 存储的是无序集合,所以我们没法通过索引来访问,但是可以判断一个元素是否在集合中。

B[1]                --TypeError: 'set' object does not support indexing
'u' in B            --True

内置方法

用 set 的内置方法就把它当成是数学上的集,那么并集、交集、差集都可以玩通了。

并集 OR

print( A.union(B) )     # All unique elements in A or B     
--{'uu', 'dd', 'd', 'u', 'du', 'ud'}
print( A | B )          # A OR B                            
--{'uu', 'dd', 'd', 'u', 'du', 'ud'}

交集 AND

print( A.intersection(B) )   # All elements in both A and B
--{'d', 'u'}
print( A & B )               # A AND B
--{'d', 'u'}

差集 A - B

print( A.difference(B) )     # Elements in A but not in B
--{'ud', 'du'}
print( A - B )               # A MINUS B
--{'ud', 'du'}

差集 B - A

print( B.difference(A) )     # Elements in B but not in A
--{'uu', 'dd'}
print( B - A )               # B MINUS A
--{'uu', 'dd'}

对称差集 XOR

print( A.symmetric_difference(B) )   # All elements in either A or B, but not both
--{'ud', 'du', 'dd', 'uu'}
print( A ^ B )                       # A XOR B
--{'ud', 'du', 'dd', 'uu'}

优点缺点

优点:不用判断重复的元素
缺点:不能存储可变对象

你看集合的「唯一」特性还不是双刃剑,既可以是优点,又可以是缺点,所有东西都有 trade-off 的,要不然它就没有存在的必要了。


3条件语句 & 迭代循环

在编写程序时,我们要

  • 在不同条件下完成不同动作,条件语句 (conditional statement) 赋予程序这种能力。
  • 重复的完成某些动作,迭代循环 (iterative loop) 赋予程序这种能力。

3.1条件语句

条件语句太简单了,大体有四种格式

  1. if 语句
  2. if-else 语句
  3. if-elif-else 语句
  4. nested 语句

3.1.1 if 语句

给定二元条件,满足做事,不满足不做事。

if x > 0:
    print( 'x is positive' )

3.1.2 if-else 语句

给定二元条件,满足做事 A,不满足做事 B。

if x % 2 == 0:
    print( 'x is even' )
else :
    print( 'x is odd' )

3.1.3 if-elif-else 语句

给定多元条件,满足条件 1 做事 A1,满足条件 2 做事 A2,..., 满足条件 n 做事 An。直到把所有条件遍历完。

f x < y:
    print( 'x is less than y' )
elif x > y:
    print( 'x is greater than y' )
else:
    print( 'x and y are equal' )

4. nested 语句

给定多元条件,满足条件 1 做事 A1,不满足就
满足条件 2 做事 A2,不满足就
...
直到把所有条件遍历完。

if x == y:
    print( 'x and y are equal' )
else:
    if x < y:
        print( 'x is less than y' )
    else:
        print( 'x is greater than y' )

3.2 迭代循环

对于迭代循环,Python 里面有「while 循环」和「for 循环」,没有「do-while 循环」。

3.2.1 while 循环

n = 5
while n > 0:
    print(n)
    n = n-1
print('I love Python')
5
4
3
2
1
I love Python

While 循环非常简单,做事直到 while 后面的语句为 False。上例就是打印从 n (初始值为 5) 一直到 1,循环执行了 5 次。

一般来说,在 「while 循环」中,迭代的次数事先是不知道的,因为通常你不知道 while 后面的语句从 True 变成 False了。

3.2.2 for 循环

更多时候我们希望事先直到循环的次数,比如在列表、元组、字典等容器类数据上遍历一遍,在每个元素层面上做点事情。这时候就需要「for 循环」了。

languages = ['Python', 'R', 'Matlab', 'C++']
for language in languages:
    print( 'I love', language )
print( 'Done!' )
I love Python
I love R
I love Matlab
I love C++
Done

读读 Python 里面的「for 循环」是不是很像读英文。通用形式的 for loop 如下

for a in A
    do something with a        

其中 forin 是关键词,A 是个可迭代数据 (list, tuple, dic, set),a 是 A 里面的每个元素.

回到具体例子,for loop 里面的 language 变量在每次循环中分别取值 Python, R, Matlab 和 C++,然后被打印。

最后介绍一个稍微有点特殊的函数 enumerate(),和 for loop 一起用的语法如下

for i, a in enumerate(A)
     do something with a  

发现区别了没?用 enumerate(A) 不仅返回了 A 中的元素,还顺便给该元素一个索引值 (默认从 0 开始)。此外,用 enumerate(A, j) 还可以确定索引起始值为 j。 看下面例子。

languages = ['Python', 'R', 'Matlab', 'C++']
for i, language in enumerate(languages, 1):
    print( i, 'I love', language )
print( 'Done!' )
1 I love Python
2 I love R
3 I love Matlab
4 I love C++
Done!

4、函数

Python 里函数太重要了 (说的好像在别的语言中函数不重要似的)。函数的通用好处就不用多说了吧,重复使用代码,增强代码可读性等等。
还记得 Python 里面『万物皆对象』么?Python 把函数也当成对象,可以从另一个函数中返回出来而去构建高阶函数,比如

  • 参数是函数
  • 返回值是函数

4.1 正规函数

Python 里面的正规函数 (normal function) 就像其他语言的函数一样,之所以说正规函数是因为还有些「不正规」的,比如匿名函数,高阶函数等等。

但即便是正规函数,Python 的函数具有非常灵活多样的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。从简到繁的参数形态如下:

位置参数 (positional argument)
默认参数 (default argument) 默认函数一定要放在位置参数后面,不然程序会报错。
可变参数 (variable argument)
关键字参数 (keyword argument)
命名关键字参数 (name keyword argument)
参数组合

每种参数形态都有自己对应的应用,接下来用定义一个金融产品为例来说明各种参数形态的具体用法。

4.1.1 位置参数和默认参数

比较简单,略过

4.1.2 可变参数

在 Python 函数中,还可以定义「可变参数」。顾名思义,可变参数就是传入的参数个数是可变的,可以是 0, 1, 2 到任意个。

image.png

金融产品未来多个折现现金流 (discounted cash flow, DCF),但不知道具体多少个,这时我们可以用 *args 来表示不确定个数的 DCF。下面程序也对 DCF 加总得到产品现值 (present value, PV)

def instrument4( id, ntl=1, curR='CNY', *args ):
    PV = 0
    for n in args:
        PV = PV + n

    print( 'id:', id )
    print( 'notional:', ntl )
    print( 'reporting currency:', curR )
    print( 'present value:', PV*ntl )

如果一个产品 (单位本金) 在后 3 年的折现现金流为 1, 2, 3,将它们传入 args,计算出它的现值为 600 = 100(1+2+3)。

instrument4( 'MM1001', 100, 'EUR', 1, 2, 3 )
--
id: MM1001
notional: 100
reporting currency: EUR
present value: 600

除了直接传入多个参数之外,还可以将所有参数先组装成元组 DCF,用以「*DCF」的形式传入函数 (DCF 是个元组,前面加个通配符 * 是拆散元组,把元组的元素传入函数中)

DCF = (1, 2, 3, 4, 5)
instrument4( 'MM1001', 10, 'EUR', *DCF )
--
id: MM1001
notional: 10
reporting currency: EUR
present value: 150

可变参数用两种方式传入

直接传入,func(1, 2, 3)
先组装列表或元组,再通过 args 传入,func([1, 2, 3]) 或 func(*(1, 2, 3))

4.1.3 关键字参数

image.png

「可变参数」和「关键字参数」的同异总结如下:

  • 可变参数允许传入零个到任意个参数,它们在函数调用时自动组装为一个元组 (tuple)
  • 关键字参数允许传入零个到任意个参数,它们在函数内部自动组装为一个字典 (dict)

在定义金融产品,有可能不断增加新的信息,比如交易对手、工作日惯例、工作日计数惯例等等。我们可以用「关键字参数」来满足这种需求,即用 **kw。

def instrument5( id, ntl=1, curR='CNY', *args, **kw ):
    PV = 0
    for n in args:
        PV = PV + n

    print( 'id:', id )
    print( 'notional:', ntl )
    print( 'reporting currency:', curR )
    print( 'present value:', PV*ntl )
    print( 'keyword:', kw)

如果不传入任何「关键字参数」,kw 为空集。

instrument5( 'MM1001', 100, 'EUR', 1, 2, 3 )
--
id: MM1001
notional: 100
reporting currency: EUR
present value: 600
keyword: {}

当知道交易对手 (counterparty) 是高盛时,给函数传入一个「关键字参数」,ctp = 'GS'。

instrument5( 'MM1001', 100, 'EUR', 1, 2, 3, ctp='GS' )
--
id: MM1001
notional: 100
reporting currency: EUR
present value: 600
keyword: {'ctp': 'GS'}

当知道日期计数 (daycount) 是 act/365 时,再给函数传入一个「关键字参数」,dc = 'act/365'。

instrument5( 'MM1001', 100, 'EUR', 1, 2, 3, dc='act/365', ctp='GS' )
---
id: MM1001
notional: 100
reporting currency: EUR
present value: 600
keyword: {'dc': 'act/365', 'ctp': 'GS'}

除了直接传入多个参数之外,还可以将所有参数先组装成字典 Conv,用以「**Conv」的形式传入函数 (Conv 是个字典,前面加个通配符 ** 是拆散字典,把字典的键值对传入函数中)

DCF = (1, 2, 3, 4, 5)
Conv = {'dc':'act/365', 'bdc':'following'}
instrument5( 'MM1001', 10, 'EUR', *DCF, **Conv )
--
id: MM1001
notional: 10
reporting currency: EUR
present value: 150
keyword: {'dc': 'act/365', 'bdc': 'following'}

4.1.4 命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。


image.png

如果要限制关键字参数的名字,就可以用「命名关键字参数」,例如,用户希望交易对手 ctp 是个关键字参数。这种方式定义的函数如下:

def instrument6( id, ntl=1, curR='CNY', *, ctp, **kw ):
    print( 'id:', id )
    print( 'notional:', ntl )
    print( 'reporting currency:', curR )
    print( 'counterparty:', ctp )
    print( 'keyword:', kw)

从调用函数 instrument6 得到的结果可看出 ctp 是「命名关键字参数」,而 dc 是「关键字参数」。

instrument6( 'MM1001', 100, 'EUR', dc='act/365', ctp='GS' )
--
id: MM1001
notional: 100
reporting currency: EUR
counterparty: GS
keyword: {'dc': 'act/365'}

使用命名关键字参数时,要特别注意不能缺少参数名。下例没有写参数名 ctp,因此 'GS' 被当成「位置参数」,而原函数只有 3 个位置函数,现在调用了 4 个,因此程序会报错:

instrument6( 'MM1001', 100, 'EUR', 'GS', dc='act/365' )
--
TypeError: instrument6() takes from 1 to 3
positional arguments but 4 were given

4.1.5 参数组合

在 Python 中定义函数,可以用位置参数、默认参数、可变参数、命名关键字参数和关键字参数,这 5 种参数中的 4 个都可以一起使用,但是注意,参数定义的顺序必须是:

  • 位置参数、默认参数、可变参数和关键字参数。
  • 位置参数、默认参数、命名关键字参数和关键字参数。
    要注意定义可变参数和关键字参数的语法:

*args 是可变参数,args 接收的是一个 tuple
**kw 是关键字参数,kw 接收的是一个 dict

命名关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。定义命名关键字参数不要忘了写分隔符 *,否则定义的是位置参数。

4.2 匿名函数

在 Python 里有两种函数:

  • 用 def 关键词的正规函数
  • 用 lambda 关键词的匿名函数

匿名函数 (anonymous function) 的说明如下:


image.png
func = lambda x, y: x*y            #函数输入是 x 和 y,输出是它们的积 x*y
func(2, 3)            --6

func = lambda *args: sum(args)      #输入是任意个数的参数,输出是它们的和
func( 1, 2, 3, 4, 5 )       --15

func = lambda **kwargs: 1             #输入是任意键值对参数,输出是 1
func( name='Steven', age='36' )    --1

正规函数vs匿名函数

lbd_sqr = lambda x: x ** 2
lbd_sqr                  --<function __main__.<lambda>(x)>

def sqr(x):
    return x ** 2
sqr                      --<function __main__.sqr(x)>

print( sqr(9) )          --81
print( lbd_sqr(9) )      --81

4.3高阶函数

高阶函数 (high-order function) 在函数化编程 (functional programming) 很常见,主要有两种形式:

  • 参数是函数 (map, filter, reduce)
  • 返回值是函数 (closure, partial, currying)

4.3.1 Map, Filter, Reduce

Python 里面的 map, filter 和 reduce 属于第一种高阶函数,参数是函数。这时候是不是很自然的就想起了 lambda 函数?
作为内嵌在别的函数里的参数,lambda 函数就像微信小程序一样,即用即丢,非常轻便。

首先看看 map, filter 和 reduce 的语法:

  • map(函数 f, 序列 x):对序列 x 中每个元素依次执行函数 f,将 f(x) 组成一个「map 对象」返回 (可以将其转换成 list 或 set)

  • filter(函数 f, 序列 x):对序列 x 中每个元素依次执行函数 f,将 f(x) 为 True 的结果组成一个「filter 对象」返回 (可以将其转换成 list 或 set)

  • reduce(函数 f, 序列 x):对序列 x 的第一个和第二个元素执行函数 f,得到的结果和序列 x 的下一个元素执行函数 f,一直遍历完的序列 x 所有元素。

lst = [1, 2, 3, 4, 5]
map_iter = map( lambda x: x**2, lst )
print( map_iter )                       --<map object at 0x0000018C83E72390>
print( list(map_iter) )                 --[1, 4, 9, 16, 25]

filter_iter = filter(lambda n: n % 2 == 1, lst)
print( filter_iter )                    --<filter object at 0x0000018C83E722E8>
print( list(filter_iter) )              --[1, 3, 5]

from functools import reduce
reduce( lambda x,y: x+y, lst )                 -- 15
reduce( lambda x,y: x+y, lst, 100 )            -- 115

注意 map_iter 是 map 函数的返回对象 (它是一个迭代器),想要将其内容显示出来,需要用 list 将其转换成「列表」形式。
小结一下,对于 map, filter 和 reduce,好消息是,Python 支持这些基本的操作;而坏消息是,Python 不建议你使用它们。下节的「解析式」可以优雅的替代 map 和 filter。

4.3.2 闭包

Python 里面的闭包 (closure) 属于第二种高阶函数,返回值是函数。下面是一个闭包函数。

def make_counter(init):
    counter = [init]
    
    def inc(): counter[0] += 1
    def dec(): counter[0] -= 1
    def get(): return counter[0]
    def reset(): counter[0] = init
        
    return inc, dec, get, reset

属于第二类 (返回值是函数) 的高阶函数还有「偏函数」和「柯里化」,由于它们比较特别,因此专门分两节来讲解。


4.4 偏函数

偏函数 (paritial function) 主要是把一个函数的参数 (一个或多个) 固定下来,用于专门的应用上 (specialized application)。要用偏函数用从 functools 中导入 partial 包:

from functools import partial

举个排序列表里元素的例子

lst = [3, 1, 2, 5, 4]
sorted( lst )                             --[1, 2, 3, 4, 5]

我们知道 sort 函数默认是按升序排列,假设在你的应用中是按降序排列,你可以把函数里的 reverse 参数设置为 True。

sorted( lst, reverse=True )               --[5, 4, 3, 2, 1]

这样每次设定参数很麻烦,你可以专门为「降序排列」的应用定义一个函数,比如叫 sorted_dec,用偏函数 partial 把内置的 sort 函数里的 reverse 固定住,代码如下:

sorted_dec = partial( sorted, reverse=True )
sorted_dec                          --functools.partial(<built-in function sorted>, reverse=True)

不难发现 sorted_dec 是一个函数,而且参数设置符合我们的应用,把该函数用到列表就能对于降序排列。

sorted_dec( lst )                            --[5, 4, 3, 2, 1]

小结,当函数的参数个数太多,需要简化时,使用 functools.partial 可以创建一个新的函数,即偏函数,它可以固定住原函数的部分参数,从而在调用时更简单。

4.5 柯里化

最简单的柯里化 (currying) 指的是将原来接收 2 个参数的函数 f(x, y) 变成新的接收 1 个参数的函数 g(x) 的过程,其中新函数 g = f(y)。

以普通的加法函数为例:

#参数是 x 和 y,输出 x + y
def add1(x, y):              
    return x + y

# 通过嵌套函数可以把函数 add1 转换成柯里化函数 add2。
# 参数是 x,输出 x + y
def add2(x):                    
    def add(y):
        return x + y
    return add

#参数是 y,输出 2 + y
g = add2(2)           


print( add1(2, 3) )                --5
print( add2(2)(3) )                --5
print( g(3) )                      --5

比较「普通函数 add1」和「柯里化函数 add2」的调用,结果都一样。


5 解析式

解析式 (comprehension) 是将一个可迭代对象转换成另一个可迭代对象的工具。
第一个可迭代对象:可以是任何容器类型数据。
第二个可迭代对象:

  • 列表解析式:可迭代对象是 list
  • 字典解析式:可迭代对象是 dict
  • 集合解析式:可迭代对象是 set

5.1 列表解析式

#从一个含整数列表中把奇数 (odd number) 挑出来
lst = [1, 2, 3, 4, 5] 
odds = []
for n in lst:
    if n % 2 == 1:
        odds.append(n )
odds                                     --[1, 3, 5]

#任务完成了,但这个代码有好几行呢,不简洁,看看下面这一行代码:
odds = [n  for n in lst if n % 2 == 1]
odds                                     --[1, 3, 5]

你可以把「for 循环」到「解析式」的过程想像成一个「复制-粘贴」的过程:

  • 将「for 循环」的新列表复制到「解析式」里
  • 将 append 里面的表达式 n 复制到新列表里
  • 复制循环语句 for n in lst 到新列表里,不要最后的冒号
  • 复制条件语句 if n%2 == 1 到新列表里,不要最后的冒号


    image.png
#用「列表解析式」将一个二维列表中的元素按行一个个展平
# for循环
flattened = []
for row in lst:
    for n in row:
        flattened.append(n)
# 解析式:
flattened = [n for row in lst for n in row]

再回顾下三种解析式,我们发现其实它们都可以实现上节提到的 filter 和 map 函数的功能,用专业计算机的语言说,解析式可以看成是 filter 和 map 函数的语法糖。


image.png
# 列表解析式
[ n*2 for n in lst if n%2 == 1]
# map/filter
list( map(lambda n: n*2, filter(lambda n: n%2 == 1, lst)) )

5.2 例子

# 用解析式将二维元组里每个元素提取出来并存储到一个列表中。
tup = ((1, 2, 3), (4, 5, 6), (7, 8, 9))
flattened = [x for t in tup for x in t]
flattened                             --[1, 2, 3, 4, 5, 6, 7, 8, 9]

#把上面「二维元组」转换成「二维列表」
[ [x for x in t] for t in tup ]       --[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# 用解析式把以下这个不规则的列表 a 打平 (flatten)
# 正规函数
a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
def f(x):
    if type(x) is list:
        return [y for l in x for y in f(l)]
    else:
        return [x]

f(a)                                  --[1, 2, 3, 4, 5, 6, 7, 8]

a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
f = lambda x: [y for l in x for y in f(l)] 
               if type(y) is list else [x]
f(a)                                 --[1, 2, 3, 4, 5, 6, 7, 8]

函数 f(x) 是一个递推函数,当 x 是元素,返回 [x], 那么
f(1) 的返回值是 [1]
f(2) 的返回值是 [2]
当 x 是列表,返回 [y for l in x for y in f(l)],当 x = [3 ,4] 时
for l in x:指的是 x 里每个元素 l,那么 l 遍历 3 和 4
for y in f(l):指的是 f(l) 里每个元素 y
当 l = 3,是个元素,那么 f(l) = [3], y 遍历 3
当 l = 4,是个元素,那么 f(l) = [4], y 遍历 4


总结

数据类型分两种:
单独类型:整型、浮点型、布尔型
容器类型:字符、元组、列表、字典、集合

条件语句 (if, if-else, if-elif-else, nested if) 是为了在不同条件下执行不同操作,而迭代循环 (while, for) 是重复的完成相同操作。

函数包括正规函数 (用 def) 和匿名函数 (用 lambda),函数的参数形态也多种多样,有位置参数、默认参数、可变参数、关键字参数、命名关键字参数。匿名函数主要用在高阶函数中,高阶函数的参数可以是函数 (Python 里面内置 map/filter/reduce 函数),返回值也可以是参数 (闭包、偏函数、柯里化函数)。

解析式并没有解决新的问题,只是以一种更加简洁,可读性更高的方式解决老的问题。解析式可以把「带 if 条件的 for 循环」用一行程序表达出来,也可以实现 map 加 filter 的功能。

按照 Python 里「万物皆对象」的思路,学习每一个对象里面的属性 (attributes) 和方法 (methods),你不需要记住所有,有个大概印象有哪些,通过 dir() 来锁定具体表达式,再去官网上查询所有细节。这么学真的很系统而简单。此外学的时候一定要带着“它的优缺点是什么”这样的问题,所有东西都有 trade-off,一个满身都是缺点的东西就没有存在的必要,既然存在肯定有可取之处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,719评论 0 10
  • Python标识符 在 Python 里,标识符有字母、数字、下划线组成。在 Python 中,所有标识符可以包括...
    tianyi6677阅读 1,080评论 0 3
  • 1.基本使用 1.1 数据类型 常用数据类型 Common Data Types 其他类型 Others 1.2 ...
    suwi阅读 1,326评论 0 3
  • 周末没有看自虐组指定的书籍,读了寂静法师写的《让孩子成才的秘密》,读到最后发现一个问题是——孩子成才与否,跟孩子没...
    定平sunny阅读 230评论 0 0
  • 除我之外 都是外面 在里面 我与我 每日每日矛盾又简单的生活着 一个人 也很精彩 走出去 看向外面 人与人 车与车...
    守夜日记阅读 106评论 0 2