0%

Python学习

Python学习

学习自廖雪峰的博客网站python篇

Pycharm内容

ctrl+d复制当前行,ctrl+y删除当前行,shift+enter换行(当前行任意位置均可换行),ctrl+/批量注释或批量取消注释,Tab键和shift+Tab键完成批量缩进和取消缩进,ctrl+f查找,ctrl+r替换,ctrl+'+'/'-'折叠或者展开代码或ctrl+shift+'+'/'-'全部折叠或展开代码。设置->KeyMap设置快捷键。#TODO 记录要做的事情。

小Tips

转义字符——除了用\外,还可以用r' '代表单引号内的内容不转义,特殊的%%表示%
多行写代码——用'''...'''的格式表示多行字符,并且还能在前面加上r一起使用表示内部内容不转义。
python除法——/默认是结果为浮点数,而//则默认会去掉小数部分保留整数。
字符编码——ASCII码(单字节)->Unicode码(通常双字节)->utf-8编码(可变长编码),计算机中编码:内存统一使用Unicode编码,需要保存到硬盘或者需要传输时则转为utf-8,用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件,浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。
格式化输出——利用%来实现,和c语言类似。例如print('hello %s %d' % ('world',4))就会输出hello world 4如果只有一个格式化输出,则不用添加括号。在%d前面添加数字代表空格数或者%f前加.和数字控制小数位数,若不确定要格式化输出的内容可以用%s代替,会将所有内容转换为字符串输出。还有一种格式化方法是使用format()方法,它会用传入的参数依次替换字符串内的占位符{0}{1}……,不过这种方式写起来比%要麻烦得多,如'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125),输出Hello, 小明, 成绩提升了 17.1%
条件判断——可用elif表示else if,同时如果if后面只有一个变量就和C语言类似。
IO——input()的返回类型为str,可用int()转换为整数型,float()转换为浮点数类型。
不可变对象——对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

全局变量——在函数中想要修改全局遍历,如果是可变类型,则可以直接进行修改,如果是不可变类型,则需要使用global声明才可修改。

Python基础

字符串

python3以unicode编码,即支持多种语言的字符串。
对于单个字符(英文或中文),可用ord()chr()函数分别读取字符的整数表示和把编码转换为字符。还可以用十六进制表示其他语言如:'\u4e2d\u6587'等价于中文
对于bytes类型的数据用带b前缀的单引号或双引号表示,如x = b'ABC',bytes类型的数据每个字符占用一个字节。用unicode编码的字符串可以通过encode()函数编码为指定的bytes,内部参数为ascii或者utf-8等其他编码方式,反过来就是使用decode()函数,如果bytes中只有一小部分无效的字节,可以传入errors='ignore'。纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes
对于str字符串可用len()函数计算字符数,如果换为bytes类型,则计算字节数。为了避免乱码问题,我们应该坚持使用utf-8来对strbytes进行转换。

list

Python内置的数据类型——列表list,用[]表示。有序的集合,len()获取长度,可用下标访问元素(从0开始的),也可以使用-1作为下标来直接访问最后一个元素,以此类推用-2,-3等来获取倒数第几个元素。可使用append()方法在列表最后添加元素,或者使用insert(int index, Object obj)方法来添加元素到指定位置;使用pop()方法删除列表末尾的元素或者添加参数index来指定下标;如果需要替换可以直接通过str[i]="..."来实现,并且list内部的各个元素的数据类型也是可以不相同的,比如可以就是list类型。

tuple

Python内置的另一种有序列表类型——元组,用()表示。和list不同,tuple一旦初始化就不能更改,也没有append(),insert()这样的方法,但是可以通过下标访问元素,不过不能替换,也更加安全,还有一个需要注意的就是如果定义的时候只有一个元素需要加上一个,来避免歧义,t=(1,),因为如果不加,t就是一个值为1的整型变量了。不过,如果tuple中存在list类型的元素,这个就list中的元素就可以改变了,从而可以理解为tuple也是”可变“的。

循环

首先是for..in循环,依次把list,tuple中的元素打印出来,例如for x in [1,2,3,4,5]:;或者使用range(x)表示从0开始到小于x的整数序列for x in range(5):,还可以使用list()range()转换为list类型。

dict

Python内置的字典类,其他语言也叫map,采用键值对存储方式存储。可通过d['key']查询对应的值,还可通过该方法放入键值对。一个key对应一个value,若对一个关键字放入多个值,则后面放入的会把前面放入的冲掉。如果key值不存在,字典会报错,为了避免,可使用'key' in d来查看key是否存在于字典中,若不存在则返回False;或者使用get()方法,如果不存在会返回None,或者返回自己指定的value,还可以使用pop('key')方法删除键值对。最重要的是key对象必须是不可变对象。

set

