深拷贝与浅拷贝
关于缓存重用规则,具体可看下表吗, 比如同一个字符串,如果之前创建过,那么后面所有索引都是指向同一个缓存
然后关于深浅拷贝,共同点是都创建了一个新的对象,但是如果对元组只能采用copy.copy如下
l1 = [1, 2, 3]
l2 = l1[:]
l1 == l2
True
l1 is l2
False
# 对于元组不适用,索引指向的是同一块地址
t1 = (1, 2, 3)
t2 = tuple(t1)
t1 == t2
True
t1 is t2
True
另外关于循环引用的问题。因为深度拷贝是采用递归的方式,如果涉及到循环引用就会导致栈溢出。所以深度拷贝函数 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回
赋值与传参
关于变量指向某个对象,这种指向就类似于指针指向,如果对象是不可变的,那么当这个不可变对象尝试“改变”的时候,因为其不可变性,python会开辟一个新空间来重新指向。而如果是可变对象,直接就可以在原地更新,所以指向并没有发生改变。需要注意的是,Python 里的变量可以被删除,但是对象无法被删除。del l 删除了 l 这个变量,从此以后你无法访问 l,但是对象[1, 2, 3]仍然存在。Python 程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 2, 3]除了 l 外,还在其他地方被引用,那就不会被回收,反之则会被回收。
l = [1, 2, 3]
del l
关于python的函数参数传递,实质是对象传递,就是把整个对象的索引传进去,但是形参的值只是一个copy值,这一点要注意。
def my_func3(l2):
l2.append(4)
l1 = [1, 2, 3]
my_func3(l1)
l1
[1, 2, 3, 4]
#
def my_func4(l2):
l2 = l2 + [4]
l1 = [1, 2, 3]
my_func4(l1)
l1
[1, 2, 3]
从上面的例子我们不难看出形参只是一个复制值,当你对一个对象索引重新赋值的时候,只是改变了指向而已。但是第一种方式实质是利用索引找到具体对象,调用具体对象的方法去改变对象,所以原来对象也会改变。但是我们往往不推荐采用第一种方式隐式修改了原来对象,而推荐采取以下方式,即通过函数返回值去修改。
def my_func5(l2):
l2 = l2 + [4]
return l2
l1 = [1, 2, 3]
l1 = my_func5(l1)
l1
[1, 2, 3, 4]
装饰器
python中一切皆是对象,所以函数其实也是对象(funtion obj)。闭包,是指内外层函数嵌套之间的作用域,也叫闭包作用域。闭包意义在于把闭包作用域与内层函数绑定在一起。如下面的例子。
装饰器其实就是闭包的语法糖,让我们的代码看上去更加优雅与简洁。实现闭包的方式有以下两种。第一种是用函数实现,带参数的装饰器。
我们可以对比一下语法糖, greet = repeat(4)(greet) 与之间在greet函数上面加@, 明显后者更加简洁优雅。
# 带有参数的装饰器实现
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
# 自定义参数可以使得装饰器更加灵活
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
# 返回的一定是funtion obj
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
第二种是用类来实现装饰器,也叫类装饰器。 调用流程如下, example = Count( example ), 得到的其实是类实例,以后每次调用example函数,其实都是调用类实例,所以要实现call, 并且在call里面去调用真正调用的函数。这一点跟上面实现的装饰器不一样的是,类装饰器是直接在call魔法函数里面完成调用,而不是返回funtion obj。
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
...
从上面的例子我们可以看出,装饰器的特点其实就是让我们可以在需求变动的时候,尽量不用修改原有的函数。在实际工作中,如果你怀疑某些函数的耗时过长,导致整个系统的 latency(延迟)增加,所以想在线上测试某些函数的执行时间,那么,装饰器就是一种很常用的手段。
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
return res
return wrapper
@log_execution_time
def calculate_similarity(items):
...
这里,装饰器 log_execution_time 记录某个函数的运行时间,并返回其执行结果。如果你想计算任何函数的执行时间,在这个函数上方加上@log_execution_time即可。