py基础考察点
py语言特性
-
py是动态强类型语言
-
动态还是静态指的是编译期还是运行期确定类型
-
强类型指的是不会发生隐式类型装换
为什么使用py?
* 胶水语言,轮子多,应用广泛
* 语言灵活,生产力高
* 性能问题,代码维护问题,py2/py3不兼容
什么是鸭子类型?
- 当看到一只鸟走起来像鸭子,游泳起来像鸭子,叫起来也像鸭子,那么这只鸟就可以被称为鸭子
关注点 在对象的行为,而不是类型(duck typing)
eg: file,StringIO,socket对象都支持read/write方法
(file like object) 再比如定义__iter__魔法方法的对象可以用for迭代 鸭子类型更关注接口而非类型
class Duck:
def quack(self):
print("gua gua")
class Person:
def quack(self):
print("我是人类, 但我也会guo guo guo")
def in_the_forest(duck):
duck.quack()
def game():
donald = Duck()
john = Person()
in_the_forest(donald)
in_the_forest(john)
print(type(donald))
print(type(john))
print(isinstance(donald, Duck))
print(isinstance(john, Person))
game()
什么是monkey patch? 哪些地方用到了?自己如何实现?
- 所谓的monkey patch 就是运行时替换
//eg:gevent库需要修改内置的socket
from gevent import monkey; monkey.patch_socket()
import socket
print(socket.socket)
print("After monkey patch")
from gevent import monkey
monkey.patch_socket()
print(socket.socket)
import select
print(select.select)
monkey.patch_select()
print("After monkey patch")
print(select.select)
import time
print(time.time())
def _time():
return 1234
time.time = _time
print(time.time())
什么是自省?
-
introspection
运行时判断一个对象的类型的能力,py一切皆对象 用type,id,isinstance获取对象类型信息
inspect模块提供了更多获取对象信息的函数
ll = [1, 3, 3] d = dict(a=1) #{a:1} print(type(ll)) print(type(d)) print(isinstance(ll, list)) print(isinstance(d, dict)) def add(a, b): if isinstance(a, int): return a + b elif isinstance(a, str): return a.upper() + b print(add(1, 2)) print(add('head', 'tail')) print(id(ll)) print(id(d)) print(ll is d) print(ll is ll)
什么是列表和字典推导
List Comprehension
'''[i for i in range(10) if i % 2 == 0]
一种快速生成list/dict/set的方式。用来替代map/filter等
(i for i in range(10) if i % 2 == 0) 返回生成器'''
eg:
a = ['a', 'b', 'c']
b = [1, 2, 3]
d = {}
for i in range(len(a)):
d[a[i]] = b[i]
print(d)
d = {k: v for k, v in zip(a, b)}
print(d)
# !< output
{'a': 1, 'b': 2, 'c': 3}
{'a': 1, 'b': 2, 'c': 3}
eg:
l = [i for i in range(10)]
print(l)
print(type(l))
l = (i for i in range(10))
print(type(l))
for i in l:
print(i)
使用生成器可以大大节省内存
py之禅
import this
py2/3差异常考题
- 使用pyenv安装python版本
py3改进
print成为函数
编码问题,py3不再有unicode对象,默认str就是unicode
除法变化,py3除号返回浮点数
5/2 = 2.5
5//2 = 2
类型注解(type hint) 帮助IDE实现类型检查
def hello(name: str)->str:
return "hello" + name
print(hello("laowang"))
优化的super()方便直接调用父类函数
高级解包操作, a,b, *ret = range(10)
a, b, *c = range(10)
print(a)
print(b)
print(c)
a, b, *_ = range(10)
print(a)
print(b)
#后面没有数字被舍弃
py3改进
Keyword only arguments 限定关键字参数
eg:
#限定关键字参数需要指定参数名传参
def add(a, b, *, c):
return a + b +c
print(add(1, 2, c = 3))
Chained exceptions Py3重新抛出异常不会丢失栈信息
#raise from 保留异常栈信息
一切返回迭代器
range, zip, map, dict.values,etc
are all iterators
py3新增
* yield from 链接子生成器
* asyncio内置库, async/await 原生协程支持异步编程
新的内置库enum, mock, asyncio, ipaddress, concurrent.futures等
py3改进
- 生成的pyc文件统一到__pycache__ 一些内置库的修改, urlib, selector等
- 性能优化等
一些兼容2/3的工具
six模块
py2 to py3等工具转换代码
__future__
py函数常考题
常考点
参数传递
(不)可变对象
可变参数
以下代码分别输出什么?
* 可变类型作为参数
def flist(l):
l.append(0)
print(l)
l = []
flist(l)
flist(l)
# !< out:
[0]
[0, 0]
- 不可变类型作为参数
def fstr(s):
s += 'a'
print(s)
s = "hehe"
fstr(s)
fstr(s)
# !< out:
"hehea"
"hehea"
py如何传递参数
一个容易混淆的问题
传递值还是引用呢?都不是。唯一支持的参数传递是共享传参
Call by Object(Call by Object Reference or Call by Sharing)
Call by sharing(共享传参) 函数形参获得实参中各个引用的副本
py一切皆对象
py可变/不可变对象
搞懂可变和不可变内置对象有利于理解函数参数的副作用
哪些是可变和不可变对象?
不可变对象 bool/int/float/tuple/str/frozenset
可变对象 list/set/dict
def clear_list(l):
l = []
ll = [1, 2, 3]
clear_list(ll)
print(ll)
py可变参数作为默认参数 记住默认参数只计算一次
def flist(l = [1]):
l.append(l)
print(l)
fl()
fl()
py *args **kwargs
函数传递中*args, **kwags 含义是?
用来处理可变参数
*args被打包成tuple
**kwargs被打包成dict
def print_multiple_args(*args):
print(type(args), args)
for idx, val in enumerate(args):
print(idx, val)
print_multiple_args('a', 'b', 'c')
print_multiple_args(*['a', 'b', 'c'])
# !< out:
<class 'tuple'> ('a', 'b', 'c')
0 a
1 b
2 c
def print_kwargs(**kwargs):
print(type(kwargs))
for k, v in kwargs.items():
print('{}: {}'.format(k, v))
print_kwargs(a=1, b=2)
print_kwargs(**dict(a=1, b=2))
# !< out:
<class 'dict'>
a: 1
b: 2
def print_all(c, *args, **kwargs):
print(c)
if args:
print(args)
if kwargs:
print(kwargs)
print_all(1, "hello", a="muke")
1
('hello',)
{'a': 'muke'}
py异常机制常考题
什么是py的异常?
py使用异常处理错误(有些语言使用错误码)
BaseException
SystemExit / KeyboardInterrupt / GeneratorExit
Exception
使用异常的常见场景
什么时候需要捕获处理异常?看py内置异常的类型
网络请求(超时, 连接错误等)
资源访问(权限问题, 资源不存在)
代码逻辑(越界访问, KeyError等)
如何处理py异常
搞懂几个关键字
try:
#func #可能会抛出异常的代码
except (Exception1, Exception 2) as e: #可以捕获多个异常并处理
#异常处理的代码
else:
#pass #异常没有发生的时候代码逻辑
finally:
#pass #无论异常有没有发生都会执行的代码, 一般处理资源的关闭和释放
如何自定义异常
如何自定义自己的异常?为什么需要定义自己的异常?
继承Exception实现自定义异常(为什么不是BaseException)
给异常加上一些附加信息
处理一些业务相关的特定异常(raise MyException)
class MyException(Exception):
pass
try:
raise MyException('my exception')
except MyException as e:
print(e)
py性能分析与优化,GIL常考题
什么是Cpython GIL
GIL, Global Interpreter Lock
-
Cpython解释器的内存管理并不是线程安全的
-
保护多线程情况下对python对象的访问
-
Cpython使用简单的锁机制避免多个线程同时执行字节码
GIL的影响
限制程序的多核执行
同一个时间只能有一个线程执行字节码
CPU密集程序难以利用多核优势
IO期间会释放GIL, 对IO密集程序影响不大
如何规避GIL影响
区分CPU和IO密集程序
CPU密集可以使用多进程+进程池
IO密集使用多线程/协程
cpython扩展
GIL的实现
# 请问这段代码输出?
import threading
n = [0]
def foo():
n[0] = n[0] + 1
n[0] = n[0] + 1
threads = []
for i in range(5000):
t = threading.Thread(target=foo)
threads.append(t)
for t in threads:
t.start()
print(n)
# !< 加锁操作
import threading
lock = threading.lock()
n = [0]
def foo():
with lock:
n[0] = n[0] + 1
n[0] = n[0] + 1
threads = []
for i in range(5000):
t = threading.Thread(target=foo)
threads.append(t)
for t in threads:
t.start()
print(n)
为什么有了GIL还要关注线程安全?
py中什么操作才是原子的?
一步到位执行完
一个操作如果是一个字节码指令可以完成就是原子的
原子的是可以保证线程安全的
使用dis操作来分析字节码
为什么有GIL还要关注线程安全?
#原子操作
import dis
def update_list(l):
l[0] = 1 #原子操作, 不用担心线程安全问题
#dis.dis(update_list)
"""
280 0 LOAD_CONST 1 (1)
2 LOAD_FAST 0 (l)
4 LOAD_CONST 2 (0)
6 STORE_SUBSCR #单字节码操作,线程安全
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
"""
#非原子操作 不是线程安全
def incr_list(l):
l[0] += 1 #危险!!不是原子操作
dis.dis(incr_list)
"""
295 0 LOAD_FAST 0 (l)
2 LOAD_CONST 1 (0)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 2 (1)
10 INPLACE_ADD #需要多个字节码操作, 有可能在线程执行
#过程中切到其它线程
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
"""
如何剖析程序性能
使用各种profile工具(内置或第三方)
- 二八定律,大部分时间耗时在少量代码上
- 内置的profile/cprofile等工具
- 使用pyflame(uber开源)的火焰图工具
服务端性能优化措施
web应用一般语 言不会成为瓶颈
- 数据结构与算法优化
- 数据库层:索引优化 慢查询消除 批量操作减少IO,NoSQL
- 网络IO:批量操作, pipeline操作 减少IO
- 缓存:使用内存数据库 redis/memcached
- 异步:asyncio celery
- 并发:gevent/多线程
py生成器与协程
Generator
- 生成器就是生成值得函数
- 当一个函数有了yield关键字就成了生成器
- 生成器可以挂起执行并且保持当前执行的状态
什么是生成器
def simple_gen():
yield 'hello'
yield 'world'
gen = simple_gen()
print(type(gen)) #'generator' object
print(next(gen)) #'hello'
print(next(gen)) #'world'
基于生成器的协程
py3之前没有原生协程,只有基于生成器的协程
pep342(Coroutines via Enhanced Generators)增强生成器功能
生成器可以通过yield暂停执行和产出数据
同时支持send()向生成器发送数据和throw()向生成器抛异常
# !< Generator Based Coroutine
def coro():
hello = yield 'hello' #yield关键字在右边作为表达式, 可以被send值
yield hello
c = coro()
#输出'hello' 这里调用next产出第一个值'hello', 之后函数暂停
print(next(c))
#再次调用send发送值 此时hello变量赋值为'world' 之后yield产出hello变量的值'world'
print(c.send('world'))
#之后协程结束, 后续再send值会抛异常StopIteration
协程注意点
协程需要使用send(None)或者next(coroutine)来预激prime才能启动
在yield处协程会暂停执行
单独的yield value 会产出值给调用方
可以通过coroutine.send(value)来给协程发送值,发送的值会赋值给yield表达式左边的变量
value = yield
协程执行完成后(没有遇到下一个yield语句)会抛出StopIteration异常
协程装饰器
避免每次都要用send预激它
from functools import wraps
def coroutine(func): #不用每次都用send(None)启动
"""装饰器:向前执行到第一个'yield'表达式,预激`func`"""
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
py3原生协程
py3.5引入async/await支持原生协程(native coroutine)
import asyncio
import datetime
import random
async def display_date(num, loop):
end_time = loop.time() + 50.0
while True:
print('Loop: {} Time: {}'.format(num, datetime.datetime.now()))
if(loop.time() + 1.0) >= end_time:
break
await asyncio.sleep(random.randint(0, 5))
loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
loop.run_forever()
单元测试
什么是单元测试
Unit Testing
针对程序模块进行正确性检验
一个函数, 一个类进行验证
自底向上保证程序正确性
为什么要写单元测试
三无代码不可取(无文档, 无注册, 无单测)
- 保证代码逻辑的正确性(甚至有些采用测试驱动开发(TDD))
- 单测影响设计, 易测的代码往往是高内聚低耦合的
- 回归测试, 防止改一处整个服务不可用
单元测试相关的库
- nose/pytest 较为常用
- mock模块用来模拟替换网络请求等
- coverage统计测试覆盖率
def binary_search(array, target):
if not array:
return -1
beg, end = 0, len(array)
while beg < end:
mid = beg + (end - beg) // 2 #py3
if array[mid] == target:
return mid
elif array[mid] > target:
end = mid
else:
beg = mid + 1
return -1
def test():
"""
如何设计测试用例:等价类划分
- 正常值功能测试
- 边界值(eg 最大最小, 最左最右值)
- 异常值(eg None, 空值, 非法值)
:return:
"""
#正常值, 包含有和无两种结果
assert binary_search([0, 1, 2, 3, 4, 5], 1) == 1
assert binary_search([0, 1, 2, 3, 4, 5], 6) == -1
assert binary_search([0, 1, 2, 3, 4, 5], -1) == -1
#边界值
assert binary_search([0, 1, 2, 3, 4, 5], 0) == 0
assert binary_search([0, 1, 2, 3, 4, 5], 5) == 5
assert binary_search([0], 0) == 0
# #异常值
assert binary_search([], 1) == -1
#pip install pytest
#pytest xx.py
py深拷贝和浅拷贝
深拷贝与浅拷贝的区别
什么是深拷贝?什么是浅拷贝?
py中如何实现深拷贝?