和dict类似,也是一组key的集合,只是不存储value值,也同样不能重复。通过add()remove()方法实现添加或者删除key,set可以看作数学上的集合,可以进行∩、∪等操作。并且要创建一个set,需要提供一个list作为输入集合,s = set([1, 2, 3])

函数

常用函数

max()——可以从传入任意个参数,返回最大的那个。
str(),int(),bool()等——数据类型转换函数。
hex()函数——把一个整数转换成十六进制表示的字符串。

函数名实际是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”。即a = abs,然后可以用a()来实现abs()的功能。
如果想要保证传入函数的参数类型是正确的,可用isinstance()方法判断是否参数类型满足你所定义的参数类型。
函数是可以返回多个值的,可以用多个变量来接收,也可以用一个,因为返回的实际上是一个tuple

默认参数

def f(x,n=2)这里就给n设置了一个默认参数,如果调用函数f()时只传入了一个参数x,则会默认传入一个等于2的n进函数,这样就比较方便,例如f(5)实际上就是f(5,2)。不过设置默认参数的时候也有几个需要注意的地方:第一是必选参数在前,默认参数在后,第二是默认参数必须指向不变对象。如果指向一个list,则每次调用时都会改变list里的元素从而导致函数无法发挥作用。可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。

在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:def calc(numbers)def calc(*numbers)前者调用需要传入list或tuple,calc([1,2,3])而后者则不需要,calc(1,2,3)。同时可变参数还可传入一个list或tuple进函数,不过需要添加*号,例如nums = [1,2,3]calc(*nums)即可,*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
# 不传入关键字参数
>>> person('Michael', 30)
name: Michael age: 30 other: {}
# 传入任意个数的关键字参数
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
# 和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数
1
2
3
4
5
6
7
8
9
10
11
12
# 如果要限制关键字参数的名字,就可以用命名关键字参数
# 例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job):
print(name, age, city, job)
# 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
# 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错,即需要写上city='..',job='..'
# 由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。
# 命名关键字参数可以有缺省值,从而简化调用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。比如定义一个函数,包含上述若干种参数:

1
2
3
4
5
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。

1
2
3
4
5
6
7
8
9
10
>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

最神奇的是通过一个tuple和dict,你也可以调用上述函数:

1
2
3
4
5
6
7
8
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

参数小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
要注意定义可变参数和关键字参数的语法:
*args是可变参数,args接收的是一个tuple;
**kw是关键字参数,kw接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))
关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})
使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

递归

尾递归优化——尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。python未提供该优化。

1
2
3
4
5
6
7
8
9
# 上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
# 可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身,`num - 1`和`num * product`在函数调用前就会被计算,不影响函数调用。

高级特性

切片

正向切片——L[0:3],从索引0开始取到索引3为止且不包含索引为3的元素,如果第一个索引为0,还可以省略,即L[:3]
倒数切片——L[-10:-1],即取从倒数第十到倒数第一的元素且不包含最后一个元素,如果想取最后一个元素,则L[-10:]即可。
设置间隔——L[0:10:2],即取前十个数且每两个数取一个,或者L[::5],取所有,每5个取一个。如果只写:则复制原有的list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 测试
# 利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()方法:
# 自己写的
def trim(s):
l = len(s)
end = 0
front = l
for i in range(l):
if s[i] != ' ':
front = i
break
for i in range(l - 1, -1, -1):
if s[i] != ' ':
end = i
break
if front > end:
return ""
return s[front:end+1]
# 网上的一些
def trim(s):
if 0==len(s):
return s

while ' '==s[0]:
s=s[1:]
if 0==len(s):
return s

while ' '==s[-1]:
s=s[:-1]
if 0==len(s):
return s
return s
迭代

只要是可迭代对象,就可以通过for进行迭代,用dict举例

1
2
3
4
5
6
for key in d:
for value in d.values():
for k, v in d.items():
# 如何判断是否可以迭代呢
from collections import Iterable
isinstance('abc', Iterable) # 判断是否可迭代
列表生成式

用来生成list,例list(range(1,11)),并且可以和if,for配合使用。

1
2
3
4
5
6
7
[x * x for x in range(1, 11)]  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[x * x for x in range(1, 11) if x % 2 == 0] # [4, 16, 36, 64, 100]
[m + n for m in 'ABC' for n in 'XYZ'] # ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
d = {'x': 'A', 'y': 'B', 'z': 'C' }
[k + '=' + v for k, v in d.items()] # ['y=B', 'x=A', 'z=C']
L = ['Hello', 'World', 'IBM', 'Apple']
[s.lower() for s in L] # ['hello', 'world', 'ibm', 'apple']

可见,在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else

生成器

generator一边循环,一边计算,可以把列表生成式的[]改为()即可,可以用next(g)来打印生成器的下一个值,在没有元素时会抛出异常。也可以通过for来迭代从而避免异常。定义generator的另一种方法,如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。

