阿巩:
坚决不咕咕咕!!
你好啊,我是阿巩。转眼已连续更新一周了,可咱毕竟是讲Python的公众号,不来点Python基础干货就有些说不过去,就像茶馆里没有茶、犬舍里没有狗子、老婆饼里没有老婆(都什么乱七八糟的比喻?!)之前有写过篇万字长文,今天来根据面试常问的内容整理下,做个精编版。日拱一卒,让我们开始吧!
Python3标准数据类型:
其中不可变类型:Number(数字)String(字符串)、Tuple(元组);
可变类型:List(列表)、Dictionary(字典)、Set(集合)。
可变/不可变对象
不可变对象,该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
可变对象,该对象所指向的内存中的值可以被改变。当引用改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,即原地改变。
字符串
Python的字符串支持索引、切片和遍历等操作。
Python的字符串不可变,要想改变,只能通过创建新的字符串完成。
实现拼接字符串用str1+= str2即可。
常用函数:
列表和元组
列表和元组,都是一个可以放置任意数据类型的有序集合。其中列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素;而元组是静态的,长度大小固定,无法增加删减或者改变。
list和tuple的内部实现都是array的形式,list因为可变,所以是一个over-allocate的array,tuple因为不可变,所以长度大小固定。
l = [1, 2, 3, 4]
l[-1]
4
tup = (1, 2, 3, 4)
tup[-1]
4
l = [1, 2, 3, 4]
l[1:3] # 返回列表中索引从1到2的子列表
[2, 3]
tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从1到2的子元组
(2, 3)
l = [[1, 2, 3], [4, 5]] # 列表的每一个元素也是一个列表
tup = ((1, 2, 3), (4, 5, 6)) # 元组的每一个元素也是一个元组
list((1, 2, 3))
[1, 2, 3]
tuple([1, 2, 3])
(1, 2, 3)
列表和元组常用的内置函数:
字典和集合
字典是一系列由键(key)和值(value)配对组成的元素的集合。相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在O(1)时间复杂度内完成。字典和集合的内部结构都是一张哈希表。
合并字典
# 两个字典
dict1 = {'a': 10, 'b': 8}
dict2 = {'d': 6, 'c': 4}
# 方法一:
dict1.update(dict2)
print(dict1)
# 方法二:
dic = {**dict1, **dict2}
print(dic)
# 输出:{'a': 10, 'b': 8, 'd': 6, 'c': 4}
遍历字典
d = {'name': 'jason', 'dob': '2000-01-01', 'gender': 'male'}
for k in d: # 遍历字典的键
print(k)
# name
# dob
# gender
for v in d.values(): # 遍历字典的值
print(v)
# jason
# 2000-01-01
# male
for k, v in d.items(): # 遍历字典的键值对
print('key: {}, value: {}'.format(k, v))
# key: name, value: jason
# key: dob, value: 2000-01-01
# key: gender, value: male
按键/值对字典排序
d1 = {3: 'three', 1: 'one', 2: 'two'}
# 按键排序
print(sorted(d1.items(), key=lambda k: k[0]))
# 输出:[(1, 'one'), (2, 'two'), (3, 'three')]
d2 = {'three': 3, 'one': 1, 'two': 2}
# 按值排序
print(sorted(d2.items(), key=lambda k: k[1]))
# 输出:[('one', 1), ('two', 2), ('three', 3)]
有序字典
collections模块的OrderedDict,所谓有序而非字典序,指的是元素的插入顺序。
Python3 数据结构
单链表节点
class SingleNode(object):
"""单链表的结点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
双向链表节点
class Node(object):
"""双向链表节点"""
def __init__(self, item):
self.item = item
self.next = None
self.prev = None
将列表当作栈:列表的append()和pop()方法
>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
将列表当作队列:deque的append()和popleft()方法
>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry")
>>> queue.append("Graham")
>>> queue.popleft()
'Eric'
>>> queue.popleft()
'John'
>>> queue
deque(['Michael', 'Terry', 'Graham'])
列表推导式
提供了一种简便方法创建列表。
[i for i in range(10) if i % 2 == 0]
s = [x*y for x in range(1, 5) if x > 2 for y in range(1, 4) if y < 3]
print(s)
# 输出 [3, 6, 4, 8]
# 等同于:
for x in range(1, 5):
if x > 2:
for y in range(1, 4):
if y < 3:
x*y
输入和输出
Python3中input()函数接受任意标准输入数据,返回String类型。
格式化输出可使用f‘{}’来代替.format()
a = 1
b = 2
s = a + b
print(f'Sum of a and b is {s}')
# 输出:Sum of a and b is 3
文件输入和输出
注意所有 I/O 都应该进行错误处理。
with open
with语句相当于在open加上try-except-finally,用with语句的好处就是到达语句末尾时会自动关闭文件。with语句实际上是一个非常通用的结构,允许你使用所谓的上下文管理器。上下文管理器是支持两个方法的对象:__enter__和 __exit__。
JSON序列化
JSON是一种轻量级的数据交换格式,它的设计意图是把所有事情都用设计的字符串来表示。实际应用中遇到多种数据类型混在一起的情况可使用JSON序列化处理:
当开发一个第三方应用程序时,可以通过 JSON 将用户的个人配置输出到文件,方便下次程序启动时自动读取。这也是现在普遍运用的成熟做法。
条件与循环
Python 不支持 switch 语句,因此,当存在多个条件判断时,我们需要用elif实现。
for循环
Python 中的数据结构只要是可迭代的比如列表、集合等等,那么都可以通过下面这种方式遍历:
for item in <iterable>:
...
使用range() 函数,拿到索引,再去遍历访问集合中的元素。
l = [1, 2, 3, 4, 5, 6, 7]
for index in range(0, len(l)):
if index < 5:
print(l[index])
当同时需要索引和元素时,使用Python 内置的函数 enumerate()。
l = [1, 2, 3, 4, 5, 6, 7]
for index, item in enumerate(l):
if index < 5:
print(item)
while循环
l = [1, 2, 3, 4]
index = 0
while index < len(l):
print(l[index])
index += 1
异常处理
try:后放会出现异常的代码;except后方要捕获的异常,捕获所有异常用Exception;as后为别名;finally后为无论如何都会执行的代码。
def fun():
try:
print('try--start')
a = 1/0
except ValueError as ret:
print(ret)
finally:
return 'finally'
print(fun())
# 输出:
try--start
finally
捕获多个异常
def model_exception(x, y):
try:
a = x / y
except(ZeroDivisionError, NameError, TypeError):
print('one of ZeroDivisionError or NameError or TypeError happend')
# 调用函数结果
model_exception(2, 0)
函数
def name(param1, param2, ..., paramN):
statements
return/yield value # optional
python支持函数嵌套
def connect_DB():
def get_DB_configuration():
...
return host, username, password
conn = connector.connect(get_DB_configuration())
return conn
我们只能通过调用外部函数 connect_DB() 来访问get_DB_configuration()
闭包
和嵌套函数类似,不同在于外部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋予一个变量,这个变量可以在后面被继续执行调用。
不定长参数
当参数个数不确定时使用不定长参数,有两种类型分别为*args和**kwargs。
加了*的参数会以元组tuple的形式导入,而**的参数以字典形式导入。
匿名函数
python使用lambda来创建匿名函数。
sum = lambda arg1, arg2: arg1 + arg2
函数式编程
map() 、filter()、reduce()
map()函数:对iterable中的每个对象都运行func函数,最后返回一个新的可遍历的集合。
l = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, l) # [2, 4, 6, 8, 10]
filter()函数:对 iterable 中的每个元素,都使用 func 判断并返回 True 或者 False,最后将返回 True 的元素组成一个新的可遍历的集合。
l = [1, 2, 3, 4, 5]
new_list = filter(lambda x: x % 2 == 0, l) # [2, 4]
reduce()函数:它通常用来对一个集合做一些累积操作。比如要计算某个列表元素的乘积。
l = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, l) # 1*2*3*4*5 = 120
迭代器和生成器
在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。列表(list: [0, 1, 2]),元组(tuple: (0, 1, 2)),字典(dict: {0:0, 1:1, 2:2}),集合(set: set([0, 1, 2]))都是容器。对于容器,你可以很直观地想象成多个元素在一起的单元;而不同容器的区别,正是在于内部数据结构的实现方法。
容器是可迭代对象,可迭代对象调用 iter() 函数,可以得到一个迭代器。迭代器可以通过 next() 函数来得到下一个元素,从而支持遍历。调用next()方法后,你要么得到这个容器的下一个对象,要么得到一个 StopIteration 的错误。
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
x = self.a
self.a += 1
return x
myclass = MyNumbers()
myiter = iter(myclass)
生成器是一种特殊的迭代器,它包含yield。每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行(yield就有点像断点)。
可以使用yield来读取文件,如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用,而yield利用固定长度的缓冲区来不断读取文件内容。
def read_file(fpath):
BLOCK_SIZE = 1024
with open(fpath, 'rb') as f:
while True:
block = f.read(BLOCK_SIZE)
if block:
yield block
else:
return
python中的参数传递方式
引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。
通过一个函数来改变某个变量的值,通常有两种方法:第一种直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改;第二种是创建一个新变量,来保存修改后的值,然后将其返回给原变量。在实际工作中,我们更倾向于使用后者,因为其表达清晰明了,不易出错。
python变量及其赋值
面向对象编程
四要素:封装、继承、多态、抽象。
封装是基础。抽象和多态依赖于继承实现。
构造函数:用def __init__(self, args...)声明,第一个参数self代表当前对象的引用,其他参数是在对象化时需要传入的属性值;构造函数在一个对象生成时(即实例化时)会被自动调用。
__init__用来初始化,__new__用来生成一个实例。
成员函数:是正常的类的函数,第一个参数必须是self;可通过此函数来实现查询或修改类的属性等功能。
静态函数:属于当前类的命名空间下,且对第一个参数没有要求;一般用来做一些简单独立的任务,既方便测试也能优化代码结构;一般使用装饰器@staticmethod来声明。
类函数:类函数的第一个参数一般为cls,表示必须传一个类进来;最常用的功能是实现不同的init构造函数;需要装饰器@classmethod来声明。
元类:MetaClass是创建类的类,元类允许我们控制类的生成,比如修改类的属性等。
元类最常见的场景是ORM中。
装饰器
在不修改原函数的情况下,为函数增加功能。
编写一个记录耗时操作的装饰器:
# 用函数编写一个装饰器
import time
def log_time(func): # 接收一个函数作为参数
def _log(*args, **kwargs):
beg = time.time()
res = func(*args, **kwargs)
print(f'use time: {time.time() - beg}')
return res
return _log
@log_time # @ 为装饰器的语法糖
def mysleep():
time.sleep(1)
# mysleep()
# 等价于:
newsleep = log_time(mysleep)
# newsleep()
# 用类编写一个装饰器
class LogTime:
def __call__(self, func):
def _log(*args, **kwargs):
beg = time.time()
res = func(*args, **kwargs)
print(f'use time: {time.time() - beg}')
return res
return _log
@LogTime() # 初始化装饰器类的实例,所以此处添加‘()’
def mysleep2():
time.sleep(1)
# mysleep2()
# 给装饰器增加参数
# 使用类装饰器方便实现装饰器参数
class LogTimeParams:
def __init__(self, use_int=False):
self.use_int = use_int
def __call__(self, func):
def _log(*args, **kwargs):
beg = time.time()
res = func(*args, **kwargs)
if self.use_int:
print(f'use time: {int(time.time()-beg)}')
else:
print(f'use time: {time.time()-beg}')
return res
return _log
@LogTimeParams(True)
def mysleep():
time.sleep(1)
mysleep()
类装饰器主要依赖于函数__call__(),每当调用一个类的示例时,函数__call__()就会被执行一次。
LRU cache缓存装饰器,在 Python 中的表示形式是@lru_cache。@lru_cache会缓存进程中的函数参数和结果,当缓存满了以后,会删除 least recenly used 的数据
编写一个用于身份认证的装饰器:
import functools
def authenticate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
request = args[0]
if check_user_logged_in(request): # 如果用户处于登录状态
return func(*args, **kwargs) # 执行函数post_comment()
else:
raise Exception('Authentication failed')
return wrapper
@authenticate
def post_comment(request, ...)
...
设计模式
工厂模式:
"""
什么是工厂模式?
解决对象创建问题
解耦对象的创建和使用
包括工厂方法和抽象工厂
"""
# 工厂模式例子
class DogToy:
def speak(self):
print('wang wang')
class CatToy:
def speak(self):
print('miao miao')
def toy_factory(toy_type):
if toy_type == 'dog':
return DogToy()
if toy_type == 'cat':
return CatToy()
单例模式:
"""
什么是单例模式?
一个类只能创建同一个实例,无论创建多少个实例,都是同一个对象
Python的模块其实就是单例的,只会import一次,在模块中定义的全局变量就是单例的
使用共享同一个实例的方式创建单例模式
"""
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
_instance = super().__new__(cls, *args, **kwargs)
cls._instance = _instance
return cls._instance
class Myclass(Singleton):
pass
c1 = Myclass()
c2 = Myclass()
assert c1 is c2 # c1和c2是同一个实例
Python对象的比较和拷贝
'==' 与 'is'
'=='操作符比较对象之间的值是否相等。执行a == b相当于是去执行a.__eq__(b),而 Python 大部分的数据类型都会去重载__eq__这个函数,其内部的处理通常会复杂一些。
比较操作符'is'效率优于'==',因为'is'操作符无法被重载,执行'is'操作只是简单的获取对象的 ID,并进行比较;而'=='操作符则会递归地遍历对象的所有值,并逐一比较。
浅拷贝和深拷贝
浅拷贝,是指重新分配一块内存,创建新对象,其内容非原对象本身的引用,而是原对象内第一层对象的引用。浅拷贝有三种形式:切片操作、工厂函数、copy 模块中的 copy 函数。
深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。深拷贝只有一种形式,copy 模块中的 deepcopy()函数。深拷贝和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。因此,它的时间和空间开销要高。
对于元组,使用 tuple() 或者切片操作符':'不会创建一份浅拷贝,相反,它会返回一个指向相同元组的引用。
拷贝注意点:
对于非容器类型,如数字、字符,以及其他的“原子”类型,没有拷贝一说,产生的都是原对象的引用。
如果元组变量值包含原子类型对象,即使采用了深拷贝,也只能得到浅拷贝。
Python协程
协程是实现并发编程的一种方式,是用户态的线程,由用户决定在什么地方交出控制权,切换到下一个任务。以下为使用协程写异步爬虫程序:
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
for task in tasks:
await task
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 输出 ##########
crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 3.99 s
协程执行的三步:
并发和并行
并发,通过线程和任务之间互相切换的方式实现,但同一时刻,只允许有一个线程或任务执行。通常应用于 I/O 操作频繁的场景,比如从网站上下载多个文件,I/O 操作的时间可能会比 CPU 运行处理的时间长得多。
并行,则是指多个进程同时执行。更多应用于 CPU heavy 的场景,比如 MapReduce 中的并行计算,为了加快运行速度,一般会用多台机器、多个处理器来完成。
单线程与多线程性能比较
#单线程实现方式
def download_all(sites):
for site in sites:
download_one(site)
#多线程实现方式
def download_all(sites):
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
executor.map(download_one, sites)
这里创建了一个线程池,总共有 5 个线程可以分配使用。executer.map() 与前面所讲的 Python 内置的 map() 函数类似,表示对 sites 中的每一个元素,并发地调用函数 download_one()。
并发编程之Asycio
Sync(同步)和Async(异步)
Sync同步,是指操作一个接一个地执行,下一个操作必须等上一个操作完成后才能执行。
Async异步,是指不同操作间可以相互交替执行,如果其中的某个操作被 block 了,程序并不会等待,而是会找出可执行的操作继续执行。
Asyncio 工作原理
Asyncio 是单线程的,但其内部 event loop 的机制,可以让它并发地运行多个不同的任务,并且比多线程享有更大的自主控制权。
假设任务只有两个状态:一是预备状态;二是等待状态。event loop 会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务,使其运行,一直到这个任务把控制权交还给 event loop 为止。当任务把控制权交还给 event loop 时,event loop 会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成。如果完成,则将其放到预备状态的列表;如果未完成,则继续放在等待状态的列表。周而复始,直到所有任务完成。
Asyncio 用法
Asyncio 版本的函数 download_all():
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)
如何选择多线程还是Asyncio
Python多进程和多线程如何创建
Python GIL
GIL全局解释器锁,每一个 Python 线程,在 CPython 解释器中执行时,都会先锁住自己的线程,阻止别的线程执行。
由于CPython解释器的内存管理并不是线程安全的,为了保护多线程下对Python对象的访问引入了GIL锁。
GIL的影响:同一时间只能有一个线程执行字节码,CPU密集程序难以利用多核优势。IO期间由于会释放锁,对IO密集程序影响不大。
如何规避GIL影响:
为什么有了GIL还要关注线程安全:python中只有原子操作是可以保证线程安全的,即一个操作如果是一个字节码指令可以完成就是原子的。
Python的内存管理机制
python的内存管理机制包括内存池机制及垃圾回收机制。
Python的垃圾回收机制
以引用计数为主,标记清除和分代回收为辅,其中标记清除和分代回收解决循环引用的问题。
引用计数
函数内部声明的局部变量,在函数返回后,局部变量的引用会注销掉;此时变量指代对象的引用数为 0,Python 便会执行垃圾回收。
s.getrefcount() 这个函数,可以查看一个变量的引用次数。
手动释放内存方法:先调用” del 变量名 “删除对象的引用;然后强制调用 gc.collect(),清除没有引用的对象。
标记清除
标记清除算法:遍历并标记一个有向图,在遍历结束后,未被标记的节点即为不可达节点,需要进行垃圾回收。(实现方法:dfs (深度优先搜索)遍历,从起点开始遍历,对遍历到的节点做个记号。遍历完成后,再对所有节点扫一遍,没有被做记号的,就是需要垃圾回收的。)
只有容器类对象才有可能产生循环引用。
分代回收
Python 将所有对象分为三代。刚刚创立的对象是第 0 代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收。 gc.get_threshold()可查看三代阈值。