Python 3 知识点查漏补缺

前言

平时遇到不太理解的概念,或看到别人的写的精妙代码都会记下来,之后再研究研究。这里稍作整理记录。

列表

解释型语言和编译型语言

程序需要被翻译成机器语言才能在计算机上运行。通过编译器先将程序编译成可执行文件,然后在运行,这个过程就是编译,对应的语言就是编译型语言,这样做的好处是只要一次编译,就可以一直运行,这样程序的运行效率很高;坏处就是不同的平台需要不同的编译器,这对跨平台开发不利。

另外一种方式就是在程序运行的时候将它翻译成机器语言,完成这项任务的部件叫解释器,过程就叫解释,这样付出的代价就是运行速度会变慢,但是在开发那种对速度要求不高、需要跨平台的程序效果很好。何况运行速度只是相对来说的。

最常用的Python解释器名为CPython.

动态类型语言和静态类型语言

动态类型语言就是程序在运行期间才会做数据类型检查;相对应的静态类型语言是在编译阶段就会检查数据类型,典型的就是C语言,需要在声明变量的时候指明变量类型。

Python中的常量、特殊变量、共有变量和私有变量

常量

Python中没有专门的关键词表明一个变量是常量并组织代码修改其值(参考C语言的const关键词)。

Python中约定使用全大写的变量名表示常量,需要注意的是,对于解释起来说大写小写都是一样的,用起来也是一样的,这样做只是为了阅读或者编写代码方便。

特殊变量

特殊变量一般以__xx__格式出现,常见的有__name____file__,一般类还会有__init__(), __get__(), __set__()之类的。

公有私有变量

私有变量或方法以_xxx__xxx 格式命名,如果代码实现在类中,作用域确实会生效。但是如果代码只是一个函数式的代码文件里,则它们只是名字不同的函数而已。

ASCII、Unicode和UTF-8的关系

编写Python使用最多的编码是UTF-8,同样浏览器交互中用的最多的编码也是UTF-8。各个编码直接的关系是什么呢?

重申一个概念:计算机交流的方式本质就是二进制的码流,是人类对码流的格式进行了约定(互联网协议的本质)。通过这些约定,发送方对要传输的内容进行编码,转化为码流后被接收方收到,按照同样的约定解码,得到适合人类阅读的内容。

最开始的时候,人类只需要用英文和数字交流,于是就发明了ASCII编码(全称American Standard Code for Information Interchange),这种编码用8位的字节(2^8)对数字、字母、符号和几个常用的控制操作进行了编码。

随着计算机发展,世界各地都有了计算机,这些计算机之间也要交流。不过大家都是闭门造车,因此各自就发明了针对本地语言的编码方式。对于中文而言,也有对应的编码方式,由于有个发展过程,中文编码方式还不止一种,它们一般是新的兼容旧的。最典型的就是GB2312和其扩展版GBK。

世界开始交流了,于是有人发明了unicode企图用同一种编码涵盖世界上所有的文字系统。然而unicode的设计有个不好的地方就是为了兼容其它语言,每个字符的所需的字节数高于ACSII里一个英文字符所需的字节数,这样如果一个网站大多数内容都是英文的,就会造成资源浪费。

于是有识之人,就在unicode上实现了一个新版本,就是UTF系列。UTF是unicode的一种实现。UTF编码最大的特点就是其编码是变长的,并且通过一些简单的转换规则兼容个了unicode的所有编码。UTF系列也有很多,比如说流传最广的UTF-8代表传输数据的单位长度是8位。

Python列表的切片操作

切片操作很简单,也常用。我最近发现其实切变操作的[]中其实是由两个:的,和for循环一样。

>>> L = [i for i in range(10)]
>>> L
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[2:9:2]
[2, 4, 6, 8]
>>> L[9:2:-1]
[9, 8, 7, 6, 5, 4, 3]
>>> L[:] == L[::]
True

可以看到[]实际上是有三个值的,即[start : end : step]规则和for一样,start默认是头,end默认是尾,step默认是1。

Python中的迭代写法

可以迭代的对象有很多,可以通过isinstance(item, Iterable)来判断。如果使用for语句来迭代这些对象的话,除了常规写法,for语句还支持直接迭代多个增量。

>>> L = [i for i in range(10)]
>>> for i, value in enumerate(L):
...     print (i, value)
...
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
>>> for i, j, k in [(2,3, 3),(2,3,3)]:
...     print (i, j, k)
...
2 3 3
2 3 3

Python的列表生成式

带条件语句的:

>>> L = [x*x for x in range(10) if x %2 == 0]
>>> L
[0, 4, 16, 36, 64]

带两个for的,:

>>> K = [x+y for x in range(3) for y in range(3)]
>>> K
[0, 1, 2, 1, 2, 3, 2, 3, 4]

相当于:

K = []
for i in range(3):
    for j in range(3):
        K.append(i+j)

Python的生成器:generator

个人以为生成器是Python最有用的特性之一,同时也是用的最少的特性之一。生成器保存一个生成某种序列的算法,除非其内置方法next()被调用,否则它不会进行任何实际的运算,这恰恰也是它厉害的地方。