生成器每次停止是在遇到yield关键字停止,下一次开始也同样从这里开始。调用generator时,需要先生成一个generator对象,o=odd() # odd是一个generator,同样使用for来迭代:for n in odd(),但是使用for语句会拿不到generatorreturn语句的值,需要使用错误捕获来获得。

1
2
3
4
5
6
7
def triangles():
list = [1]
while True:
yield list[:]
list.append(0)
for index,value in enumerate(list[:-1]):
list[index+1] += value
迭代器

isinstance([], Iterable)判断是否是Iterable对象。可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator。把listdictstrIterable变成Iterator可以使用iter()函数。Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。所以list等就不是Iterator

函数式编程

map/reduce

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

1
2
3
4
5
6
7
def f(x):
return x * x

r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列
# 因此通过list()函数让它把整个序列都计算出来并返回一个list。
list(r) # [1, 4, 9, 16, 25, 36, 49, 64, 81]

reduce()也接受两个参数,一个是函数,一个Iterablereduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
from functools import reduce
def add(x, y):
return x + y

reduce(add, [1, 3, 5, 7, 9]) # 25
# 利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。
# 输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']:
def normalize(name):
return name[:1].upper() + name[1:].lower()
# str2float
def str2float(s):
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '.': -1}
def f(x):
return DIGITS[x]
L = list(map(f, s))
index = L.index(-1)
def r(x, y):
return x *10 + y
x1 = reduce(r, L[:index])
x2 = reduce(r, L[index+1:])
return float(str(x1)+'.'+str(x2))
filter

filter()函数用于过滤序列。和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。且filter()返回的是Iterator,惰性序列,需要使用list()来生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 用于生成素数

# 生成器-从3开始的奇数序列
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
# 筛选函数
def _not_divisible(n):
return lambda x: x % n > 0
# 生成素数序列的生成器
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
sorted

Python内置的sorted()函数就可以直接对list进行排序(默认从小到大),此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序。key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

1
2
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
# ['Zoo', 'Credit', 'bob', 'about']
返回函数
1
2
3
4
5
6
7
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum

返回的是一个函数,并且是在调用返回的函数f时,才会真正开始计算。且每次调用lazy_sum()返回的都是不同的函数,即使函数相同。

闭包——我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中的程序结构,就称为闭包。需要注意,返回的函数不是立即执行,而是在函数调用时才会执行。所以返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

1
2
3
4
5
6
7
8
9
# 每次返回递增整数的函数
def create_counter:
l = [0] # 这里使用int类型会报错 如果使用l = 0,后面l += 1,则会报错
def counter:
l[0] += 1
return l[0]
return counter
# 原因:不可变类型所指向的地址随值的变化而变化,即l=0指向一个地址,而l+=1,l=1后,指向其他地址
# 而对于可变对象,则其指向的地址不会随值变化而变化,l[0]对应的地址一直不会变,只是该地址对应的值在变
匿名函数

lambda关键字表示匿名函数,匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。

1
2
3
4
lambda x: x * x # def f(x): return x * x 
# 也可以没有参数,即和函数没有参数一样的道理,例子:
def f(x, y)
return lambda: x + y
装饰器

函数有一个_name_属性,可以通过该属性拿到函数的名字。在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。即不改变函数定义,增加功能。装饰器接受函数作为参数,且使用装饰器时,要借助Python的@语法,把decorator置于函数的定义处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 装饰器
import functools

def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

@log
def now():
print('2015-3-25')
# 即表示now = log(now)
# 且now._name_ = wrapper

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import functools

def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

@log('execute')
def now():
print('2015-3-25')
# 即now = log('execute')(now)

在oop设计模式中还有一些应用。

偏函数

functools.partial帮助我们创建一个偏函数,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。但是创建完后,也仍然可以传递参数来改变功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import functools

int2 = functools.partial(int, base=2)
int2('1000000') # 64
int2('1000000', base=10) # 1000000

# 实际上固定了int()函数的关键字参数base,也就是:
int2('10010')
# 相当于
kw = { 'base': 2 }
int('10010', **kw)

max2 = functools.partial(max, 10)
max2(5, 6, 7)
# 相当于
args = (10, 5, 6, 7)
max(*args)

模块

使用模块
1
2
3
4
5
6
7
8
9
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

if __name__ == '__main__':
# xxxxxx

第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码。第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释。第6行使用__author__变量把作者写进去。

sys模块中的agrv变量,即命令行参数,第一个参数是该.py文件的名称,argv是一个list

当我们在命令行运行模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

模块的作用域——在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。类似_xxx__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc__abc等;

1
2
3
# example
def _private_1(name):
return 'Hello, %s' % name
模块搜索路径

当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错。默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中,如果我们要添加自己的搜索目录,有两种方法。

