你会Python嘛? 我会! 那你给我讲下Python装饰器吧! Python装饰器啊…. 我没用过哎
以上是一个哥们面试的时候发生的真实对白。
本篇是python必刷面试题
系列的第4篇文章,集中讲解了面试时重点考察的python基础原理和语法特性,如python的垃圾回收机制、多态原理、MRO以及装饰器和静态方法等语法特性。相信认真读完本文,你不仅可以轻松化解类似上面场景中小尴尬,对今后写出更加高效、优雅的代码也有很大帮助。
方法
,一般可以认为是对象里面定义的函数,比如一个对象的普通方法、私有方法、属性方法、魔法方法、类方法等,而函数
则是那些和对象无关的,比如lambda函数、python内置函数等等。判别方法可参考下面的实例:
from types import MethodType, FunctionType
def aaa():
pass
class A(object):
def __init__(self):
pass
def bbb(self):
pass
a = A()
print(isinstance(aaa, MethodType)) # False
print(isinstance(aaa, FunctionType)) # True
print(isinstance(a.bbb, MethodType)) # False
print(isinstance(a.bbb, FunctionType)) # True
本质:仍然是一个 Python 函数,实现由由闭包
支撑,装饰器的返回值也是一个函数对象。
作用:让函数在无需修改任何代码的前提下给其增加功能。
应用场景:有切面需求的场景,如下:
优点:抽离与函数功能本身无关的雷同代码,并重复重用。
实例:实现一个@timer装饰器,记录每个函数的运行时间。
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
print("[Time out]: %.4f s" % (time.time() - start_time))
return res
return wrapper
@timer
def sum(a, b):
time.sleep(2)
return a + b
print("计算结果:", sum(1, 2))
# 运行结果:
[Time out]: 2.0019 s
计算结果: 3
Python中3种方式定义类方法, 常规方式(self), @classmethod修饰方式, @staticmethod修饰方式。
实例对象
。类对象
参数cls(调用时可以不写)。实例对象
和类对象
。总结:
下面是一个实例,可以帮助理解:
class A(object):
bar = 1
def foo(self):
print('foo')
@staticmethod
def static_foo():
print('static_foo')
print(A.bar)
@classmethod
def class_foo(cls):
print('class_foo')
print(cls.bar)
cls().foo()
A.static_foo()
A.class_foo()
# 输出结果
static_foo
1
class_foo
1
foo
注意: @classmethod方法和@staticmethod方法既可以通过类调用,也可以通过类的实例对象调用,输出结果是一致的。
鸭子类型(duck typing),是python面向对象的一种多态机制。一种通俗的解释方法,“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
从原理上理解:
由于python是解释型语言,在运行时,边"翻译"边执行,当执行时遇到一个对象,将要调用对象的一个方法或者获取其属性时,只要这个对象实例存在这些方法或属性,那个程序就可以成功执行。因此,我们不用管一个对象是classA的实例化对象还是classB的实例化对象,我们只关心这个对象的属性或行为是否能够满足程序执行的需求。
打个比方就是,程序现在需要一个像鸭子一样的对象来执行游泳、走、叫的功能,但是这时候传第过来的是一个鸟,这个鸟具有这些功能,而且执行的效果和鸭子完全一样!!那么,我们就可以认为这个鸟就是一个鸭子类型
。
MRO
,全称是Method Resolution Order(方法解析顺序),它指的是对于一棵类继承树,当调用最底层类对象所对应实例对象的一个方法时,Python解释器在继承树上搜索该方法的顺序。
对于一棵类继承树,可以调用最底层类对象的方法mro()或访问最底层类对象的特殊属性_ mro _,来获得这颗类继承树的MRO。
当子类通过super()调用其父类中的方法时,该方法的搜索顺序基于以该子类为最底层类对象的类继承树的MRO。
如果想调用指定某个父类中被重写的方法,可以给super()传入两个实参:super(A_type, obj),其中,第一个实参A_type是个类对象,第二个实参obj是个实例对象,这样,被指定的父类是:
obj所对应类对象的MRO中,A_type类后面的那个类对象。
下面通过一个实例,理解一下Python中多重继承关系下的MRO。
类继承关系示例
# 首先定义A-F共6个类,继承关系如上图。
class A(object):
def fun(self):
print('A.fun')
class B(object):
def fun(self):
print('B.fun')
class C(object):
def fun(self):
print('C.fun')
class D(A, B):
def fun(self):
print('D.fun')
class E(B, C):
def fun(self):
print('E.fun')
class F(D, E):
def fun(self):
print('F.fun')
# 打印最底层F类的mro
print(F.mro())
# F类实例对象作为底层对象时,查找其mro中位于A类后面的那个类对象
super(A, F()).fun()
输出结果如下:
[<class '__main__.F'>, <class '__main__.D'>, <class '__main__.A'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
E.fun
也就是说,当查找F类中一个方法时,查找的顺序是:F->D->A->E->B->C->object,object是所有类的基类。
至于这个搜索顺序如何生成,其实是采用的C3算法
:每次将继承树中入度为0的结点放入列表,如果有多个结点符合,左侧优先。
其过程如下:
类继承关系示例
1. 传值、传址的概念和区别:
传值就是传入一个参数的值,传址就是传入一个参数的地址,也就是内存的地址(相当于指针)。他们的区别是如果函数里面对传入的参数赋值,函数外的全局变量是否相应改变,用传值传入的参数是不会改变的,用传址传入就会改变。
2. python中的传参形式:传址
Python采用的是“传对象引用”的方式,相当于传值
和传址
的一种综合。
如果函数收到的是一个可变对象(比如dict或者list)的引用,就能修改对象的原始值——相当于传址。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用(其实也是对象地址!!!),就不能直接修改原始对象——相当于是传值。
所以python的传值和传址是比如根据传入参数的类型来选择的:
python有很多内置魔法方法,一般表现为双下划线开头和结尾。这些魔法方法会让对象持有特殊行为,使python的自由度变得更高。下面是常用的一些魔法函数:
允许一个类的实例像函数一样被调用:x(a, b) 调用 x.call(a, b)
定义:
def isinstance(object, class_or_type_or_tuple):
...
作用:判断一个对象是否为另一个对象或其子类的实例。注意哈,Python中万物皆对象,其实里面的东西还不少,可以通过下面的习题检验一下。
print isinstance(1, int)
print isinstance(True, int)
print isinstance(1, (str, bool, int))
print isinstance(int, type)
print isinstance(int, object)
print isinstance(type, object)
print isinstance(object, type)
print isinstance(type, type)
答案:全是True。。。不知道那答对了吗
解析:
isinstance(obj, (A,B))
相当于isinstance(obj, A) or isinstance(obj, A)
篇幅限制,讲的比较粗糙,感兴趣的可以加入交流群讨论,我们每天打卡学习哦~
接口:只是定义了一些方法,而没有去实现,多用于程序设计时,只是设计需要有什么样的功能,但是并没有实现任何功能,这些功能需要被另一个类(B)继承后,由类B去实现其中的某个功能或全部功能。
在python中,其实没必要使用类似java的interface。因为Python里有多继承和使用鸭子类型。
如果非要在python中实现接口,方法如下:
from abc import ABCMeta, abstractmethod
class A(object):
__metaclass__ = ABCMeta # 指定这是一个抽象类
@abstractmethod # 在类中声明一个抽象方法,该类的子类必须实现该方法
def hello(self, name):
pass
class B(A):
# 接口中的所有抽象方法都要实现
def hello(self, name):
print('你好呀,%s' % name)
obj = B()
obj.hello("lihong")
# 如果B中存在未实现的接口方法,实例化时将报错:
# Can't instantiate abstract class B with abstract methods hello
异常类 | 含义 |
---|---|
KeyError | 试图访问字典里不存在的键 |
ValueError | 传入一个调用者不期望的值,即使值的类型是正确的 |
TypeError | 在运算或函数调用时,使用了不兼容的类型时引发的异常 |
IndexError | 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5] |
AttributeError | 访问对象属性时引发的异常,如属性不存在或不支持赋值等。 |
NameError | 尝试访问一个没有定义过的变量 |
AssertionError | 断言语句失败 |
SyntaxError | Python 语法错误 |
NotImplementedError | 尚未实现的方法 |
UnboundLocalError | 访问未初始化的本地变量 |
MemoryError | 内存溢出错误 |
IOError | 输入/输出异常,基本上是无法打开文件 |
垃圾回收机制(简称GC)是Python解释器自带一种机制,专门用来回收不可用的变量值所占用的内存空间,主要运用了引用计数机制
来跟踪和回收垃圾。在引用计数的基础上,还可以通过标记-清除
解决容器对象可能产生的循环引用的问题。通过分代回收
以空间换取时间进一步提高垃圾回收的效率。
Python中万物皆对象。每个对象都会记录着自己被引用的个数,当一个对象的引用数为0时,它占据的内存将被回收。
(1)增加引用个数的情况:对象被创建;被引用;被当作参数传入函数;被存储到容器对象中。
(2)减少引用个数的情况:对象的别名被销毁;别名被赋予其他对象;对象离开自己的作用域;对象从容器对象中删除,或者容器对象被销毁。
循环引用问题
: 如果a引用b, b也引用a, 导致相互引用的对象的引用计数永远不为0,内存也就永远不会被释放。
在了解标记清除算法前,我们需要明确一点,内存中有两块区域:堆区与栈区,在定义变量时,变量名存放于栈区,变量值存放于堆区,内存管理回收的则是堆区的内容。
标记-清除
只关注那些可能会产生循环引用
的对象,比如list、dict、set、class等,因为它们内部可能很持有其它对象的引用。
原理:
python中垃圾回收的时机有两种:手动回收
和自动回收
。
import gc
# 手动回收
gc.collect()
# 检测自动回收是否开启
gc.isenabled()
# 开启自动回收
gc.enable()
# 关闭自动回收
gc.disable()
# 获取自动回收配置
print gc.get_threshold()
# 结果:(700, 10, 10) ,700是垃圾回收启动的阈值,10,10是下面讲
引用计数回收机制,每次回收都非常耗时地遍历全部对象,分代回收的核心思想是:经多次扫描没有被回收的变量,肯定是常用变量,应该降低对其扫描的频率。
python将所有的对象分为0,1,2三代。新建对象都是0代。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。可用set_threshold()来调整。
值得注意的是,python2.x中,默认都是经典类(不继承子object),Python 3.x 中默认都是新式类,经典类被移除,且不用显式的继承 object(默认会继承)。