和生成器紧密相连的关键词是yield, 它一般用于生成器的实现函数里。在使用生成函数时,每次调用next()的时候函数会执行,遇到yield语句返回,再次调用next()时会从上次返回的yield语句处继续执行,直到 StopIteration 异常出现。

举个例子,我现在要一个类似于[1,-2,3,-4,5,-6...]的序列:

def list_generater():
    f = 1
    for i in range(10):
        yield (i+1)*f
        f = f * (-1)

g = list_generater()
L = [next(g) for i in range(6)]

如果你只是需要一个简单的[1,4,9,16,25...]的序列,你只需要:

g = (x * x for x in range(10))

()改成[]就是list了。

返回函数

简单来说就是函数的返回值是函数,Python有个可以在函数中再定义一个函数的特性(虽然我在C代码中从来没见过,但是这仍让我产生了C语言是不是也可以的疑问,后来去试了一下,是不可以的!),这种函数叫嵌套函数(nested function)。

闭包

因为作用域的存在,函数中的变量在函数调用完就会被释放了(生命周期到了)。闭包提供了一种保存这种变量的方法:

def f1():
    local_var = 1
    def f2():
        print(local_var)
    return f2

f = f1()
f()

理论上在第7行结束后,local_var被释放了(它确实被释放了,后面会到),但是通过某种方法,即f(),我们仍然能得到local_var的值。

那Python是怎么实现的呢?其实每个函数都有一个名为__closure__的特殊值。我们看这个代码:

def f1():
    local_var1 = 1
    local_var2 = 3
    def f2():
        print(local_var1, local_var2)
    return f2

f = f1()
fc = f.__closure__

print (f1.__closure__)
print (fc)
print (type(fc[0]))
print (fc[0].cell_contents)
print (fc[1].cell_contents)
# 输出
# None
# (, )
# 
# 1
# 3

fc也就是f2()的__closure__值其实是一个tuple,包含了2个类cell的实例,通过访问其cell_contents可以得到两个本地变量的值,由此可见,并不是闭包突破作用域的限制了,只是闭包实现的时候将这些变量保存下来了。

廖老师在教程里写到,使用闭包的时候应该避免使用循环变量,因为循环变量会递增。其实用还是可以用的,只是要理解代码的正确运作方式:

def count():
    fs = []
    for i in range(1, 5):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3, f4 = count()

print (f1(), f2(), f3(), f4())
# 16 16 16 16
# f1.__closure__[0].cell_contents 的值是4

像这个代码,fs中每个方法f都在其cell_contents里记录了这个i的值,只是这个值需要到执行return fs的时候才能确定下来,因为此时for循环已经结束,所以i的值是4,自然f1、f2、f3、f4里的i值都是4

那怎么解决呢?我看到一种方法就是在闭包的外侧再封装一层。但是其实这种情况我们不必拘泥于闭包的概念,如果你只是想返回类似于f1()=1, f2()=4, f3()=9...的结果,直接使用嵌套函数就行:

def count():
    fs = []
    for i in range(1, 5):
        def f(j=i):
             return j*j
        fs.append(f)
    return fs

f1, f2, f3, f4 = count()

print (f1(), f2(), f3(), f4())
print (f1.__closure__)
# 1 4 9 16
# None

我以为判断是不是构成闭包的关键条件就是看内部的函数有没有用到外部函数的变量,像这种改法,i作为参数传入, 内部函数使用的j其实是定值,这样就不构成闭包了,只是个嵌套函数。

匿名函数

匿名函数,顾名思义就是没有名字的函数。理论上它具备正常def语句具备的所有能力,但是它主要是用来声明和实现一些逻辑很简单,调用次数也不多的函数。它存在的目的是为了让代码简介,常和map, reducefilter一起使用。实现一个跟上文中类似的函数:

fl = lambda x: x**2
fs = []

for i in range(1,5):
    fs.append(fl(i))

print (fs)

lambda可以不带参数,也可以带一个参数,也可以带多个参数,也可以给参数带默认值。它可以被当作表达式使用,最后会返回一个函数对象。

