我的博客: https://www.luozhiyun.com/archives/269
'==' VS 'is'
'=='操作符比较对象之间的值是否相等.
'is'操作符比较的是对象的身份标识是否相等, 即它们是否是同一个对象, 是否指向同一个内存地址.
如:
- a = 10
- b = 10
- a == b
- True
- id(a)
- 4427562448
- id(b)
- 4427562448
- a is b
- True
Python 会为 10 这个值开辟一块内存, 然后变量 a 和 b 同时指向这块内存区域, 即 a 和 b 都是指向 10 这个变量, 因此 a 和 b 的值相等, id 也相等.
不过, 对于整型数字来说, 以上 a is b 为 True 的结论, 只适用于 -5 到 256 范围内的数字. 这里和 java 的 Integer 的缓存有点像, java 缓存 - 127 到 128.
当我们比较一个变量与一个单例 (singleton) 时, 通常会使用'is'. 一个典型的例子, 就是检查一个变量是否为 None:
- if a is None:
- ...
- if a is not None:
- ...
比较操作符'is'的速度效率, 通常要优于'=='. 因为'is'操作符不能被重载, 而执行 a == b 相当于是去执行 a.eq(b), 而 Python 大部分的数据类型都会去重载__eq__这个函数.
浅拷贝和深度拷贝
浅拷贝
浅拷贝, 是指重新分配一块内存, 创建一个新的对象, 里面的元素是原对象中子对象的引用. 因此, 如果原对象中的元素不可变, 那倒无所谓; 但如果元素可变, 浅拷贝通常会带来一些副作用, 如下:
- l1 = [[1, 2], (30, 40)]
- l2 = list(l1)
- l1.append(100)
- l1[0].append(3)
- l1
- [[1, 2, 3], (30, 40), 100]
- l2
- [[1, 2, 3], (30, 40)]
- l1[1] += (50, 60)
- l1
- [[1, 2, 3], (30, 40, 50, 60), 100]
- l2
- [[1, 2, 3], (30, 40)]
在这个例子中, 因为浅拷贝里的元素是对原对象元素的引用, 因此 l2 中的元素和 l1 指向同一个列表和元组对象.
l1[0].append(3), 这里表示对 l1 中的第一个列表新增元素 3. 因为 l2 是 l1 的浅拷贝, l2 中的第一个元素和 l1 中的第一个元素, 共同指向同一个列表, 因此 l2 中的第一个列表也会相对应的新增元素 3.
l1[1] += (50, 60), 因为元组是不可变的, 这里表示对 l1 中的第二个元组拼接, 然后重新创建了一个新元组作为 l1 中的第二个元素, 而 l2 中没有引用新元组, 因此 l2 并不受影响.
深度拷贝
所谓深度拷贝, 是指重新分配一块内存, 创建一个新的对象, 并且将原对象中的元素, 以递归的方式, 通过创建新的子对象拷贝到新对象中. 因此, 新对象和原对象没有任何关联.
Python 中以 copy.deepcopy() 来实现对象的深度拷贝.
- 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)]
不过, 深度拷贝也不是完美的, 往往也会带来一系列问题. 如果被拷贝对象中存在指向自身的引用, 那么程序很容易陷入无限循环:
- import copy
- x = [1]
- x.append(x)
- x
- [1, [...]]
- y = copy.deepcopy(x)
- y
- [1, [...]]
这里没有出现 stack overflow 的现象, 是因为深度拷贝函数 deepcopy 中会维护一个字典, 记录已经拷贝的对象与其 ID. 拷贝过程中, 如果字典里已经存储了将要拷贝的对象, 则会从字典直接返回.
- def deepcopy(x, memo=None, _nil=[]):
- """Deep copy operation on arbitrary Python objects.
- See the module's __doc__ string for more info.
- """
- if memo is None:
- memo = {}
- d = id(x) # 查询被拷贝对象 x 的 id
- y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
- if y is not _nil:
- return y # 如果字典里已经存储了将要拷贝的对象, 则直接返回
- ...
Python 参数传递
Python 中参数的传递是赋值传递, 或者是叫对象的引用传递. 这里的赋值或对象的引用传递, 不是指向一个具体的内存地址, 而是指向一个具体的对象.
如果对象是可变的, 当其改变时, 所有指向这个对象的变量都会改变.
如果对象不可变, 简单的赋值只能改变其中一个变量的值, 其余变量则不受影响.
例如:
- def my_func1(b):
- b = 2
- a = 1
- my_func1(a)
- a
- 1
这里的参数传递, 使变量 a 和 b 同时指向了 1 这个对象. 但当我们执行到 b = 2 时, 系统会重新创建一个值为 2 的新对象, 并让 b 指向它; 而 a 仍然指向 1 这个对象. 所以, a 的值不变, 仍然为 1.
- def my_func3(l2):
- l2.append(4)
- l1 = [1, 2, 3]
- my_func3(l1)
- l1
- [1, 2, 3, 4]
这里 l1 和 l2 先是同时指向值为 [1, 2, 3] 的列表. 不过, 由于列表可变, 执行 append() 函数, 对其末尾加入新元素 4 时, 变量 l1 和 l2 的值也都随之改变了.
- def my_func4(l2):
- l2 = l2 + [4]
- l1 = [1, 2, 3]
- my_func4(l1)
- l1
- [1, 2, 3]
这里 l2 = l2 + [4], 表示创建了一个 "末尾加入元素 4" 的新列表, 并让 l2 指向这个新的对象. 这个过程与 l1 无关, 因此 l1 的值不变.
装饰器
首先我们看一个装饰器的简单例子:
- 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
这段代码中, 变量 greet 指向了内部函数 wrapper(), 而内部函数 wrapper() 中又会调用原函数 greet(), 因此, 最后调用 greet() 时, 就会先打印'wrapper of decorator', 然后输出'hello world'.
my_decorator() 就是一个装饰器, 它把真正需要执行的函数 greet() 包裹在其中, 并且改变了它的行为.
在 python 中, 可以使用更优雅的方式:
- def my_decorator(func):
- def wrapper():
- print('wrapper of decorator')
- func()
- return wrapper
- @my_decorator
- def greet():
- print('hello world')
- greet()
@my_decorator 就相当于前面的 greet=my_decorator(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
保留原函数的元信息
如下:
- greet.__name__
- ## 输出
- 'wrapper'
- help(greet)
- # 输出
- Help on function wrapper in module __main__:
- wrapper(*args, **kwargs)
greet() 函数被装饰以后, 它的元信息变了. 元信息告诉我们 "它不再是以前的那个 greet() 函数, 而是被 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)
- greet.__name__
- # 输出
- 'greet'
类装饰器
类装饰器主要依赖于函数__call_(), 每当你调用一个类的示例时, 函数__call__()就会被执行一次.
- 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")
- example()
- # 输出
- num of calls is: 1
- hello world
- example()
- # 输出
- num of calls is: 2
- hello world
装饰器的嵌套
如:
- @decorator1
- @decorator2
- @decorator3
- def func():
- ...
等效于:
decorator1(decorator2(decorator3(func)))
例子:
- import functools
- def my_decorator1(func):
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- print('execute decorator1')
- func(*args, **kwargs)
- return wrapper
- def my_decorator2(func):
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- print('execute decorator2')
- func(*args, **kwargs)
- return wrapper
- @my_decorator1
- @my_decorator2
- def greet(message):
- print(message)
- greet('hello world')
- # 输出
- execute decorator1
- execute decorator2
- hello world
协程
协程和多线程的区别, 主要在于两点, 一是协程为单线程; 二是协程由用户决定, 在哪些地方交出控制权, 切换到下一个任务.
我们先来看一个例子:
- 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
执行协程有多种方法, 这里我介绍一下常用的三种:
首先, 我们可以通过 await 来调用. await 执行的效果, 和 Python 正常执行是一样的, 也就是说程序会阻塞在这里, 进入被调用的协程函数, 执行完毕返回后再继续, 而这也是 await 的字面意思.
其次, 我们可以通过 asyncio.create_task() 来创建任务. 要等所有任务都结束才行, 用 for task in tasks: await task 即可.
最后, 我们需要 asyncio.run 来触发运行. asyncio.run 这个函数是 Python 3.7 之后才有的特性. 一个非常好的编程规范是, asyncio.run(main()) 作为主程序的入口函数, 在程序运行周期内, 只调用一次 asyncio.run.
在上面的例子中, 也可以使用 await asyncio.gather(*tasks), 表示等待所有任务.
- 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]
- await asyncio.gather(*tasks)
- %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: 4.01 s
协程中断和异常处理
- import asyncio
- async def worker_1():
- await asyncio.sleep(1)
- return 1
- async def worker_2():
- await asyncio.sleep(2)
- return 2 / 0
- async def worker_3():
- await asyncio.sleep(3)
- return 3
- async def main():
- task_1 = asyncio.create_task(worker_1())
- task_2 = asyncio.create_task(worker_2())
- task_3 = asyncio.create_task(worker_3())
- await asyncio.sleep(2)
- task_3.cancel()
- res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)
- print(res)
- %time asyncio.run(main())
- ########## 输出 ##########
- [1, ZeroDivisionError('division by zero'), CancelledError()]
- Wall time: 2 s
这个例子中, 使用了 task_3.cancel()来中断代码, 使用了 return_exceptions=True 来控制输出异常, 如果不设置的话, 错误就会完整地 throw 到我们这个执行层, 从而需要 try except 来捕捉, 这也就意味着其他还没被执行的任务会被全部取消掉.
Python 中的垃圾回收机制
python 采用的是引用计数机制为主, 标记 - 清除和分代收集 (隔代回收) 两种机制为辅的策略.
引用计数法
引用计数法机制的原理是: 每个对象维护一个 ob_ref 字段, 用来记录该对象当前被引用的次数, 每当新的引用指向该对象时, 它的引用计数 ob_ref 加 1, 每当该对象的引用失效时计数 ob_ref 减 1, 一旦对象的引用计数为 0, 该对象立即被回收, 对象占用的内存空间将被释放.
它的缺点是它不能解决对象的 "循环引用".
标记清除算法
对于一个有向图, 如果从一个节点出发进行遍历, 并标记其经过的所有节点; 那么, 在遍历结束后, 所有没有被标记的节点, 我们就称之为不可达节点. 显而易见, 这些节点的存在是没有任何意义的, 自然的, 我们就需要对它们进行垃圾回收.
在 Python 的垃圾回收实现中, mark-sweep 使用双向链表维护了一个数据结构, 并且只考虑容器类的对象(只有容器类对象才有可能产生循环引用).
分代收集算法
Python 将所有对象分为三代. 刚刚创立的对象是第 0 代; 经过一次垃圾回收后, 依然存在的对象, 便会依次从上一代挪到下一代. 而每一代启动自动垃圾回收的阈值, 则是可以单独指定的. 当垃圾回收器中新增对象减去删除对象达到相应的阈值时, 就会对这一代对象启动垃圾回收.
来源: https://www.cnblogs.com/luozhiyun/p/12685722.html