1
2
3
4
5
6
7
# 一是直接修改sys.path,添加要搜索的目录:
import sys
sys.path.append('/Users/michael/my_py_scripts')
# 这种方法是在运行时修改,运行结束后失效。

# 第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。
# 设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。

面向对象编程

类和实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(Object):
def __init__(self, age, score):
self.age = age
self.score = score
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'

Jack = Student() # 实例
Jack.name = jack # 添加属性

定义类,用class关键字,Object即该类的父类。创建实例即类名+(),还可以自由地给一个实例变量绑定属性,比如,给实例Jack绑定一个name属性。__init__方法可以理解为构造方法,可以绑定需要的属性。且__init__方法的第一个参数永远为self,表示创建的实例本身。和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

访问限制

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。

1
2
3
4
5
class Student(Object):
def __init__(self, age, score):
self.__age = age
self.__score = score
# 无法从外部访问实例变量.__age和实例变量.__score

如果想要访问相应的私有变量,可以通过gettersetter方法来获得和修改私有变量。还有需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____score__这样的变量名。

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

1
Jack._Student__name # jack

最后注意下面的这种错误写法

1
2
3
4
5
6
7
8
bart = Student('Bart Simpson', 59)
bart.get_name()
# 'Bart Simpson'
bart.__name = 'New Name' # 设置__name变量!
bart.__name
# 'New Name'
bart.get_name() # get_name()内部返回self.__name
# 'Bart Simpson'

表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。

继承和多态
1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')

def run_twice(animal):
animal.run()
animal.run()

在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行。

多态的开闭原则:1)对扩展开放:允许新增Animal子类 2)对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

静态语言和动态语言——对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了

1
2
3
4
5
class Timer(object):
def run(self):
print('Start...')

# 这里没有继承于Animal 但是还是可以当作Animal类型的类传入,因为其具有run()方法

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

获取对象信息

判断对象类型,使用type()函数

1
2
3
4
5
type(123) # <class 'int'>
type('str') # <class 'str'>
type(None) # <type(None) 'NoneType'>
type(abs) # <class 'builtin_function_or_method'>
type(a) # <class '__main__.Animal'>

可以在if语句中判断两个变量的类型是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type(123)==type(456) # True
type(123)==int # True
type('abc')==type('123') # True
type('abc')==str # True
type('abc')==type(123)# False
# 还可以使用types模块中来判断函数、生成器、对象等等
import types
def fn():
pass

type(fn)==types.FunctionType # True
type(abs)==types.BuiltinFunctionType # True
type(lambda x: x)==types.LambdaType # True
type((x for x in range(10)))==types.GeneratorType # True

还可以使用isinstance()来判断类的继承等关系,而不建议使用type()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# object -> Animal -> Dog -> Husky
a = Animal()
d = Dog()
h = Husky()
isinstance(h, Husky) # True
isinstance(h, Dog) # True
isinstance(h, Animal) # True
# isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
isinstance(d, Husky) # False
# 能用type()判断的基本类型也可以用isinstance()判断:
isinstance('a', str) # True
# 并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
isinstance([1, 2, 3], (list, tuple)) # True
isinstance((1, 2, 3), (list, tuple)) # True

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

1
2
dir('ABC')
# ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

1
2
3
len('ABC') # 3
'ABC'.__len__() # 3
'ABC'.lower() # abc

同样如果是我们自己写的类,如果也想用len(myObj)的话,也可以自己写一个__len__()方法。还有就是lower()普通方法。

仅仅把属性和方法列出来是不够的,配合getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x

obj = MyObject()

紧接着,可以测试该对象的属性:

1
2
3
4
5
6
7
hasattr(obj, 'x') # 有属性'x'吗? True
obj.x # 9
hasattr(obj, 'y') # 有属性'y'吗? False
setattr(obj, 'y', 19) # 设置一个属性'y'
hasattr(obj, 'y') # 有属性'y'吗? True
getattr(obj, 'y') # 获取属性'y' 19
obj.y # 获取属性'y' 19

如果试图获取不存在的属性,会抛出AttributeError的错误:

1
2
3
4
getattr(obj, 'z') # 获取属性'z'
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# AttributeError: 'MyObject' object has no attribute 'z'

还可以传入一个default参数,如果属性不存在,就返回默认值:

1
2
getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
# 404

也可以获得对象的方法:

1
2
3
4
5
6
7
hasattr(obj, 'power') # 有属性'power'吗? True
getattr(obj, 'power') # 获取属性'power'
# <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
fn # fn指向obj.power
# <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
fn() # 调用fn()与调用obj.power()是一样的 81

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接使用类名.属性的形式来进行。

一个正确的用法的例子如下:

1
2
3
4
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
实例属性和类属性

实例属性归属于类本身,但是类的实例也可以像访问属性一样访问,同时还可以通过类名.类属性名来访问。

需要注意的是,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。