5

《Python基础教程》笔记

 2 years ago
source link: https://xiaochai.github.io/2020/06/28/beginning-python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Back to Posts
Jun 28, 2020

《Python基础教程》笔记

Posted in Reading

行结尾可加可不加分号;

除法运算的结果为浮点数;

执行整数除法可以使用//操作符;

乘方运算符**

>>> 1//2
0
>>> 1/2
0.5
>>> (-2)**2
4

十六进制使用0x开头,八进制使用0o开头,二进制使用0b开头:

>>> 0xF
15
>>> 0o10
8
>>> 0b10
2

Python中变量没有默认值,所以使用前必须赋值;

以下示例是input、print、pow、abs、round等内置函数使用:

>>> x = input("The square of: ")
The square of: 10
>>> int(x) ** 2
100
>>> print("Hello world!")
Hello world!
>>> print(2**2)
4
>>> pow(2,2)
4
>>> abs(-199)
199
>>> round(123.12)
123
>>> round(123.72)
124

使用import导入模块:

>>> import math
>>> math.sqrt(9)
3.0
>>> from math import sqrt
>>> sqrt(25)
5.0

Python本身对复数有支持,cmath库对复数进行扩展:

>>> 1j*1j
(-1+0j)
>>> import cmath
>>> cmath.sqrt(-1)
1j

海龟绘图小示例,使用turtle库(本身依赖于tkintery库)画一个正方形,其中forward前进n个像素点,left函数为方向逆时针旋转n度:

from  turtle import *

forward(100)
left(90)
forward(100)
left(90)
forward(100)
left(90)
forward(100)

input("press enter to continue")

Python的注释以#开头;

Python使用单引号和双引号都可以表示字符串,字符串拼接使用+运算符;另外,直接把两个字符串放一起,也可以实现拼接:

>>> a="Let's say " '"Hello world!"'
>>> a
'Let\'s say "Hello world!"'

repr函数配合print使用可保留原始字符串的样子:

>>> print(repr("Hello,\nworld"))
'Hello,\nworld'
>>> print(str("Hello,\nworld"))
Hello,
world

