python实践中杂项
python的模块化
sys.path可以查看项目的寻找模块的路径
pycharm默认会将当前项目的根路径加入到sys.path中,并且是加入到很前的位置,即程序跑起来位置的,下一个位置。
Python 是脚本语言,和 C++、Java 最大的不同在于,不需要显式提供 main() 函数入口。那么下面的代码作用是什么呢?
if __name__ == '__main__':
print('Hello World')
import 在导入文件的时候,会自动把所有暴露在外面的代码全都执行一遍,为了importst时不让外面的代码执行一遍,可以放到main里面去。为什么呢?其实,__name__
作为 Python 的魔术内置参数,本质上是模块对象的一个属性。我们使用 import 语句时,__name__
就会被赋值为该模块的名字,自然就不等于 __main__
了。只有在跑起来的那个脚本__name__
才是main
导包原则:在大型工程中模块化非常重要,模块的索引要通过绝对路径来做,而绝对路径从程序的根目录开始。即设定根路径到sys.path中,设置方式可以查一下。pycharm自动做到了这点。
python中的类
__开头的属性是私有属性
self.__context = context # 私有属性
类函数、成员函数和静态函数。静态函数与类没有什么关联,最明显的特征便是,静态函数的第一个参数没有任何特殊性,静态函数可以用来做一些简单独立的任务,既方便测试,也能优化代码结构。静态函数还可以通过在函数前一行加上 @staticmethod 来表示,代码中也有相应的示例。类函数的第一个参数一般为 cls,表示必须传一个类进来。类函数最常用的功能是实现不同的 init 构造函数,比如上文代码中,我们使用 create_empty_book 类函数,来创造新的书籍对象,其 context 一定为 'nothing'
。这样的代码,就比你直接构造要清晰一些。类似的,类函数需要装饰器 @classmethod 来声明。
成员函数则是我们最正常的类的函数,它不需要任何装饰器声明,第一个参数 self 代表当前对象的引用,可以通过此函数,来实现想要的查询 / 修改类的属性等功能。
class Document():
WELCOME_STR = 'Welcome! The context for this book is {}.'
def __init__(self, title, author, context):
print('init function called')
self.title = title
self.author = author
self.__context = context
# 类函数
@classmethod
def create_empty_book(cls, title, author):
return cls(title=title, author=author, context='nothing')
# 成员函数
def get_context_length(self):
return len(self.__context)
# 静态函数
@staticmethod
def get_welcome(context):
return Document.WELCOME_STR.format(context)
类的继承
class Sub(Parent):
def __init__(self, sub_name):
self.sub_name = sub_name
# 调用父类的构造函数了
Parent.__init__(self, 'Parent')
def print_sub_parent(self):
print('-'.join((self.sub_name, self.name)))
class Parent(object):
def __init__(self, name):
self.name = name
python对象的比较
==
是值比较,执行a == b
相当于是去执行a.__eq__(b)
,而 Python 大部分的数据类型都会去重载__eq__
这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__
函数会去遍历列表中的元素,比较它们的顺序和值是否相等。is
是比较的是对象的身份标识是否相等,在python中,对象标识符能通过id(object)获取。
出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。这样,每次你试图创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。但是,如果整型数字超过了这个范围,比如上述例子中的 257,Python 则会为两个 257 开辟两块内存区域,因此 a 和 b 的 ID 不一样,a is b
就会返回 False 了。
对于不可变(immutable)的变量,如果我们之前用'=='
或者'is'
比较过,结果是不是就一直不变了呢?不是的,因为不可变对象可以嵌套可变对象,比如元组嵌套列表。
python的浅拷贝和深拷贝
import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2], (30, 40)]
python中的值传递和引用传递
准确地说,Python 的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。个人认为,这是殊途同归。不可变对象对应值传递。可以和java传递值,传递String引用做对比。
python中的装饰器
在python中,函数也是对象,我们可以把函数赋予变量,这样就可以用变量调用函数。
我们可以把函数当作参数,传入另一个函数中。
我们可以在函数里定义函数。
函数的返回值也可以是函数(闭包)
一个简单的装饰器例子
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
def greet():
print('hello world')
greet = my_decorator(greet)
greet()
# 输出
wrapper of decorator
hello world
装饰器更优雅的表达方式
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
@my_decorator
def greet():
print('hello world')
greet()
如果装饰器需要带有参数,通常情况下,我们会把*args和**kwargs,作为装饰器内部函数wrapper()的参数。*args和*kwargs,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
带有自定义参数的装饰器
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(4)
def greet(message):
print(message)
greet('hello world')
# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
装饰器后原函数还是原函数吗
不是的,他的__name
会变成wrapper,为了解决这个问题,可以使用内置的装饰器@functools.wrap
,它会帮助保留原函数的元信息
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
@my_decorator
def greet(message):
print(message)
类装饰器
类装饰器主要依赖于函数__call_()
,每当你调用一个类的示例时,函数__call__()
就会被执行。
__call__
的用法:
stu = Stu()
# 调用了Stu类的__call__方法
stu()
```python
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
装饰器的嵌套
@decorator1
@decorator2
@decorator3
def func():
...
# 这个情况是允许的
# 等价于
decorator1(decorator2(decorator3(func)))
元类
所有的Python的用户定义类,都是type这个类的实例,用户自定义类,只不过是type类的__call__
运算符重载,metaclass是type的子类,通过替换type的__call__
运算符重载机制,"超越变形"正常的类。
myclass =MyClass()
# 这边这行代码其实调用的是type('MyClass', (), {'data': 1})
# 而之前我们说过对象引号直接加括号是调用__call__
# 所以他就是调用的type的__call__
# type的__call__做的事情包括以下
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
# 使用元类之后发生了一些变化
class = type(classname, superclasses, attributedict)
# 变为了
class = MyMeta(classname, superclasses, attributedict)
# 而MyMeta的init进行了超越变形,他会给这个类添加一些功能,修改了行为
# 下面就是利用元类,在每次实例化类的时候都会调用add_constructor
class YAMLObjectMetaclass(type):
def __init__(cls, name, bases, kwds):
super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
# 省略其余定义
python的迭代器和生成器
判断一个对象是否可迭代
def is_iterable(param):
try:
iter(param)
return True
except TypeError:
return False
生成器即只有在被使用的时候才会去生成对象,所以他不会像迭代器那样占用大量内存。
# ()是生成器, []是直接生成数组
list_2 = (i for i in range(100000000))
使用迭代器返回与指定元素相等的下标
def index_generator(L, target):
for i, num in enumerate(L):
if num == target:
yield i
print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)))
这个是个生成器,你可以理解为,函数运行到yield这一行的时候,程序会从这里暂停,然后跳出,不过跳到哪里呢?答案是 next() 函数。那么 i ** k
是干什么的呢?它其实成了 next() 函数的返回值。当下次再运行到这里时,暂停的程序就会又复活了,从yield这里向下继续执行,同时注意变量i并没有被清除掉,而是会继续累加。一个Generator对象,需要使用list转换为列表后,才能用print输出。
生成器的技巧:
b = (i for i in range(5))
print(2 in b)
print(4 in b)
print(3 in b)
########## 输出 ##########
True
True
False
上面的(2 in b)等价于
while True:
val = next(b)
if val == 2:
yield True
所以会过了4之后,想再回3是不行的。
python的协程
python的全局解释器锁
python的解释器并不是线程安全的,所以引入了全局解释器锁,也就是同一个时刻,只允许一个线程执行。当然在执行I/O操作时,如果一个线程被block了,全局解释器锁就会被释放,从而让另一个线程能够继续执行。
Asyncio工作原理
Asyncio和其他Python程序一样,单线程的,它只有一个主线程,但是可以进行多个不同任务,这里的任务,就是特殊的future对象,被一个叫做eventloop的对象所控制,这个任务只有两个状态: 一是预备状态,二是等待状态。eventloop会维护两个任务列表,分别对应这两种状态,并选取预备状态的一个任务,使其运行,一直到这个任务把控制权交还给eventloop为止。当任务把控制权交还给 event loop 时,event loop 会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成。
Asyncio的用法
import asyncio
import aiohttp
import time
async def download_one(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print('Read {} from {}'.format(resp.content_length, url))
async def download_all(sites):
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*tasks)
def main():
sites = [
'https://en.wikipedia.org/wiki/Portal:Arts',
'https://en.wikipedia.org/wiki/Portal:History',
'https://en.wikipedia.org/wiki/Portal:Society',
'https://en.wikipedia.org/wiki/Portal:Biography',
'https://en.wikipedia.org/wiki/Portal:Mathematics',
'https://en.wikipedia.org/wiki/Portal:Technology',
'https://en.wikipedia.org/wiki/Portal:Geography',
'https://en.wikipedia.org/wiki/Portal:Science',
'https://en.wikipedia.org/wiki/Computer_science',
'https://en.wikipedia.org/wiki/Python_(programming_language)',
'https://en.wikipedia.org/wiki/Java_(programming_language)',
'https://en.wikipedia.org/wiki/PHP',
'https://en.wikipedia.org/wiki/Node.js',
'https://en.wikipedia.org/wiki/The_C_Programming_Language',
'https://en.wikipedia.org/wiki/Go_(programming_language)'
]
start_time = time.perf_counter()
asyncio.run(download_all(sites))
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
if __name__ == '__main__':
main()
Asyncio
要想用好Asyncio,很多情况下必须得有相应的Python库支持,比如请求http时,必须要用支持协程的http库,比如aiohttp库,它兼容Asyncio。
多线程还是Asyncio
if io_bound:
# io密集型
if io_slow:
# 每个io操作很慢
print('Use Asyncio')
else:
# io操作很快
print('Use multi-threading')
else if cpu_bound:
# cpu密集的
print('Use multi-processing')
python的GIL
CPython解释器使用引用计数作内存管理,所有Python脚本中创建的实例,都会有一个引用技术,来记录有多少个指针,当引用计数只有0时,则会自动释放。如果有两个Python线程同时引用了a,就会造成引用计数的race condition,引用计数可能最终只增加1,这样就会造成内存被污染。
所以说,CPython 引进 GIL 其实主要就是这么两个原因:
- 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);
- 二是因为 CPython 大量使用 C 语言库,但大部分 C 语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)。
为什么Python线程会主动释放GIL呢?check_interval,CPython解释器会去轮询检查线程GIL的锁住情况,每隔一段时间,Python解释器就会强制当前线程去释放GIL,这样别的线程才有机会执行。
python的垃圾内存回收机制
查看python进程的内存
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memory used: {} MB'.format(hint, memory))
查看python对象的内部引用计数
import sys
a = []
# 两次引用,一次来自 a,一次来自 getrefcount
print(sys.getrefcount(a))
def func(a):
# 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount
print(sys.getrefcount(a))
func(a)
# 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在
print(sys.getrefcount(a))
########## 输出 ##########
2
4
2
Python使用标记清除算法和分代收集,来启用针对循环引用的自动垃圾回收。标记清除可以类似于Java的GC root,分代收集也是类似于Java的分代。python的垃圾收集是以引用计数+不可达+分代实现的。
调试内存泄漏
推荐使用objgraph库,可以分析引用
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
写出对机器和阅读者友好的python代码
-
对字典的遍历不要使用keys,因为keys会生成一个临时列表,导致多余的内存浪费并且运行缓慢,使用iterator。
-
is和==的正确使用
-
不要使用import一次导入多个模块
合理地运用assert
assert 1==2, 'This should fail'
这个语句等价于
if __debug__:
if not expression1: raise AssertionError(expression2)
这里的__debug__
是一个常数。如果 Python 程序执行时附带了-O
这个选项,比如Python test.py -O
,那么程序中所有的 assert 语句都会失效,常数__debug__
便为 False;反之__debug__
则为 True。
巧用上下文管理器和With语句精简代码
在python中,使用上下文管理器帮助程序员自动分配并且释放资源,其中最典型的就是with语句。
with open('test.txt', 'w') as f:
f.write('hello')
some_lock = threading.Lock()
with somelock:
...
自定义上下文管理器
基于类的上下文管理器
class FileManager:
def __init__(self, name, mode):
print('calling __init__ method')
self.name = name
self.mode = mode
self.file = None
def __enter__(self):
print('calling __enter__ method')
self.file = open(self.name, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
print('calling __exit__ method')
if self.file:
self.file.close()
with FileManager('test.txt', 'w') as f:
print('ready to write to file')
f.write('hello world')
## 输出
calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ method
如果在with内抛出了异常,exit可以接收到,如果你在exit内处理了异常,记得返回True,否则异常仍会继续抛出。
基于生成器的上下文管理器
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
try:
f = open(name, mode)
yield f
finally:
f.close()
with file_manager('test.txt', 'w') as f:
f.write('hello world')
python的调试工具及性能分析工具
pdb使用例子
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)
cprofile使用例子
python3 -m cProfile xxx.py