lambda中可是使用很多的技巧,比如说lambda x, y: x if x < y else y, lambdax: list(map(sys.stdout.write, x) ,但须知这也是双刃剑,谨慎使用。

装饰器

装饰器是闭包的一个典型实现。先推荐一篇很好的关于装饰器的文章:理解 Python 装饰器看这一篇就够了。

最典型的一个场景就是日志,比如说我要记录所有被函数调用的时间,除了在每个函数中添加日志语句外,最好的方法莫过于提供一个函数,它可以记录当前函数名字和当前时间,同时让每个函数被调用的时候都来调用一次这个函数。这就是装饰器发挥作用的地方了。

带多个参数的方法的装饰器

def use_logging(fn):
    def logger_adder(*args):
        print ("{0:s} is running".format(fn.__name__))
        return fn(*args)

    return logger_adder

@use_logging
def foo(name, name2):
    print ("I am foo." + name + name2)

foo("Ethan", "12306")

这里一开始我不理解这个参数是怎么传进去的,后来发现是我自己蠢了,其实参数是不需要传的,见如下代码:

def use_logging(fn):
    def logger_adder(*args):
        print ("{0:s} is running".format(fn.__name__))
        return fn(*args)

    return logger_adder

def foo(name):
    print ("I am foo. " + name)

foo = use_logging(foo)

print (foo)
print (foo.__closure__[0].cell_contents)
# 输出
# .logger_adder at 0x000001B65C2982F0>
# 

这这个代码是等价的,其实这样就很明显了,foo变量的内容其实指向的是logger_adder的地址,因此参数直接传进去就行了。而logger_adder里因为用到了fn,形成了闭包,所以foo里的__closure__存储了foo方法的地址,这样就可以最终调用到foo方法了。

带参数的装饰器

def use_logging(level):
    def decorator(fn):
        def log_adder(*args):
            if level == 'warn':
                print ("{0:s} is running".format(fn.__name__))
            return fn(*args)

        return log_adder
    return decorator

@use_logging(level="warn")
def foo(name, name2):
    print ("I am foo." + name + name2)


foo("Ethan", "12306")

类装饰器

主要是要实现__call__方法,如下,可以计算程序运行的时间:

import time

class elapsedtime(object):
    def __init__(self, fn):
        self._fn = fn

    def __call__(self):
        t = time.time()
        self._fn()

        t = time.time() - t
        print ('elaspsed time: {0:f}s'.format(t))

@elapsedtime
def foo():
    for i in range(10000):
        i = i**i

foo()

多个装饰器

如果有多个装饰器,会依次从上往下执行。

functools模块

functools是为高层次的函数而设计的,这种函数可以调用或返回其他函数。一般来说,任何可被调用的对象都可以用到这个模块。里面包含了6个函数,这里是说一个。

偏函数

偏函数的作用是对一个函数进行封装,并传入一些自定义的参数。一个例子:

from functools import partial

basetwo = partial(int, base=2)
basetwo.__doc__ = 'Convert base 2 string to an int.'
print (int('10010', 2))
print (basetwo('10010'))

它适用于某些经常需要调用的函数,同时这些函数的某些参数是固定的。

StringIO和BytesIO

StringIO

StringIO实现了和文件操作类似的接口,它可以从一个string buffer中读写数据,和文件不同,这些数据是储存在内存里的。 除了StringIO还有个cStringIO,它们两个除了声明的时候不一样,其他都很类似。

cStringIO比StringIO快一点,它是StringIO的一个工厂函数,因此它不可以被继承,也不能设置它的属性。

但是,它们在Python 3.0以上的版本里被取代了。现在是这样的:

from io import StringIO

output = StringIO()
output.write('First line.\n')
output.write('Second line.')

contents = output.getvalue()

print (contents)
output.close()

也可以这样写:

from io import StringIO, BytesIO

with StringIO("ABCDEFGHIJKLMN") as output:
    output.write('---')
    contents = output.getvalue()

print (contents)

# 输出
# ---DEFGHIJKLMN 注意前面会被覆盖掉

也支持类似文件的seek, read方法

from io import StringIO, BytesIO

with StringIO("ABCDEFGHIJKLMN") as output:
    output.seek(2)
    output.write('---')
    
    output.seek(6)
    ccc = output.read(2)
    output.write('---')
    
    contents = output.getvalue()

print (ccc, contents)
# 输出
# GH AB---FGH---LMN

BytesIO

这个和StringIO类似,只是操作的对象是二进制数据。

with BytesIO() as output:
    output.write('中文'.encode('utf-8'))
    contents = output.getvalue()

print (contents.decode('utf-8'))

序列化和反序列化

序列化和反序列化的本质就是保存和读取数据到本地磁盘,也就是数据持久化,这样运行中的数据不会程序退出而丢失。在Python中主要有两个模块可以使用:json和pickle。后者是Python特有的,前者则经常出现在各种语言里。

其实在爬虫学习中,经常接触到json文件,不过它们都是字典类型的,理论上json也可以保存其他类型的内容,不过这可能会需要自己编写编码解码器。

json主要的方法就是dump, dumps, loadloads。关于json可以参考json — JSON encoder and decode, pickle和json模块基本一样,不同的是pickle操作的是二进制数据,同时因为pickle是Python特有的,它几乎可以支持所有python数据类型的转换。

pickle模块还有个升级版,名为shelve模块,通过一个类似字典(key:value)的操作,它可以将数据持久化到本地。它最大的优势是value的内容可以是任何Python对象。

一般来说:

  • 如果是网络开发,推荐使用json
  • 少量文件存储,推荐使用pickle
  • 大量文件存储,推荐使用shelve

给我的感觉是一个高效版本的读写文件库。个人以为,如果后端是数据库,还是使用数据库比较好。

psutil模块

psutil模块可以用来监控当前系统的资源使用情况,包括CPU、内存、磁盘和网络等。它是做系统监控,性能分析,进程管理的好东西。最关键的事,它是跨平台的。

文档在psutil。用起来很简单。

你可能感兴趣的:(Python 3 知识点查漏补缺)