长字符串可以使用三引号(引号使用'"都行);反斜杠换行;原始字符串表示(以r开头,注意最后一个字符不能是反斜杠):

>>> print('''That's great, like a "boss"!''')
That's great, like a "boss"!
>>> print("Hello,\
... world!")
Hello,world!
>>> print(r"C:\Program Files")
C:\Program Files

unicode的支持,以及编码的转换,以及使用b开头的字符串可以直接转化成bytes对象,以及可变bytes bytearray:

>>> print("\N{cat}\U0001F60A\u00C6")
🐈😊Æ

>>> "Hello".encode("ASCII")
b'Hello'
>>> "Hello".encode("UTF-8")
b'Hello'
>>> "Hello".encode("UTF-32")
b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00'

>>> "Hello\N{cat}".encode("UTF-8")
b'Hello\xf0\x9f\x90\x88'
>>> "Hello\N{cat}".encode("ASCII")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\U0001f408' in position 5: ordinal not in range(128)
>>> "Hello\N{cat}".encode("ASCII", "replace")
b'Hello?'
>>> "Hello\N{cat}".encode("ASCII", "ignore")
b'Hello'

>>> x = bytearray(b'Hello')
>>> x[1] = ord(b'c')
>>> x
bytearray(b'Hcllo')

Python中默认使用UTF-8编码。

列表与元组

Python中的容器主要有序列(元组、列表等)、映射(字典等)、集合。

列表可修改,元组不可修改:

>>> edward = ['Edward Gumby', 42]
>>> john = ['John Smith', 50]
>>> database = [edward, john]
>>> database
[['Edward Gumby', 42], ['John Smith', 50]]
>>> ['rd']*2 + ['rs']
['rd', 'rd', 'rs']

序列的索引从0开始,可使用负数从最后往前数,-1表示最后一个元素。

字符串也是一个序列,Python中没有字符的概念,所以str[0]表示只有str第一个字符表示的字符串。

>>> tag = '01234567'
>>> tag[1:3]
'12'

切片使用冒号来提取特定范围内的元素,冒号前后表示对应的索引,最终结果包含第一个索引,而不包含第二个索引;

索引可以是负数,也可以省略,省略第一个表示从头开始,省略第二个表示直到最后一个元素(包含);

还可以使用步长来间隔获取元素,负数的步长表示从右往左提取:

>>> tag[1:-1]
'123456'
>>> tag[:]
'01234567'
>>> tag[:-1]
'0123456'
>>> tag[-3:-1]
'56'
>>> tag[-3:]
'567'

>>> tag[::2]
'0246'
>>> tag[-2::-2]
'6420'

乘法操作可以复制序列元素,加法可以拼接两个序列,另外在Python中使用None关键字表示什么也没有;

in运算符用于判断指定元素是否在序列中,注意对于两个字符串使用in操作,表示的含义就变成检查子串了:

>>> 'ml' in ['mlh', 'abc']
False
>>> 'mlh' in ['mlh', 'abc']
True
>>> 'ml' in 'mlh'
True

获取长度,最小值,最大值:

>>> len([1,2,3])
3
>>> max(32,2,99)
99
>>> max([32,2,99])
99
>>> min([32,2,99])
2

使用list类创建列表,列表是可以修改的,所以可以进行赋值,还有其它操作:

>>> a = list("Hello")
>>> a
['H', 'e', 'l', 'l', 'o']
>>> a[1] = 'a'
>>> ''.join(a)
'Hallo'

del a[1]: 删除元素;

给切片赋值可以实现删除,插入,替换等多种功能;

append:追加元素;

clear:清空列表;

copy:复制列表,如果正常赋值只是做一个关联,复制需要使用copy;

count:计算元素出现的个数;

extend:与加法拼接类似,只是会修改第一个列表值;

index:查找对应的元素,未找抛出异常,找到返回索引;

insert:插入对象到列表;

pop:从列表中删除参数指定的元素,默认为最后一个元素;

remove:删除第一个指定值的元素;

reverse:原地反转列表;

sort:原地排序;

sorted:并非list上的函数,而是将list做为参数传入,返回排序后的副本;

以上的sort和sorted支持key和reverse两个参数:

key:是一个函数,用于对元素生成一个关键字,按这个关键字排序;
reverse:接受bool类型值,表示是否倒序排列:

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

>>> a.append(8)
>>> a
[1, 6, 7, 8]
>>> b = a.copy()
>>> a.clear()
>>> a
[]
>>> b
[1, 6, 7, 8]

>>> b.count(8)
1
>>> b.extend([8,9,10])
>>> b.count(8)
2

>>> b.index(8)
3
>>> b.pop(3)
8
>>> b.index(8)
3
>>> b.reverse()
>>> b
[10, 9, 8, 7, 6, 1]

>>> b.sort()
>>> b
[1, 6, 7, 8, 9, 10]
>>> sorted(b, reverse=True)
[10, 9, 8, 7, 6, 1]

元组与列表类似,只是无法修改元组的值,元组的切片还是元组。tuple可以将序列转为元组。

>>> 1,2,3
(1, 2, 3)
>>> 1,
(1,)
>>> 3*(1+2,)
(3, 3, 3)
>>> x = 1,2,3
>>> x[0:1]
(1,)
>>> tuple('abc')
('a', 'b', 'c')
>>> tuple([1,2,3])
(1, 2, 3)

使用字符串

格式化输出

百分号运算符除了可以取模运算,还可以用于格式化输出字符串:

>>> "Your name is %s, age :%d\n" % ("Lee", 12)
'Your name is Lee, age :12\n'

这里的%s%d与C语言中的print函数类似,还可以使用%.3f这样的表示来获取指定位数的小数。

还可以使用Template的方式来处理占位符的替换;

或者使用{}这样的方式配合format函数使用,更加灵活(如果需要输出共括号时,使用两个花括号);

如果变量与替换字段同名,可以在字符串前加f轻松处理:

>>> from string import Template
>>> Template("Hello $name, welcome to $city").substitute(name="Lee", city="China")
'Hello Lee, welcome to China'


>>> "{3} {0} {2} {1} {3} {0}: {percent:.2f} }} {{ {arr[1]:03d}".format("be", "not", "or", "to", percent=0.324223432442343424, arr=[1,2,3])
'to be or not to be: 0.32 } { 002'
>>> "Hello {name} , welcome to {country}!".format_map({"name" : "Lee", "country":"China"})
'Hello Lee , welcome to China!'

>>> from math import pi
>>> f"pi is {pi:2.6f}"
'pi is 3.141593'

使用花括号的格式化还支持其它的语义,例如在变量后跟!s,!r, !a表示使用str、repr和ascii进行转换。更多的资料可以参考这里

>>> "{str!s} {str!r} {str!a}".format(str="ok!\n")
"ok!\n 'ok!\\n' 'ok!\\n'"

字符串方法

string模块中定义了很多字符串的操作方法,但大部分在新版本的Python中都迁移到字符串方法上了,所以一般不再使用string模块中的方法,但有一些常用的常量:

>>> import string
>>> string.digits
'0123456789'
>>> string.ascii_letters
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>> string.printable
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
>>> string.ascii_uppercase
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

这里列举了一些字符串常用方法,更多的可以参考这里

center:将字符串以给定长度居中显示;

find:在指定的范围内查到对应子串,返回位置,不存在返回-1,如果只需要判断是否包含字串,使用in操作符;

join:对字符串序列进行合并;

split:对字符串进行分割,可指定最多分割数量;

lower:字符串转小写;

replace:字符串替换;

strip:与php的trim一样,去掉头尾的空格或者指定字符;

translate:单字母替换指定字符,并可以删除指定字符;

isspace/isdigit/isupper:这一系列的is函数用于判断字符串是否满足指定规则:

>>> "abc".center(11)
'    abc    '
>>> "abc".center(10, '-')
'---abc----'
>>> "abc".center(1, '-')
'abc'

>>> "i am ok ".find("i ")
0
>>> "i am ok ".find("i m")
-1
>>> "i am ok ".find("i", 3, 5)
-1
>>> "i am ok ".find("am", 1, 5)
2

>>> "+".join(["1","2"])
'1+2'
>>> "+".join(("1","2"))
'1+2'
>>> "+".join((1,2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found
>>> "1 2 3 4".split(" ")
['1', '2', '3', '4']
>>> "1 2 3 4".split(" ",2)
['1', '2', '3 4']

>>> "i am ok".replace("ok", "bad")
'i am bad'

>>> table = str.maketrans('cs', 'kz', ' ')
>>> 'this is an incredible test'.translate(table)
'thizizaninkredibletezt'

创建字典以及对应的操作,其中键的类型可以是任何不可变的类型,如字符串,元组,浮点数等:

>>> {"Alice":10, "Bob":20}
{'Alice': 10, 'Bob': 20}
>>> dict([["Alice", "Bob"],[10,20]])
{'Alice': 'Bob', 10: 20}
>>> dict(Alice=10, Bob=20)
{'Alice': 10, 'Bob': 20}

>>> d =  dict(Alice=10, Bob=20)
>>> d["Alice"]
10
>>> d["Lee"] = 21
>>> d
{'Alice': 10, 'Bob': 20, 'Lee': 21}
>>> del d["Lee"]
>>> d
{'Alice': 10, 'Bob': 20}
>>> "Lee" in d
False
>>> "Bob" in d
True

常用的方法如下,更多查看这里

clear:清空字典;

copy:复制字典;

fromkeys:创建并返回一个包含给定键的字典,可以设置默认值;

get:获取某一个键的值,与直接下标访问的区别是如果不存在,get会返回None,而下标访问会报错;

items:返回二元组的一个列表,包含了所以键值对,这个函数返回内容只是字典的另一个视图,不会进行复制;

keys:返回一个字典视图,其中包含指定字典中的键;

pop:删除指定key的元素,并返回,可指定没有这个元素时的默认值;

popitem:随机弹出一个键值项;

setdefault:如果某一个key不存在,则设置上某值;

update:更新值;

values:返回一个字典视图,其中包含指定字典中的值,可重复:

>>> d =  dict(Alice=10, Bob=20)
>>> d.copy()
{'Alice': 10, 'Bob': 20}
>>> e = d.copy()
>>> d.clear()
>>> d
{}
>>> e
{'Alice': 10, 'Bob': 20}
>>> e.get('abc')
>>> e['abc']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'abc'

>>> e.fromkeys(["Lee","Chen"])
{'Lee': None, 'Chen': None}
>>> e
{'Alice': 10, 'Bob': 20}
>>> dict.fromkeys(["Lee","Chen"])
{'Lee': None, 'Chen': None}

>>> e.items()
dict_items([('Alice', 10), ('Bob', 20)])
>>> it = e.items()
>>> len(it)
2
>>> e["Bob"] = 21
>>> it
dict_items([('Alice', 10), ('Bob', 21)])

>>> e.keys()
dict_keys(['Alice', 'Bob'])
>>> e.pop("Lee", 22)
22

>>> e.popitem()
('Bob', 21)
>>> e
{'Alice': 10}

print支持更多的参数,可以打印多个参数,并指定分隔符和结束符号,而import也可以为引入的包创建别名:

>>> print("Hello", "world", "my", "friend", sep = ', ', end=" -_- \n");
Hello, world, my, friend -_-

>>> import math as myMath
>>> myMath.sqrt(9)
3.0
>>> from math import sqrt as mySqrt
>>> mySqrt(9)
3.0

序列解包,可以使用*来收集多余的值:

>>> x,y,z = 1,2,3
>>> x,y = y,x
>>> x,y,z
(2, 1, 3)
>>> key,val = {"Bob":12, "Alice":11}.popitem()
>>> (key,val)
('Alice', 11)

>>> x,*y,z = 1,2
>>> x,y,z
(1, [], 2)
>>> x,*y,z = 1,2,3,4,5
>>> x,y,z
(1, [2, 3, 4], 5)
>>> x,*y,z = 1,2,5
>>> x,y,z
(1, [2], 5)

Python中使用冒号指示接下来是一个代码块,相同缩进的代码被认为同一个代码块,不相同则此代码块结束。

在Python中这些值被认为是假False None 0 "" () [] {}

使用例子:

num = int(input('Enter a number: '))
if num > 0:
    print('The number is positive')
elif num < 0:
    print('The number is negative')
else:
    print('The number is zero')

在Python中有一些特殊的条件,例如支持链式比较,isis not判断两对象是否为一个,使用and/or/not进行布尔运算(短路逻辑),assert断言直接在为假时让程序异常退出:

>>> x=10
>>> 9 < x < 100
True

>>> x = y = [1,2]
>>> z = [1,2]
>>> x is y
True
>>> x is not z
True
>>> x ==  z
True

>>> assert x
>>> assert not x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert not x, x is not empty
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'empty' is not defined
# 直到你输入了非空的名字之后才退出
name = ''
while not name:
    name = input('Please enter your name: ') 
print('Hello, {}!'.format(name))

# 每一行输出一个单词
words = ["this", "is", "a", "man"]
for word in words:
    print(word)

# 输出1到100
for number in range(1,101):
    print(number)

# 对于map的遍历
d = {'x': 1, 'y': 2, 'z': 3}
for key, value in d.items():
    print(key, 'corresponds to', value)
# 输出
# x corresponds to 1
# y corresponds to 2
# z corresponds to 3

# 对于同时需要遍历两个数组的情况,可以使用索引,也可以使用zip拼接成元组之后遍历
names = ['anne', 'beth', 'george', 'damon']
ages = [12, 45, 32, 102]
for i in range(len(names)):
    print(names[i], 'is', ages[i], 'years old')
for name, age in zip(names, ages):
    print(name, 'is', age, 'years old')
# 以上两个都输出
# anne is 12 years old
# beth is 45 years old
# george is 32 years old
# damon is 102 years old

# zip可以拼接做任意个数量,即使两个参数的数量不一致,则按最短的来
print(list(zip([1,2,3],range(0,100), [1,2])))
# 输出 [(1, 0, 1), (2, 1, 2)]

# enumerate可以将序列自动添加索引,在需要对序列遍历并需要索引的时候很管用
list(enumerate([1,2,3]))
# 输出 [(0, 1), (1, 2), (2, 3)]


# reversed返回一个倒序的视图,用于反向遍历
list(reversed([1,2,3]))
# 输出 [3, 2, 1]


# break和continue的用法与其它语言基本一致
while True:
    word = input('Please enter a word: ') 
    if not word:
        break
    print('The word was ', word)


# for可以配合else使用,只有当break没有执行到时,才会执行else下的内容
# 以下这个程序用于判断输入的数字是不是素数
n = int(input('enter a number: '))
for i in range(2,97):
    if n % i == 0:
        print("%d is a not prime number" % (n))
        break
else:
    print("%d is a prime number" % (n))

推导类似于数学上的集合表示方式,与Haskell中的列表内包一致,例如[x*x|x<-[0...10]]表示0~10的平方列表,[x|x<-[0...10],y%3==0]表示0~10内可以被3整数的数,这些表达式使用Python的推导式表示如下:

>>> [x*x for x in range(0, 11)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> [x for x in range(0, 11) if x%3==0]
[0, 3, 6, 9]
>>> [(x,y) for x in range(0, 4) for y in range(100, 102)]
[(0, 100), (0, 101), (1, 100), (1, 101), (2, 100), (2, 101), (3, 100), (3, 101)]

>>> {x:"{} * {} = {}".format(x,x,x*x) for x in range(0,10)}
{0: '0 * 0 = 0', 1: '1 * 1 = 1', 2: '2 * 2 = 4', 3: '3 * 3 = 9', 4: '4 * 4 = 16', 5: '5 * 5 = 25', 6: '6 * 6 = 36', 7: '7 * 7 = 49', 8: '8 * 8 = 64', 9: '9 * 9 = 81'}

可以看到改成使用花括号就可以写出字典的生成器了。

pass:空语句,什么也不做,用于占位,解决Python不支持空语句块的问题;

del:取消变量的引用指向,并不能真正释放值,因为依赖于垃圾回收;

exec:将字符串当成Python脚本来执行,可以指定命名空间;

eval:与exec类似,执行语句,返回结果,而exec是不返回结果的:

>>> from math import sqrt
>>> scope={}
>>> exec("sqrt=1", scope)
>>> sqrt(4)
2.0
>>> scope["sqrt"]
1

>>> scope = {}
>>> scope['x'] = 2
>>> scope['y'] = 3
>>> eval('x * y', scope)
6
>>> exec('x = 4', scope)
>>> eval('x * x', scope)
16
# 基本的函数定义,斐波那契数列
def fibs(num):
    'generate a list of Fibonacci'
    res = [0, 1]
    for i in range(num-2):
        res.append(res[i]+res[i+1])
    return res

print(fibs(10))
# 输出[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

print(fibs.__doc__)
# 输出 generate a list of Fibonacci

help(fibs)
# 以下是输出
# Help on function fibs in module __main__:
# 
# fibs(num)
#     generate a list of Fibonacci


# 如果函数没有返回值,即空的return 语句,则实际会返回None
def noReturn():
    return
print(noReturn())
# 输出None

# 对于传入的参数如果是可变类型,则修改会影响参数的值,可以通过切片(切片生成的序列是复制的)来避免这一点
def modify(a):
    a[0] = 1
b = [0,1,2]
modify(b)
print(b)
# 输出[1,1,2]

# 如果传切片就不会有修改原值
b = [0,1,2]
modify(b[:])
print(b)
# 输出[0,1,2]

# 关键字参数与默认值,以及可变长参数
# 对于有默认值的参数,可以用关键字指定,带*号的变量表示会收集所有无关键字并且未匹配的参数,**表示收集关键字参数
def print_params(x, y, z=3, r=4, *pospar, **keypar): 
    print("x,y,z,r:", x, y, z, r, "\tpospar:", pospar, "\tkeypar", keypar)
# 有默认值的参数并非都要指定,可以跳过
print_params(1,2,r=9)
# 输出 x,y,z,r: 1 2 3 9        pospar: ()      keypar {}

# *和**指定的收集参数,都可以为空
print_params(1,2,z=3,r=4,k1=5,k2=6)
# 输出 x,y,z,r: 1 2 3 4        pospar: ()      keypar {'k1': 5, 'k2': 6}
print_params(1,2,3,4,5,6,7,8,k1=9,k2=10)
# 输出 x,y,z,r: 1 2 3 4        pospar: (5, 6, 7, 8)    keypar {'k1': 9, 'k2': 10}

# 以下调用报错,因为关键字参数必须放在最后
# print_params(1,2,z=100,r=32,433,k=1,k3=2)

# 参数分配,将序列或者字典分配到对应的参数中去
def add(x,y,z):
    return x+y+z
# 以下例子的结果都是6
add(1,2,3)
add(*(1,2,3))
add(*[1,2,3])
add(**{"x":1,"y":2,"z":3})

# 作用域
x = 10
y = 11
def g(y):
    global x
    x = x+1
    y = y+1
g(y)
# 可以看到x已经变了,但y却没有影响
print(x,y)

# 闭包
def f(x):
    def g(y):
        return x+y
    return g
# 每一次调用f()返回的函数,都包含了当时的x值
f1 = f(10)
f2 = f(11)
print(f1(1))
print(f2(1))

# 递归
# 阶乘
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)
# 返回120
print(factorial(5))

# 幂
def power(x, n):
    if n == 0:
        return 1
    else:
        return x * power(x, n-1)
print(power(2,10))

# 二分查找
def search(sequence, number, lower = 0, upper = None):
    if upper is None:
        upper = len(sequence) - 1
    if lower > upper:
        return -1
    mid = int((lower+upper) / 2)
    if sequence[mid] == number:
        return mid
    elif sequence[mid] > number:
        return search(sequence, number, 0, mid-1)
    else:
        return search(sequence, number, mid+1, upper)
l = [1,2,3,4,5,6,7,8,9]
i = search(l, 4, 0, len(l)-1)
print(i) # 输出3
# 类的创建和基本使用
# 这个self是约定命名,可以是任何变量名
class Person:
    def set_name(self, name):
        self.name = name
    def get_name(self):
        return self.name
    def greet(self):
        print("Hello, world! I'm {}.".format(self.name))
p1 = Person()
p1.set_name("Jonh")
p2 = Person()
p2.set_name("Lee")
p1.greet() # Hello, world! I'm Jonh.
p2.greet() # Hello, world! I'm Lee.
print(p1.get_name()) # Jonh

# 可以直接对属性进行设置
p1.name = "Mark"
print(p1.name) # Mark

# 类方法的调用,也可以使用类名的方式调用,我理解在类上的调用应该与此等价
Person.greet(p1) # Hello, world! I'm Mark.


# Python中没有私有属性,可以通过以下方法来达到私有属性的效果
class Sec :
    __name = "J" # 两个下划线开头的属性或者方法,实际上都会添加下划线开头加类名的前缀
    def set_name(self, name):
        self.__name = name
    def get_name(self):
        return self.__name
    def greet(self):
        print("Hello, world! I'm {}.".format(self.__name))

s = Sec()
s.greet() # Hello, world! I'm J.
s.__name = "K"
s.greet() # Hello, world! I'm J.
s._Sec__name = "K" # 双下划线开头的会添加前缀,实际中不推荐这样使用
s.greet() # Hello, world! I'm K.
# 也可以对一个不存在的属性赋值
s.__k  = "s"

# 单下划线也被视为不能直接修改约定,在使用from module import *中不会导入以下划线开头的名称

# 类名空间下的成员是所有对象共享的,也是独立于对象的,如果对象的属性没有定义,则使用类的属性
print(Sec._Sec__name) # J
# 所以类也可以被当成命名空间来使用,因为在类中可以直接执行语句
class C:
    print("Class C being defined")
# 以上输出 Class C being defined


# 继承的基本使用
class Base:
    def init(self):
        self.p = "base"
    def show(self):
        print("I'm ", self.p)

# 在定义类时添加括号表示继承的基类
class Extend(Base):
    def init(self):
        self.p = "extend"

b = Base()
b.init()
b.show() # I'm  base
e = Extend()
e.init()
e.show() # I'm  extend

# 与继承相关的方法
# 是否是继承关系
print(issubclass(Extend, Base)) # True
# 获取基类,由于支持多重继承,所以还一个属性__bases__
print(Extend.__base__) # <class '__main__.Base'>
# object是所有类的基类
print(Base.__base__) # <class 'object'>

# 以下两个都返回True,判断对象是否是某个类
print(isinstance(Extend(), Base))
print(isinstance(Extend(), Extend))
# 获取对象属于哪个类
print(Extend().__class__) # <class '__main__.Extend'>

# 多重继承
# 如果写成Super(Base, Extend)则无法运行,因为不满足MRO,可阅读参考文献
# 所以在多重继承中注意两个超类含有同一个属性或者方法的情况
class Super(Extend, Base): pass
s = Super()
s.init()
s.show() # I'm  extend


# 判断某一个对象是否有某一个属性,以及这个属性是否可调用
print(hasattr(s, "show")) # True
# getattr的第三个参数表示不存在时的默认值,如果不提供默认值且不存在,则报错
print(getattr(s, "p")) # extend
# callable检查是否可调用
print(callable(getattr(s, "show", None))) # True
# 设置对象的属性,返回None
setattr(s, "p1", "p1val")
# 获取所有属性
print(s.__dict__) # {'p': 'extend', 'p1': 'p1val'}

# 抽象基类
# 在Python中并没有提供原生的语法来支持抽象类,但可以使用abc这个官方模块解决此问题
from abc import ABC, abstractclassmethod
class Talker(ABC):
    @abstractclassmethod
    def talk(self): pass
# 以下语句会报错TypeError: Can't instantiate abstract class Talker with abstract methods talk
# t = Talker()
# 如果只是继承了而没有实现抽象方法,还是会报错
class Foo(Talker): pass
# t = Foo()
class Knigget(Talker):
    def talk(self):
        print("Ni")
k = Knigget()
# 一般的抽象类应用场景中会有isinstance判断
if isinstance(k, Talker):
    k.talk() # Ni
# 但这与Python的鸭子类型的编程思想,如果一个类有talk方法,但没有继承Talker,则在此场景中没有办法调用talk

# register提供了将某一类注册为另外一个类子类的办法
class Herring:
    def talk(self):
        print("Blue")
h = Herring()
print(isinstance(h, Talker)) # False
# 将Herring 注册为Talker的子类
Talker.register(Herring)
print(isinstance(h, Talker)) # True
print(issubclass(Herring, Talker)) # True

# 这种做法的问题是,如果Herring没有实现talk方法,则此时调用talk方法会直接报错,如下:
class Clam: pass
Talker.register(Clam)
c = Clam()
if isinstance(c, Talker):
    c.talk() # 这里将报错,因为Clam没有实现talk方法,但又是Talker的子类

使用raise可以抛出异常:

>>> raise Exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception
>>> raise Exception("hyperdrive overload")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: hyperdrive overload

>>> class MyException(Exception): pass
... 
>>> raise MyException("My")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.MyException: My

Exception是几乎所有异常类的基类,raise可以直接跟类名,也可以跟对象。

可以直接继承Exception来定义自己的异常类。

除了Exception异常类,还有以下常用的内置异常类:

AttributeError :引用属性或给它赋值失败时引发;

OSError: 操作系统不能执行指定的任务(如打开文件)时引发,有多个子类;

IndexError : 使用序列中不存在的索引时引发,为LookupError的子类;

KeyError : 使用映射中不存在的键时引发,为LookupError的子类;

NameError : 找不到名称(变量)时引发;

SyntaxError : 代码不正确时引发;

TypeError :将内置操作或函数用于类型不正确的对象时引发;

ValueError : 将内置操作或函数用于这样的对象时引发:其类型正确但包含的值不合适;

ZeroDivisionError:除法或求模运算的第二个参数为零时引发。

# 异常的基本用法
try:
    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: ')) 
    print(x / y)
except ZeroDivisionError: # 如果y为0,则为ZeroDivisionError,走此流程
    print("The second number can't be zero!")
except (Exception, TypeError) as e: # 其它的Exception,走此流程,并捕获了异常的对象,将其打印出来,这里的TypeError多余,只是为了显示一次except可以捕获多个异常类型
    print("Other error occur: ", e)
except as e: # 其它非Exception的异常走此流程,例如Ctrl+C中止程序的异常(KeyboardInterrupt),他的基类是BaseException(这类也是Exception的基类)
    print("not Exception occur")
else: # 如果没有异常,会执行此语句块
    print("Calculate complete")
finally: # 无论是否有异常,都会执行此语句块,一般用于做清理工作,例如连接的关闭等
    print("clear")

有时候需要捕获异常之后记录日志并重新抛出异常,可以使用无参数的raise:

try: 1/0
except ZeroDivisionError as e:
    print("The second number can't be zero!")
    raise
# The second number can't be zero!
# Traceback (most recent call last):
#   File "test.py", line 1, in <module>
#     try: 1/0
# ZeroDivisionError: division by zero

try: 1/0
except ZeroDivisionError as e:
    print("The second number can't be zero!")
    raise ValueError
# 如果引发了别的异常,则在原先的异常会被存储在异常上下文中,在最终输出时体现出来
# 这里也可以写成raise ValueError from e
# The second number can't be zero!
# Traceback (most recent call last):
#   File "test.py", line 1, in <module>
#     try: 1/0
# ZeroDivisionError: division by zero
# 
# During handling of the above exception, another exception occurred:
# 
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     raise ValueError
# ValueError

try: 1/0
except ZeroDivisionError as e:
    print("The second number can't be zero!")
    raise ValueError from None
# 如果需要禁用上下文,可以使用from None
# The second number can't be zero!
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     raise ValueError from None
# ValueError

warning包提供了一些警告相关的工具,他们会在控制台输出警告信息而不打断程序的运行。

此包也提供了对这些警告的控制,例如过滤某些警告,将某些警告上升为异常等等。

更多内容可以参考

from warnings import warn,filterwarnings
warn("I've got a bad feeling about this.")
print("I'm ok")
# test.py:2: UserWarning: I've got a bad feeling about this.
#   warn("I've got a bad feeling about this.")
# I'm ok

filterwarnings("ignore")
warn("I've got a bad feeling about this.") # 不再输出

filterwarnings("error")
warn("I've got a bad feeling about this.") # 直接抛出异常

魔法方法、特性和迭代器

class FooBar:
    # 构造函数
    def __init__(self, value = 42):
        self.somevar = value
    # 析构函数,在垃圾回收时被调用,但这个调用的时机很难把握
    def __del__(self):
        self.somevar = 1111
        print("del", self.somevar)
print(FooBar().somevar)
print(FooBar("hhh").somevar)
# 以下是这个输出,很奇怪的是析构函数的输出早于正常输出
# del 1111
# 42
# del 1111
# hhh

# 函数重写
class Bird:
    def __init__(self):
        self.hungry = True
    def eat(self):
        if self.hungry:
            print("Aaaah..")
        else:
            print("No, thanks")
class SongBird(Bird):
    def __init__(self):
        # 调用超类的构造函数方法,也可以使用Bird.__init__(self),但使用super可以在有多个超类的情况下也能正确处理
        super().__init__()
        self.sound = "Squawk!"
    def sing(self):
        print(self.sound)

s = SongBird()
s.eat()

def check_index(key): 
    """
    指定的键是否是可接受的索引?
    键必须是非负整数,才是可接受的。如果不是整数, 将引发TypeError异常;如果是负数,将引发Index Error异常(因为这个序列的长度是无穷的)
    """
    if not isinstance(key, int): 
        raise TypeError 
    if key < 0: raise IndexError

class ArithmeticSequence:
    def __init__(self, start = 0, step = 1, max = 1000):
        """
        初使化这个算术序列

        start   -序列中的第一个值
        step    -两个相邻序列的差
        max     -最大值,包括此最大值 
        changed -一个字典,保存用户修改过的值
        """
        self.start = start
        self.step = step
        self.max = max
        self.changed = {}
    def __getitem__(self, key):
        """
        从序列中获取一个元素
        """
        check_index(key)

        try: return self.changed[key]
        except KeyError:
            return self.start + self.step * key
    def __setitem__(self, key, value):
        """
        修改算术序列中的元素
        """
        check_index(key)
        self.changed[key] = value

    def __len__(self):
        """
        返回序列的长度
        """
        return int((self.max-self.start)/self.step)
    def __delitem__(self, key):
        """
        删除对应的change值,返回默认值
        """
        check_index(key)
        del self.changed[key]


s = ArithmeticSequence(0, 1)
print(s[4], s[5]) # 依赖于__getitem__方法,输出 4 5
s[5] = 10 # 依赖于__setitem__方法,将值 设置到changed里
print(s[5], s[4]) # 输出10 4
print(len(s)) # 依赖于__len__方法,输出 1000
del s[5] # 依赖于__delitem__
print(s[5]) # 输出5

通过继承list来实现扩展列表的功能:

# 从list中继承来扩展列表
class CounterList(list):
    def __init__(self, *args):
        super().__init__(*args)
        self.counter = 0
    # 有一些操作访问的方法并不都调用__getitem__,所以像pop这一类的操作无法增加计数
    def __getitem__(self, index):
        self.counter += 1
        return super().__getitem__(index)

c = CounterList([0,1,2,3,4,5,6,7])
print(c[1] + c[2]) # 3
print(c.counter) # 2

更多的魔法方法可以参考Special method names

特性(property)

一般的属性我们会为其设置get和set方法。通过property可以把一个虚拟的属性当成正常的属性来设置和获取:

class Rectangle:
    def __init__(self):
        self.width = 0
        self.height = 0
    def set_size(self, size):
        self.width, self.height = size
    def get_size(self):
        return (self.width, self.height)
    size = property(get_size, set_size)

r1 = Rectangle()
# 正常我们访问size时,是通过get和set方法来的
r1.set_size((10,20))
print(r1.get_size()) # (10, 20)
# 添加了size = property(get_size, set_size)之后,就可以直接给属性赋值和访问了
# property方法还支持可选的fdel,删除函数,doc文档 等参数
r1.size = (20, 40)
print(r1.size, r1.get_size()) # (20, 40) (20, 40)

property的实现原理是通过property类中的__get____set____delete__这些魔术方法来处理的。这些方法被定义为描述符协议,可以拦截对属性的访问,设置和删除。

除了使用property来实现对象属性的访问拦截,也可以通过以下这些方法来实现;相比函数property,这些魔法方法使用起来要棘手些(从某种程度上说,效率也更低),但在同时处理多个特性时很有用:

__getattribute__(self, name):在属性被访问时自动调用(只适用于新式类);
__getattr__(self, name):在属性被访问而对象没有这样的属性时自动调用;
__setattr__(self, name, value):试图给属性赋值时自动调用;
__delattr__(self, name):试图删除属性时自动调用。

class Rectangle:
    def __init__(self):
        self.width = 0
        self.height = 0
    # 所有的设置都调用__setattr__,所以也要处理好非size的属性赋值,对__dict__的赋值不会再次调用__setattr__
    def __setattr__(self, name, value):
        if name == 'size':
            self.width, self.height = value
        else:
            self.__dict__[name] = value
    # 只有在没有找到对应属性的情况下才会调用__getattr__,所以非size的,都直接报错,这个错一定要是AttributeError才能在hasattr和getattr方法下正确处理
    def __getattr__(self, name):
        if name == "size":
            return (self.width, self.height)
        else:
            raise AttributeError()
    # 不使用__getattr__也可以使用__getattribute__方法,注意这个方法是代理所有的访问,包括__dict__的问题,所以需要调用到超类的此方法
    # def __getattribute__(self, name):
    #     if name == "size":
    #         return (self.width, self.height)
    #     else:
    #         return super().__getattribute__(name)

r = Rectangle()
r.size = (10, 20)
r.a = 100
print(r.size, r.a, r.width) # (10, 20) 100 10

类方法和静态方法

# 静态方法与类方法
class MyClass:
    # 这个注解也可以换成使用这一语句smeth = staticmethod(smeth)
    @staticmethod
    def smeth():
        print('This is a static method')

    # 这个注解也可以换成使用这一语句cmeth = classmethod(smeth)
    @classmethod
    def cmeth(cls):
        print('This is a class method of ', cls)

MyClass.smeth() # This is a static method
MyClass.cmeth() # This is a class method of  <class '__main__.MyClass'>

实现了__iter__的对象可以使用for来迭代;__iter__方法返回包含有__next__方法的迭代器对象,这个__next__方法没有参数,返回下一个值,如果没有值了,需要引发StopIteration异常:

class Fibs:
    def __init__(self, max = 1000):
        self.a = 0
        self.b = 1
        self.max = max
    def __next__(self):
        self.a, self.b = self.b, self.a + self.b
        if self.b > self.max:
            raise StopIteration()
        return self.b
    def __iter__(self):
        return self

for i in Fibs():
    print(i)
print(list(Fibs())) # [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
# 包含yield语句的函数被称之为生成器,生成器可以生成多个值,每一次使用yield生成一个值之后就被冻结,等待被重新唤醒。
# 调用生成器的函数,返回了生成器的迭代器,可以像使用普通迭代一样来使用生成器的迭代器
def p(max):
    for i in range(max):
        yield i**2
gp = p(10)
print(gp) # <generator object p at 0x10d486550>
print(next(gp), next(gp), next(gp), next(gp), next(gp), next(gp)) # 0 1 4 9 16 25
# 也可以使用类似于列表推导的方式(将中括号换成小括号)来创建生成器
print((i**2 for i in range(10))) # <generator object <genexpr> at 0x10d5584d0>
# 如果直接既有的小括号内使用生成器推导,则可以省掉生成器使用的小括号,如下:
print(i**2 for i in range(10)) # <generator object <genexpr> at 0x10d5584d0>

# 将一个n层的嵌套列表,展开成一级列表
def flatten(nested):
    try:
        # 字符串即使单个字符也可以迭代,所以需要对字符串做特殊处理
        try: nested + ''
        except TypeError: pass
        else: raise TypeError
        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested
print(list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))) # [1, 2, 3, 4, 5, 6, 7, 8]
print(list(flatten(['foo', ['bar', ['baz']]]))) # ['foo', 'bar', 'baz']

# 外部可以通过生成器的迭代器的send方法往生成器中发送数据,体现出来的就是yield的返回值,这与next的区别就是next唤醒的yield,返回值为None
def seqWithSkip(begin):
    i = begin
    while True:
        skip = (yield i)
        if skip is not None: i+=skip
        i+=1
sws = seqWithSkip(100)
print(next(sws), next(sws), next(sws)) # 100 101 102
print(sws.send(100), next(sws), next(sws)) # 203 204 205

八皇后问题

# 八皇后问题,state为已经确认的前几行皇后的位置,判断下一行的位置nextX是否会冲突
# 冲突的定义是有两个皇后在同一列,或者在同一行或者在对角线上
def conflict(state, nextX):
    nextY = len(state)
    for i in range(nextY):
        if state[i] == nextX or abs(state[i] - nextX) == abs(i - nextY):
            return True
    return False

# 使用递归的方式来解题,queens函数根据皇后数量(棋盘的大小)和给出的状态,补全剩下的状态
# 下一个状态肯定是从0~num中选一个,而且必须满足条件的,假设取出了pos
# 如果这个pos是最后一个(这也是递归的终结条件),则直接返回(pos,)此元组
# 如果非最后一个,则将此位置放入state中,递归求剩下的状态,返回的满足条件的元组再补上pos就是最终要求的状态了
def queens(num = 8, state = ()):
    for pos in range(num):
        if not conflict(state, pos):
            if len(state) == num - 1: 
                yield (pos,)
            else:
                for result in queens(num, state + (pos,)):
                    yield (pos,) + result

print(list(queens(4))) # [(1, 3, 0, 2), (2, 0, 3, 1)]
print(list(queens(8)))

可以使用sys.path.append添加模块的查找路径,注意模块只会导入一次,可以使用importlib模块的importlib.reload(queens)来重新导入。

>>> import sys
>>> sys.path
['', '/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip', '/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload', '/Users/liqingshou/Library/Python/3.7/lib/python/site-packages', '/usr/local/lib/python3.7/site-packages', './']
>>> sys.path.append("./")
>>> import queens
>>> list(queens.queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]

>>> import importlib
>>> importlib.reload(queens)

模块的测试很重要,可以在模块中添加测试代码。为了不在正常导入模块时运行测试代码,可以使用以下方式if __name__ == '__main__': test(),这样就不会在导入的时候运行了。

更专业的测试代码应该使用独立的程序来处理。

除修改sys.path的方法来改变模块的搜索目录外,还可以通过环境变量的方式:

$ PYTHONPATH=$PYTHOPATH:./ python3
Python 3.7.7 (default, Mar 10 2020, 15:43:33) 
[Clang 11.0.0 (clang-1100.0.33.17)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import queens
>>> list(queens.queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]

在某一个目录当中添加__init__.py文件,则这个目录可以被识别为一个包,可以像导入模块一样导入包。

例如目录结构为:

drawing
├── __init__.py
├── colors.py
└── shapes.py

则可以使用以下语句:

import drawing: 可使用__init__.py中定义的内容,但不能使用colors和shapes的内容;
import drawing.colors:可使用colors的内容,但必须使用drawing.colors全限定名,当然__init__.py中的内容也可以使用;
from drawing import shapes:可直接通过shapes.xxx来使用shapes模块中的内容,当然__init__.py中的内容也可以使用。

使用dir函数,可以获得模块中的所有属性,包括类、函数、变量等,另外如果模块中包含了__all__这样的变量,那么使用from module import *的话只会导入__all__所包含的属性,其它的只能通过精确导入才行。

# drawing/colors.py
def green():
    """
    just a demo function of green
    """
    pass
def red():pass

__all__ = ["green"]
>>> from drawing.colors import *
>>> green()
>>> red()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'red' is not defined

使用help函数可以获取对应函数的文档help(green),如果想获取doc字段可以使用print(green.__doc__)

通过模块的__file__属性可以获取这个模块的文件路径:

>>> import drawing; print(drawing.__file__)
/Users/liqingshou/Work/xiaochai/batman/Python/drawing/__init__.py

一些标准库

argv: 命令行参数,包括脚本名;
exit([arg]):退出当前程序,可通过可选参数指定返回值或错误消息;
modules:一个字典,将模块名映射到加载的模块;
path:一个列表,包含要在其中查找模块的目录的名称;
platform: 一个平台标识符,如sunos5或win32;
stdin:标准输入流——一个类似于文件的对象;
stdout:标准输出流——一个类似于文件的对象;
stderr:标准错误流——一个类似于文件的对象。

environ:包含环境变量的映射;
system(command):在子shell中执行操作系统命令;
sep:路径中使用的分隔符;
pathsep 分隔不同路径的分隔符;
linesep:行分隔符(‘\n’、’\r’或’\r\n’);
urandom(n):返回n个字节的强加密随机数据:

>>> os.environ
environ({'USER': 'liqingshou', 'COMMAND_MODE': 'unix2003', ...})
>>> os.system("pwd")
/Users/liqingshou/Work/xiaochai/batman/Python
0
>>> os.sep
'/'
>>> os.pathsep
':'
>>> os.linesep
'\n'
>>> os.urandom(10)
b'\x85\x9cs\xfa\xecA\xc5\xfa@\xa4'

fileinput

input([files[, inplace[, backup]]]):帮助迭代多个输入流中的行,如果指定了inplace为True,则原文件将被标准输出的内容所替代;
filename():返回当前文件的名称;
lineno():返回(累计的)当前行号;
filelineno():返回在当前文件中的行号;
isfirstline():检查当前行是否是文件中的第一行;
isstdin():检查最后一行是否来自sys.stdin;
nextfile():关闭当前文件并移到下一个文件;
close():关闭序列:

# 对于一系列输入了的文件,在开头添加文件名,在行末添加行号,只取前5行
import fileinput
for line in fileinput.input(inplace=False):
    if fileinput.isfirstline():
        print("# filename:", fileinput.filename(), 
        ", isstdin:", fileinput.isstdin())
    if fileinput.filelineno() >= 5: 
        fileinput.nextfile()
    line = line.rstrip()
    print('{:<50} # {:2d} # {} # '.format(
        line, fileinput.lineno(), 
        fileinput.filelineno()))
fileinput.close()

# 运行命令 python3 test.py test.py test.py
# 运行结果如下:
# filename: test.py , isstdin: False
# import fileinput                                   #  1 # 1 # 
# for line in fileinput.input(inplace=False):        #  2 # 2 # 
#     if fileinput.isfirstline():                    #  3 # 3 # 
#         print("# filename:", fileinput.filename(), #  4 # 4 # 
#         ", isstdin:", fileinput.isstdin())         #  5 # 5 # 
# # filename: test.py , isstdin: False
# import fileinput                                   #  6 # 1 # 
# for line in fileinput.input(inplace=False):        #  7 # 2 # 
#     if fileinput.isfirstline():                    #  8 # 3 # 
#         print("# filename:", fileinput.filename(), #  9 # 4 # 
#         ", isstdin:", fileinput.isstdin())         # 10 # 5 # 

新版本的Python中内置支持了set集合,创建方式使用花括号表示,注意空的花括号会被识别为字典。

集合上支持一些操作:

union:取并集,与符号|一致;
intersection: 取交集,与符号&一致;
issubset:调用者是否是参数指定集合的子集;
differece:调用集合排除掉参数集合后的结果,与符号-一致;
symmetric_difference:取两个集合不一样的用户,与^符号一致;
add:往集合中添加元素;
remove:从集合中删除元素:

>>> type({})
<class 'dict'>
>>> type({1})
<class 'set'>
>>> {1,2,2,2,2,2,3}
{1, 2, 3}
>>> a = {1,2,3}
>>> b = {2,3,4}

>>> a | b
{1, 2, 3, 4}
>>> a.union(b)
{1, 2, 3, 4}

>>> a & b
{2, 3}
>>> a.intersection(b)
{2, 3}

>>> a.issubset(b)
False
>>> a.issubset(a)
True

>>> a.difference(b)
{1}
>>> a-b
{1}
>>> b-a
{4}

>>> a.symmetric_difference(b)
{1, 4}
>>> a ^ b
{1, 4}

>>> a.add(5)
>>> a
{1, 2, 3, 5}

集合是可变的,所以不能用作字典的键;

另外集合的成员只能是不可变的值,所以现实中的集合的集合需要借助于frozenset类型(不可变集合)来实现:

>>> a = {1}
>>> b = {2}
>>> a.add(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
>>> a.add(frozenset(b))
>>> a
{1, frozenset({2})}

Python中的堆数据结构使用列表与一些函数配合实现,主要提供的函数有以下这些:

heappush(heap, x):将x压入堆中;
heappop(heap):从堆中弹出最小的元素;
heapify(heap):让列表具备堆特征;
heapreplace(heap, x):弹出最小的元素,并将x压入堆中;
nlargest(n, iter):返回iter中n个最大的元素;
nsmallest(n, iter):返回iter中n个最小的元素:

>>> l = [99,2,87,27,19,50,92,43,39]
>>> from heapq import *
>>> l
[99, 2, 87, 27, 19, 50, 92, 43, 39]
>>> heapify(l)
>>> l
[2, 19, 50, 27, 99, 87, 92, 43, 39]
>>> heappop(l)
2
>>> l
[19, 27, 50, 39, 99, 87, 92, 43]
>>> heappop(l)
19
>>> heapreplace(l, 1)
27
>>> l
[1, 39, 50, 43, 99, 87, 92]
>>> nlargest(2, l)
[99, 92]
>>> l
[1, 39, 50, 43, 99, 87, 92]
>>> nsmalle

双端队列在collections模块中的deque类型,以下示例展示了常用的函数使用方式:

>>> from collections import deque
>>> q = deque([1,2,3,4,5])
>>> q
deque([1, 2, 3, 4, 5])
>>> q.append(6)
>>> q.appendleft(0)
>>> q
deque([0, 1, 2, 3, 4, 5, 6])
>>> q.pop()
6
>>> q.popleft()
0
>>> q
deque([1, 2, 3, 4, 5])
>>> q.rotate(3)
>>> q
deque([3, 4, 5, 1, 2])
>>> q.rotate(-1)
>>> q
deque([4, 5, 1, 2, 3])

asctime([tuple]):将时间元组转换为字符串;
localtime([secs]):将秒数转换为表示当地时间的日期元组;
mktime(tuple):将时间元组转换为当地时间;
sleep(secs):休眠(什么都不做)secs秒;
strptime(string[, format]):将字符串转换为时间元组;
time():当前时间(从新纪元开始后的秒数,以UTC为准);

以上的tuple为8元组,分别为(年,月,日,时,分,秒,星期,儒略日,夏令时):

>>> l = time.localtime()
>>> l
time.struct_time(tm_year=2020, tm_mon=6, tm_mday=20, tm_hour=20, tm_min=12, tm_sec=52, tm_wday=5, tm_yday=172, tm_isdst=0)
>>> l = list(l)
>>> l[0] = 2019
>>> time.asctime(tuple(l))
'Sat Jun 20 20:12:52 2019'
>>> time.time()
1592655205.469694

random

random():返回一个0~1(含)的随机实数;
getrandbits(n):以长整数方式返回n个随机的二进制位;
uniform(a, b):返回一个a~b(含)的随机实数;
randrange([start], stop, [step]):从range(start, stop, step)中随机地选择一个数;
choice(seq):从序列seq中随机地选择一个元素;
shuffle(seq[, random]):就地打乱序列seq;
sample(seq, n):从序列seq中随机地选择n个值不同的元素:

>>> import random
>>> random.random()
0.2918300245721943
>>> random.getrandbits(10)
831
>>> random.uniform(20,30)
21.144689897763417
>>> random.randrange(1000, 2000, 1)
1890
>>> random.choice(range(100))
49
>>> l = list(range(10))
>>> random.shuffle(l)
>>> l
[9, 8, 2, 4, 1, 5, 3, 7, 0, 6]
>>> random.sample(l, 3)
[8, 4, 6] 

shelve

shelve算是一个将python的数据结构序列化到文件中的一个方便的类:

$ python3
Python 3.7.7 (default, Mar 10 2020, 15:43:33) 
>>> import shelve
>>> s = shelve.open('test.dat')
>>> s['x'] = ['a','b','c']
>>> s['y'] = {"a" : 1}
>>> 
$ python3
Python 3.7.7 (default, Mar 10 2020, 15:43:33) 
>>> import shelve
>>> s = shelve.open('test.dat')
>>> s['x']
['a', 'b', 'c']
>>> s['y']
{'a': 1}
import shelve

def enter_command():
    cmd = input("Enter command(? for help):")
    cmd = cmd.strip().lower()
    return cmd

def store_person(db):
    id = input("Enter ID:")
    person = {}
    person['name'] = input('Enter name:')
    person['age'] = input('Enter age:')
    db[id] = person

def lookup_person(db):
    id = input("Enter ID:")
    print(db[id])

def print_help():
    print("""The available commands are:
    store: stores information
    lookup: looks up a person by ID
    quit: save change and exit
    ?: prints this message
    """)

def main():
    database = shelve.open("./database.dat")
    try:
        while True:
            cmd = enter_command()
            if cmd == 'store':
                store_person(database)
            elif cmd == 'lookup':
                lookup_person(database)
            elif cmd == '?':
                print_help()
            elif cmd == 'quit':
                return 
    finally:
        database.close()

if __name__ == '__main__': main()


# 以下是运行结果

# ➜  Python git:(master) ✗ python3 database.py
# Enter command(? for help):'?
# The available commands are:
#     store: stores information
#     lookup: looks up a person by ID
#     quit: save change and exit
#     ?: prints this message
#     
# Enter command(? for help):'store
# Enter ID:1
# Enter name:Lee
# Enter age:10
# Enter command(? for help):'store
# Enter ID:2
# Enter name:Chen
# Enter age:12
# Enter command(? for help):'quit
# ➜  Python git:(master) ✗ python3 database.py
# Enter command(? for help):lookup
# Enter ID:1
# {'name': 'Lee', 'age': '10'}
# Enter command(? for help):lookup
# Enter ID:2
# {'name': 'Chen', 'age': '12'}
# Enter command(? for help):quit

compile(pattern[, flags]) : 根据包含正则表达式的字符串创建模式对象;
search(pattern, string[, flags]): 在字符串中查找模式;
match(pattern, string[, flags]) :在字符串开头匹配模式,注意与search的不同,match是整个字符串匹配,而search可以部分匹配;
split(pattern, string[, maxsplit=0]):根据模式来分割字符串;
findall(pattern, string): 返回一个列表,其中包含字符串中所有与模式匹配的子串;
sub(pat, repl, string[, count=0]):将字符串中与模式pat匹配的子串都替换为repl ;
escape(string):对字符串中所有的正则表达式特殊字符都进行转义:

>>> p = re.compile('[0-9]+')
>>> p
re.compile('[0-9]+')
>>> re.search(p, "ik2131,k21lsdf")
<re.Match object; span=(2, 6), match='2131'>
>>> re.match(p, "ik2131,k21lsdf")
>>> re.match(p, "32423423")
<re.Match object; span=(0, 8), match='32423423'>
>>> re.search('[a-z]+', '4323kljjk32jlj342')
<re.Match object; span=(4, 9), match='kljjk'>
>>> p.split("3423kjlkjl2432kjl34j24l34")
['', 'kjlkjl', 'kjl', 'j', 'l', '']
>>> re.findall('[a-z]+', '4323kljjk32jlj342')
['kljjk', 'jlj']
>>> p.sub("....", '4323kljjk32jlj342')
'....kljjk....jlj....'
>>> re.escape("[0-9]+")
'\\[0\\-9\\]\\+'
>>> re.sub("([0-9]+?)", r"\1\1", "kjl23423,32424")
'kjl2233442233,3322442244'
>>> re.sub("([0-9]+)", r"\1\1", "kjl23423,32424")
'kjl2342323423,3242432424'

match和search如果没有匹配返回None,匹配上时会返回re.Match对象,此对象上可以应用如下方法:

groups():以元组的方式返回所有的匹配编组;
group([group1, …]):获取与给定子模式(编组)匹配的子串;
start([group]):返回与给定编组匹配的子串的起始位置 ;
end([group]):返回与给定编组匹配的子串的终止位置(与切片一样,不包含终止位置);
span([group]):返回与给定编组匹配的子串的起始和终止位置:

>>> str = "Info, Name:Lee,Age:23,From:China"
>>> g = re.match(".*Name:([a-zA-Z.]+),[ ]?Age:([0-9]+).*", str)
>>> g.groups()
('Lee', '23')
>>> g.group(2)
'23'
>>> str[g.start(1):g.end(1)]
'Lee'
>>> g.span(1)
(11, 14)

argparse:解析命令行参数,比sys.argv更加好用;

cmd:这个模块让你能够编写类似于Python交互式解释器的命令行解释器。你可定义命令, 让用户能够在提示符下执行它们;

csv:csv文件的解析与写入;

json:json字符串的处理;

datetime: 更多的日期操作支持;

difflib:比较两个序列的相似程度;

enum:对于枚举的支持;

functools、hashlib、itertools、logging、statistics、timeit、profile、trace等等。

open函数用于打开文件,可以指定一个模式选项:

r:读取模式(默认值);
w:写入模式;
x:独占写入模式;
a:追加加模式 ;
b:二进制模式(与其他模式结合使用);
t:文本模式(默认值,与其他模式结合使用);
+:读写模式(与其他模式结合使用),r+和w+都表示读写,但是w+会截断文件。

open返回的对象,可以使用write,read,close这些函数;sys.stdin也是一个资源对象:

>>> f = open("./a", "w+")
>>> f.write("helloworld\n")
11
>>> f.writelines(["1\n", "2\n"])
>>> f.close()
>>> f = open("./a", "r+")
>>> f.read(1)
'h'
>>> f.readline()
'elloworld\n'
>>> f.seek(0)
0
>>> f.readlines()
['helloworld\n', '1\n', '2\n']
>>> f.close()

文件打开后需要调用close()关闭文件,为了防止文件被锁定无法修改,或者占用太多的文件描述符号。一般我们使用以下语句来处理关闭文件:

f = open("./a", "w+")
try:
    # 写入数据
    f.write("xxx\n")
finally:
    f.close()

# with语句专门用于处理此情况,称之为上下文管理器

with open("./a", "w+") as f:
    f.write("xxx\n")

在Python中,打开的文件是可以直接迭代的,相当于每一次读取一行的文件内容:with open("./clz.py", "r") as f: list(f)

通过简单的文件加载编辑器来演示tkinter的使用:

from tkinter import *
from tkinter.scrolledtext import ScrolledText

def load():
    with open(filename.get()) as f:
        # 表示删除第一行第0个字符开始,到结束
        contents.delete('1.0', END)
        contents.insert(INSERT, f.read())
def save():
    with open(filename.get(), 'w') as f:
        f.write(contents.get('1.0', END))

top = Tk()
top.title("Editor")

contents = ScrolledText()
contents["bg"] = "black"
contents["fg"] = "white"
contents.pack(side=BOTTOM, expand=True, fill=BOTH)

filename = Entry()
filename.pack(side=LEFT, expand=True, fill=X)

openBtn = Button(text="Open", command=load, highlightbackground='black')
openBtn.pack(side=LEFT)
saveBtn = Button(text="Save", command=save, highlightbackground='black')
saveBtn.pack(side=LEFT)
mainloop()

Python中对数据库定义了一些标准,所有的第三方数据库需要按照这个标准来实现。

apilevel:用于指代这个模块所支持的标准数据库接口的版本号,Python中的DB API2.0指出这个值可以是1.0或者2.0,如果不存在这个变量,说明这个模块不兼容DB API2.0标准;

threadsafety:模块的线程安全级别,3表示绝对的线程安全,2表示线程可以共享模块和连接但不能共享游标,1表示可共享模块,但不能共享连接,0表示也不能共享模块;

paramstyle:在SQL查询中使用的参数风格支持,format表示使用标准的字符串格式,如%s等;pyformat使用字典的形式如%(foo)s;qmark使用问号占位,numeric使用数字编号占位如:1、:2这样;named使用命名占位如:foobar。

DB API定义的一些通用异常:

StandardError: 所有异常的超类;
Warning:发生非致命问题时引发;
Error:所有错误条件的超类;
InterfaceError:与接口(而不是数据库)相关的错误;
DatabaseError:与数据库相关的错误的超类;
DataError:与数据相关的问题,如值不在合法的范围内;
OperationalError :数据库操作内部的错误;
IntegrityError :关系完整性遭到破坏,如键未通过检查;
InternalError :数据库内部的错误,如游标无效;
ProgrammingError :用户编程错误,如未找到数据库表;
NotSupportedError:请求不支持的功能,如回滚。

一些标准函数

connect:连接数据函数,其它参数支持有dsn(数据源名称,因数据库而异);user(用户名);password(密码);host(主机);database(数据库名称)。connect会返回连接对象,表示当前的数据库会话,支持以下方法:

close():关闭连接,之后连接对象以及其游标都不可用;注意对于支持事务的数据库,关闭连接会把没有提交的事务回滚了,所以注意在关闭之前提交事务;
commit():提交事务,如果此数据库不支持事务,则这个函数啥也不做;
rollback():回滚操作,如果数据库不支持事务,则抛出异常;
cursor():返回连接的游标对象,使用游标来执行SQL查询和查看结果。

游标对象支持下以方法和属性:

callproc(name[, params]) :使用指定的参数调用指定的数据库过程(可选);
close():关闭游标。关闭后游标不可用;
execute(oper[, params]) :执行一个SQL操作——可能指定参数;
executemany(oper, pseq) :执行指定的SQL操作多次,每次都序列中的一组参数;
fetchone() :以序列的方式取回查询结果中的下一行;如果没有更多的行,就返回None;
fetchmany([size]) :取回查询结果中的多行,其中参数size的值默认为arraysize;
fetchall():以序列的序列的方式取回余下的所有行;
nextset() :跳到下一个结果集,这个方法是可选的;
setinputsizes(sizes) :用于为参数预定义内存区域;
setoutputsize(size[, col]):为取回大量数据而设置缓冲区长度;

description :由结果列描述组成的序列(只读);
rowcount :结果包含的行数(只读);
arraysize:fetchmany返回的行数,默认为1。

Date(year, month, day) :创建包含日期值的对象;
Time(hour, minute, second) :创建包含时间值的对象;
Timestamp(y, mon, d, h, min, s) :创建包含时间戳的对象;
DateFromTicks(ticks) :根据从新纪元开始过去的秒数创建包含日期值的对象;
TimeFromTicks(ticks) 根据从新纪元开始过去的秒数创建包含时间值的对象;
imestampFromTicks(ticks) :根据从新纪元开始过去的秒数创建包含时间戳的对象;
Binary(string):创建包含二进制字符串值的对象;
STRING:描述基于字符串的列(如CHAR);
BINARY:描述二进制列(如LONG或RAW);
NUMBER:描述数字列;
DATETIME:描述日期/时间列;
ROWID:描述行ID列。

使用sqlite存储和查询数据:

import sqlite3

conn = sqlite3.connect('my.db')
curs = conn.cursor()

try:
    curs.execute("""
    create table people(
        id int primary key,
        name string,
        age int
    )
    """)
except sqlite3.DatabaseError as e:
    print("create table:", e, "; but continue")

data = [
    [1, "Lee", 10],
    [2, "Lucy", 12],
    [3, "John", 9],
    [4, "Lily", 10],
    [5, "Green", 11]
]

sql =  "insert into people values(?,?,?)"

for row in data :
    try:
        curs.execute(sql, row)
    except sqlite3.DatabaseError as e:
        print("insert error:", e, data, ",oh..")


curs.execute("select * from people where age >= 11")
#print(list(curs.description))
# [('id', None, None, None, None, None, None), ('name', None, None, None, None, None, None), ('age', None, None, None, None, None, None)]
#print(list(curs.fetchall()))
# [('id', None, None, None, None, None, None), ('name', None, None, None, None, None, None), ('age', None, None, None, None, None, None)]

names = [f[0] for f in curs.description]
for row in curs.fetchall():
    for pair in zip(names, row):
        print('{}:{}'.format(*pair))
    print()
# id:2
# name:Lucy
# age:12
# 
# id:5
# name:Green
# age:11

如果是mysql的话,需要安装pymysql扩展pip3 install cryptography pymysql;另外可以使用dokcer启动一个mysql:

$ docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql
$ docker exec -it mysql bash
root@088053eba10b:/# mysql -uroot -p123456
mysql> create database test;
import pymysql
conn = pymysql.connect("localhost","root","123456","test",charset='utf8')
curs = conn.cursor()

try:
    curs.execute("""
    create table people(
        id int primary key,
        name varchar(100),
        age int
    )
    """)
except pymysql.DatabaseError as e:
    print("create table:", e, "; but continue")

data = [
    [1, "Lee", 10],
    [2, "Lucy", 12],
    [3, "John", 9],
    [4, "Lily", 10],
    [5, "Green", 11]
]

# pymysql中的paramstyle为pyformat,而且所有的参数必须为字符串,即%s
sql =  "insert into people values(%s,%s,%s)"

for row in data :
    try:
        # 由于mysql需要将所有的参数转成字符串
        curs.execute(sql, row)
    except pymysql.DatabaseError as e:
        print("insert error:", e, row, ",oh..")


curs.execute("select * from people where age >= 11")
#print(list(curs.description))
# [('id', None, None, None, None, None, None), ('name', None, None, None, None, None, None), ('age', None, None, None, None, None, None)]
#print(list(curs.fetchall()))
# [('id', None, None, None, None, None, None), ('name', None, None, None, None, None, None), ('age', None, None, None, None, None, None)]

names = [f[0] for f in curs.description]
for row in curs.fetchall():
    for pair in zip(names, row):
        print('{}:{}'.format(*pair))
    print()
# id:2
# name:Lucy
# age:12
# 
# id:5
# name:Green
# age:11

# 一定要commit, 因为mysql支持回滚,所以如果没有commit,则之前的添加全部没有了
conn.commit()
conn.close()

使用socket展示一个简单的客户端和服务端:

import socket
import sys
def server():
    s = socket.socket()
    s.bind(("127.0.0.1", 8888))
    s.listen(5) # 这个参数是backlog
    while True:
        c, addr = s.accept()
        # Got connection from  ('127.0.0.1', 64504)
        print('Got connection from ', addr)
        c.sendall(b'Thank you for connecting')
        c.close()

def client():
    s = socket.socket()
    s.connect(("127.0.0.1", 8888))
    print(s.recv(1024))

# 一个简单地只能每一次处理一个请求的server和client示例
if len(sys.argv) >= 2 and sys.argv[1] == "server":
    server()
else:
    client()

使用urllib和urllib2这两个网络库可以很方便地访问网络文件:

urlopen返回的对象是类似于文件资源的对象,所以可以使用类似read等方法以及迭代等,读取出来的内容可以通过正则来获取想要的数据。

urlretrieve可以下载文件到指定路径,如果文件没有指定,会存储在一个临时的位置,可以调用urlcleanup函数(不带参数)来直接清理对应的文件。

>>> import urllib.request as urlreq
>>> webpage = urlreq.urlopen('http://baidu.com')
>>> webpage.read()
b'<html>\n<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">\n</html>\n'
>>> urlreq.urlretrieve("http://baidu.com", "./a")
('./a', <http.client.HTTPMessage object at 0x1057ce730>)

>>> import urllib.parse
>>> urllib.parse.urlencode({"callback":"http://baidu.com","name":"Lee"})
'callback=http%3A%2F%2Fbaidu.com&name=Lee'

SocketServer

之前的server/client的示例,同时只能处理一个请求,使用SocketServer这一模块,可以有多种方式同时处理多个请求:

先来看一个简单的server端:

# 使用socketserver来创建服务端
from socketserver import TCPServer, StreamRequestHandler
def sServer():
    class Handler(StreamRequestHandler):
        def handle(self):
            addr = self.request.getpeername()
            print('Got connection from', addr)
            self.wfile.write(b'Thank you for connecting')
    server = TCPServer(('127.0.0.1', 8888), Handler)
    server.serve_forever()

使用forking和创建线程来同时处理多个请求的代码实现很简单:

# 通过每来一个请求就fork进程的方式来同时处理多个请求
# 在client请求时,可以使用ps -ef| grep python| grep server来查看新起的进程
def forkingServer():
    class Server(ForkingMixIn, TCPServer): pass
    server = Server(('127.0.0.1', 8888), Handler)
    server.serve_forever()

# 通过每来一个请求创建一个线程的方式来同时处理多个请求
def threadServer():
    class Server(ThreadingMixIn, TCPServer): pass
    server = Server(('127.0.0.1', 8888), Handler)
    server.serve_forever()

使用多路复用的方式来实现同时处理多个请求:

import select
# 这个例子中使用select来同时保持多个连接,并且可以随时响应任意连接的数据写入
def selectServer():
    s = socket.socket()
    s.bind(("127.0.0.1", 8888))
    s.listen(5) # 这个参数是backlog
    inputs = [s]
    while True:
        rs, ws, es = select.select(inputs, [], [])
        for r in rs:
            if r is s:
                c, addr = s.accept()
                print('Got connection from ', addr)
                inputs.append(c)
            else:
                try:
                    data = r.recv(1024)
                    disconnected = not data
                except socket.error:
                    disconnected = True
                if disconnected:
                    print(r.getpeername(), "disconnected")
                    inputs.remove(r)
                else:
                    print("from ", r.getpeername(), ":", data)

需要对应的给client端进行一些改造,使得能够响应输入并发送这些数据:

def iclient():
    s = socket.socket()
    s.connect(("127.0.0.1", 8888))
    while True:
        i = input("enter:")
        s.sendall( bytes(i, "utf8"))

使用poll和epoll(mac中没有epoll只有kqueue)的例子就不在这里处理了。

使用twisted基于事件的网络库pip3 install twisted

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory 

def twistedServer():
    class SimpleLogger(Protocol):
        def connectionMade(self):
            print('Got connection from', self.transport.client)
        def connectionLost(self, reason): 
            print(self.transport.client, 'disconnected')
        def dataReceived(self, data): 
            print("from ", self.transport.client, ":", data)
    factory = Factory()
    factory.protocol = SimpleLogger
    reactor.listenTCP(8888, factory)
    reactor.run()

在网页抓取工作中,由于HTML编写得并不都十分规范,所以就需要使用像Tidy之类的工具将其修复。

例如如下这个不规范的html文件,使用tidy -o fixed.html bad.html命令转换完成之后的效果如下面的代码:

<h1>Pet Shop <h2>Complaints</h3>
<p>There is <b>no <i>way</b> at all</i> we can accept returned parrots.
<h1><i>Dead Pets</h1>
<p>Our pets may tend to rest at times, but rarely die within the
    warranty period.
<i><h2>News</h2></i>
<p>We have just received <b>a really niparrot.
<p>It's really nice.</b>
<h3><hr>The Norwegian Blue</h3>
<h4>Plumage and <hr>pining behavior</h4>
<a href="#norwegian-blue">More information<a>
<p>Features:
<body>
<li>Beautiful plumage
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Apple macOS version 5.6.0">
<title></title>
</head>
<body>
<h1>Pet Shop</h1>
<h2>Complaints</h2>
<p>There is <b>no <i>way</i></b> <i>at all</i> we can accept
returned parrots.</p>
<h1><i>Dead Pets</i></h1>
<p><i>Our pets may tend to rest at times, but rarely die within the
warranty period. </i></p>
<h2><i>News</i></h2>
<p>We have just received <b>a really niparrot.</b></p>
<p><b>It's really nice.</b></p>
<hr>
<h3>The Norwegian Blue</h3>
<h4>Plumage and</h4>
<hr>
<h4>pining behavior</h4>
<a href="#norwegian-blue">More information</a>
<p>Features:</p>
<li>Beautiful plumage</li>
</body>
</html>

也可以使用subprocess模块来通过Python来调用tidy命令,也可以使用pytidylib这个封装器来使用。

有了标准的html之后,可以使用HTMLParser来进行内容的解析了,它是基于事件的方式来处理,需要继承HTMLParser类,并实现对应的回调函数:

from html.parser import HTMLParser

class GrabH1(HTMLParser):
    h1 = False
    def handle_starttag(self, tag, attrs):
        if tag == "h1":
            self.h1 = True
    def handle_data(self, data):
        if self.h1 == True:
            print("DATA:", data)

    def handle_endtag(self, tag):
        if self.h1 and tag == "h1":
            self.h1 = False
text = open("./fixed.html").read()
parser = GrabH1()
parser.feed(text)
parser.close()

# DATA: Pet Shop
# DATA: Dead Pets

Beautiful Soup

Beautiful Soup是一个可以从HTML或XML文件中提取数据的Python库。

安装:pip install beautifulsoup4文档参考

# beautiful soup试用
from urllib.request import urlopen 
from bs4 import BeautifulSoup

text = urlopen('https://www.crummy.com/software/BeautifulSoup/').read()

soup = BeautifulSoup(text, 'html.parser')
links = {}
for a in soup.find_all('a'):
    try:
        links[a.string] = a["href"]
    except KeyError: pass

print(links)

# {'Download': '#Download', 'Documentation': 'bs4/doc/', 'Hall of Fame': '#HallOfFame', 'For enterprise': 'enterprise.html', 'Source': 'https://code.launchpad.net/beautifulsoup',...}

Web Server

书中说的使用cgi来架设web服务器在目前看来已经淘汰了,我们使用uwsgi和fastcgi来搭建服务器吧。

$ pip3 install uwsgi
$ brew install uwsgi

uwsgi提供了直接作为web server的能力,假设我们有文件uwsgi.py如下,可以直接运行uwsgi --http :9090 --wsgi-file uwsgi.py,就可以在浏览器上访问http://localhost:9090/ 获得Hello World的输出内容:

# uwsgi在启动时就会运行指定的python文件,并在每一次请求的时候查找application函数,这可以通过配置来修改
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]

也可以与nginx配合,使用uwsgi协议来进行通信:

$ uwsgi --socket 127.0.0.1:3031 --wsgi-file uwsgi.py

nginx的配置如下:

# 配置文件地址为:/usr/local/etc/nginx/nginx.conf
# 启动命令 brew services start nginx
location / {
    include uwsgi_params;
    uwsgi_pass 127.0.0.1:3031;
}

这样在浏览器中访问http://localhost:8080 即可访问与之前一致的页面(nginx监听的是8080端口)。

Flask

使用Python的Web框架Flask

创建文件myflaskapp.py:

from flask import Flask, request

app = Flask(__name__)
@app.route('/')
def index():
    return "<span style='color:red'>I am app 1</span>"

@app.route('/page', methods=['POST', 'GET'])
def page():
    return "GET:{}\nMETHOD:{}\nPOST:{}\n".format(request.args, request.method, request.form)

@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

flask支持Jinja2模板引擎,并会在templates目录下找render_template指定的文件,以下是templates/hello.html

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello World!</h1>
{% endif %}

运行uwsgi程序uwsgi --socket 127.0.0.1:3001 --wsgi-file myflaskapp.py --callable=app,这个app就是上面代码中app = Flask(name)创建的入口函数,如果这个启动比较麻烦,可以使用配置文件uwsgi.ini,启动时uwsgi uwsgi.ini

[uwsgi]
socket=:3001
wsgi-file=./myflaskapp.py
callable=app
$ curl 'http://localhost:8080/page?abcdefg=2xxx&dkfj=sfa' -d 'a=1&b=1'
GET:ImmutableMultiDict([('abcdefg', '2xxx'), ('dkfj', 'sfa')])
METHOD:POST
POST:ImmutableMultiDict([('a', '1'), ('b', '1')])

还有一些其它的web框架:DjangoTurboGearsweb2pyGrokZope2Pyramid

介绍两个测试库doctest和unittest,这两测试库Python都自带,以及代码检查工具pylint和pychecker。

doctest

从函数的文档中获取交互式命令相关的内容做为测试case:

def square(x): 
    '''
    计算平方并返回结果
    >>> square(2) 
    4
    >>> square(3) 
    9
    '''
    return x * x

if __name__ == '__main__':
    import doctest
    # doctest将测试本文件内的函数,用例是从函数的注释中获取
    doctest.testmod()

# $ python3 doct.py -v
# Trying:
#     square(2) 
# Expecting:
#     4
# ok
# Trying:
#     square(3) 
# Expecting:
#     9
# ok
# 1 items had no tests:
#     __main__
# 1 items passed all tests:
#    2 tests in __main__.square
# 2 tests in 2 items.
# 2 passed and 0 failed.
# Test passed.

unittest

使用unittest来测试刚才的square函数,我们可以把测试用例使用单独的文件来写。unittest.main()方法运行所有的测试,包括所有TestCase的子类以及类中的test打头的方法。另外它还提供了setUp和tearDown等在测试不同阶段可以回调的函数,方便做数据的准备了清理。

from doct import square
import unittest

class ProductTestCase(unittest.TestCase):
    def test_integers(self):
        for x in range(-10, 10):
            self.assertEqual(square(x), x*x, 'Integer psquare failed')
    def test_floats(self):
        for x in range(-10, 10):
            self.assertEqual(square(x/10), x/10*(x/10), 'Float psquare failed')

if __name__ == "__main__":
    unittest.main()

# $ python3 unitt.py -v
# test_floats (__main__.ProductTestCase) ... ok
# test_integers (__main__.ProductTestCase) ... ok
# 
# ---------------------------------------------------------------------
# Ran 2 tests in 0.000s

pylint和pychecker

pylint将给出一些代码的建议,如是否换行,缺少空格等等:

$ brew install pylint
$ pylint doct.py 
************* Module doct
doct.py:1:14: C0303: Trailing whitespace (trailing-whitespace)
doct.py:4:17: C0303: Trailing whitespace (trailing-whitespace)
doct.py:6:17: C0303: Trailing whitespace (trailing-whitespace)
doct.py:18:15: C0303: Trailing whitespace (trailing-whitespace)
doct.py:23:15: C0303: Trailing whitespace (trailing-whitespace)
doct.py:39:15: C0303: Trailing whitespace (trailing-whitespace)
doct.py:47:0: C0304: Final newline missing (missing-final-newline)
doct.py:1:0: C0114: Missing module docstring (missing-module-docstring)
doct.py:1:0: C0103: Argument name "x" doesn't conform to snake_case naming style (invalid-name)

--------------------------------------------------------------------
Your code has been rated at -8.00/10 (previous run: -8.00/10, +0.00)

标准库中的profile用于性能分析,他可以给出每一个函数的调用次数以及执行的时间,还可以将结果保存于文件中,使用pstats这个模块来分析。profile模块也有对应更快件的C语言版本cProfile

扩展Python

使用Jython扩展

Jython是使用Java编写的Python解释器,所以扩展Jython只需要编写对应的Java类即可:

public class JythonTest{
    public void greeting(){
        System.out.println("Hello");
    }
}

然后编译成class文件,并在运行Jython时指定对应的CLASSPATH,即可直接import对应的Java类了:

$ javac JythonTest.java
$ CLASSPATH=JythonTest.class jython
Jython 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:03:58)
[OpenJDK 64-Bit Server VM (Oracle Corporation)] on java13.0.2
>>> import JythonTest
>>> t = JythonTest()
>>> t.greeting()
Hello

扩展标准版本Python:CPython

编写扩展思路与PHP写扩展的思路是一样的,只是Python中有一个swig工具可以简化这一部分的工作。

例如我们要写一个简单的回文检测函数,涉及到palindrome.c和palindrome.i两个文件,代码如下:

#include <string.h>
int is_palindrome(char *text) { 
    int i, n=strlen(text);
    for (i = 0; i <= n/2; ++i) {
        if (text[i] != text[n-i-1]) 
            return 0; 
    }
    return 1; 
}
%module palindrome
%{
#include <string.h> 
%}
extern int is_palindrome(char *text);

使用swig -python -module myp palindrome.i命令会自动生成palindrome_wrap.c文件,并指定扩展的模块名称为myp,生成myp.py这个模块的封装代码;

然后将这两个文件.c文件编译成共享链接库:

gcc -I/usr/local/Cellar/[email protected]/3.8.3/Frameworks/Python.framework/Versions/3.8/include/python3.8/ -shared palindrome_wrap.c palindrome.c -lpython3.8 -L/usr/local/Cellar/[email protected]/3.8.3/Frameworks/Python.framework/Versions/3.8/lib/ -o _myp.so

其中-I指定头文件的搜索目录,-L指定了链接库的搜索目录,而-l指定了需要链接的python的动态链接库,这是我在mac上的路径,其它环境有可能不一样;注意动态链接库的名称需要是模块名前添加下划线。

这样就可以使用myp这个模块了:

$ PYTHONPATH=$PYTHOPATH:./ python3
>>> import myp
m>>> myp.is_palindrome("abba")
1
>>> import _myp
>>> _myp.is_palindrome("abbaa")
0

可以看出如果使用import myp则加载的是myp.py,而这个封装文件最终也是使用的_myp.so这个动态库,与直接使用import _myp是一样的结果。

如果手写扩展需要处理好引用计数相关的问题,可以参考

以下是不用swig手写的回文字符串检测palindrome1.c:

#include <Python.h>
#include <string.h>

/**
 * 定义的目标函数,函数的返回值必须是*PyObject,self指向模块本身,args参数包含所有传入的参数
 */
static PyObject *is_palindrom(PyObject *self, PyObject *args){
    int i, n;
    const char *text;
    // 传入的参数可以使用PyArg_ParseTuple按一定的格式解析到对应的变量中,其中s表示是字符串
    if (!PyArg_ParseTuple(args, "s", &text)){
        return NULL;
    }
    n = strlen(text);
    // 这次将返回值改成返回True和False
    for (i = 0; i<= n/2; i++){
        if (text[i] != text[n-i-1]){
            Py_INCREF(Py_False);
            return Py_False;
        }
    }
    // 需要增加Py_True的引用计数的数量
    Py_INCREF(Py_True);
    // 如果是要返回整数1的话可以使用Py_BuildValue("i", 1);
    return Py_True;
}

// 导出的方法列表定义,在PyModuleDef中使用,而PyModuleDef又在PyInit_modulename上使用
static PyMethodDef PalindromeMethods[] = {
    // 名称(这影响在python中调用时的名字)、具体的函数、参数类型、文档
    {"is_palindrom1", is_palindrom, METH_VARARGS, "Dected palindromes"},
    // 列表结束标志
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef palindrome = 
{
    PyModuleDef_HEAD_INIT,
    "palindrome1", // 模块名,好像改完之后没有啥效果,只是在help时会在NAME里体现
    "", // 文档
    -1, // 存储在全局变量中的信号状态
    PalindromeMethods // 方法列表
};

// 初始化模块的函数,PyInit_modulename,名字必须是这样的命名规则
PyMODINIT_FUNC PyInit_palindrome1(void){
    return PyModule_Create(&palindrome);
}


// 编译: gcc -I/usr/local/Cellar/[email protected]/3.8.3/Frameworks/Python.framework/Versions/3.8/include/python3.8/ -shared  -lpython3.8 -L/usr/local/Cellar/[email protected]/3.8.3/Frameworks/Python.framework/Versions/3.8/lib/ palindrome1.c -o palindrome1.so -v

// >>> import palindrome1
// >>> palindrome1.is_palindrom1("FD")
// False
// >>> palindrome1.is_palindrom1("ABBA")
// True

另外一种不用手工编译的方式就是使用setuptools提供的方法:

from setuptools import setup, Extension

setup(
    name = 'palindrome1',
    version = '1.0',
    ext_modules = [
        Extension('palindrome1', ['palindrome1.c'])
    ]
)
$ python3 setup.py build_ext
running build_ext
building 'palindrome1' extension
creating build
creating build/temp.macosx-10.15-x86_64-3.8
clang -Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers -I/usr/local/include -I/usr/local/opt/[email protected]/include -I/usr/local/opt/sqlite/include -I/usr/local/Cellar/[email protected]/3.8.3/Frameworks/Python.framework/Versions/3.8/include/python3.8 -c palindrome1.c -o build/temp.macosx-10.15-x86_64-3.8/palindrome1.o
creating build/lib.macosx-10.15-x86_64-3.8
clang -bundle -undefined dynamic_lookup -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk build/temp.macosx-10.15-x86_64-3.8/palindrome1.o -L/usr/local/lib -L/usr/local/opt/[email protected]/lib -L/usr/local/opt/sqlite/lib -o build/lib.macosx-10.15-x86_64-3.8/palindrome1.cpython-38-darwin.so
$ cd build/lib.macosx-10.15-x86_64-3.8
$ PYTHONPATH=$PYTHOPATH:./ python3
>>> import palindrome1
>>> palindrome1.is_palindrom1("daf")
False
>>> palindrome1.__file__
'/Users/liqingshou/Work/xiaochai/batman/Python/ext/build/lib.macosx-10.15-x86_64-3.8/palindrome1.cpython-38-darwin.so'
>>>

程序打包的概念与PHP的phar文件一样,在Python中有两种格式egg和wheel(后缀为whl),wheel将会逐渐取代egg。另外pi2exe扩展可以打包成windows平台的exe文件,而且不需要用户安装额外的解释器。

通过在PyPI上注册还可以让别人使用pip来安装你开发的包,这块可以参考官方教程

# setuptools可以帮忙处理一些事情
# 运行python3 setup.py install将在dist目录下生成egg文件
# setup.py文件内容
from setuptools import setup

setup(
    name = "hello",
    version = "1.0",
    description = "simple example",
    author = "Lee",
    py_modules = ["hello"]
)

Python的MRO

Python部署web开发程序的几种方法

CGI、FastCGI、WSGI、uwsgi、uWSGI


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK