月薪过万不是梦!2018年最全/最新Python面试题(整理汇总)
根据市场所需,近期对众多公司面试题进行了整理,挑选出了其中出现频率最高的十个题目,附上答案供小伙伴们参考!
1.*args和**kwargs是什么意思?
答:*args表示可变参数(variadic arguments),它允许你传入0个或任意个无名参数,这些参数在函数调用时自动组装为一个tuple; **kwargs表示关键字参数(keyword arguments),它允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。同时使用*args和**kwargs的时候,必须保证*args在**kwargs之前。
释:*args 是用来发送一个(非键值对)可变数量的参数列表给一个函数
这里有个例子帮你理解这个概念:
def test_var_args(f_arg, *argv): print("first normal arg:", f_arg) for arg in argv: print("another arg through *argv:", arg)test_var_args('yasoob', 'python', 'eggs', 'test')
这会产生如下输出:
first normal arg: yasoobanother arg through *argv: pythonanother arg through *argv: eggsanother arg through *argv: test
**kwargs 是允许你将不定长度的键值对, 作为参数传递给一个函数。 如果你想要在一个函数里处理带名字的参数, 你应该使用**kwargs。
这里有个让你上手的例子:
def greet_me(**kwargs): for key, value in kwargs.items(): print("{0} == {1}".format(key, value))>>> greet_me(name="yasoob")name == yasoob
现在你可以看出我们怎样在一个函数里, 处理了一个键值对参数了。
- 什么时候使用它们?
这还真的要看你的需求而定。
最常见的用例是在写函数装饰器的时候。
此外它也可以用来做猴子补丁(monkey patching)。猴子补丁的意思是在程序运行时(runtime)修改某些代码。 打个比方,你有一个类,里面有个叫get_info的函数会调用一个API并返回相应的数据。如果我们想测试它,可以把API调用替换成一些测试数据。例如:
import someclassdef get_info(self, *args): return "Test data"someclass.get_info = get_info
2.python里面如何拷贝一个对象?
答:
(1) 赋值(=),就是创建了对象的一个新的引用,修改其中任意一个变量都会影响到另一个;
(2)浅拷贝(copy.copy()),创建一个新的对象,但它包含的是对原始对象中包含项的引用(如果用引用的方式修改其中一个对象,另一个也会被改变);
(3)深拷贝(copy.deepcopy()),创建一个新的对象,并且递归的复制它所包含的对象(修改其中一个,另一个不会改变)
(注意:并不是所有的对象都可以拷贝)
释:Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果。
对象赋值
直接看一段代码:
will = ["Will", 28, ["Python", "C#", "JavaScript"]]wilber = willprint id(will)print willprint [id(ele) for ele in will]print id(wilber)print wilberprint [id(ele) for ele in wilber]
will[0] = "Wilber"will[2].append("CSS")print id(will)print willprint [id(ele) for ele in will]print id(wilber)print wilberprint [id(ele) for ele in wilber]
代码的输出为:
下面来分析一下这段代码:
首先,创建了一个名为will的变量,这个变量指向一个list对象,从第一张图中可以看到所有对象的地址(每次运行,结果可能不同)
然后,通过will变量对wilber变量进行赋值,那么wilber变量将指向will变量对应的对象(内存地址),也就是说"wilber is will","wilber[i] is will[i]"
可以理解为,Python中,对象的赋值都是进行对象引用(内存地址)传递
第三张图中,由于will和wilber指向同一个对象,所以对will的任何修改都会体现在wilber上
这里需要注意的一点是,str是不可变类型,所以当修改的时候会替换旧的对象,产生一个新的地址39758496
浅拷贝
下面就来看看浅拷贝的结果:
import copy
will = ["Will", 28, ["Python", "C#", "JavaScript"]]wilber = copy.copy(will)
print id(will)print willprint [id(ele) for ele in will]print id(wilber)print wilberprint [id(ele) for ele in wilber]
will[0] = "Wilber"will[2].append("CSS")print id(will)print willprint [id(ele) for ele in will]print id(wilber)print wilberprint [id(ele) for ele in wilber]
代码结果为:
分析一下这段代码:
- 首先,依然使用一个will变量,指向一个list类型的对象
- 然后,通过copy模块里面的浅拷贝函数copy(),对will指向的对象进行浅拷贝,然后浅拷贝生成的新对象赋值给wilber变量
- 浅拷贝会创建一个新的对象,这个例子中"wilber is not will"
- 但是,对于对象中的元素,浅拷贝就只会使用原始元素的引用(内存地址),也就是说"wilber[i] is will[i]"
- 当对will进行修改的时候
- 由于list的第一个元素是不可变类型,所以will对应的list的第一个元素会使用一个新的对象39758496
- 但是list的第三个元素是一个可不类型,修改操作不会产生新的对象,所以will的修改结果会相应的反应到wilber上
总结一下,当我们使用下面的操作的时候,会产生浅拷贝的效果:
- 使用切片[:]操作
- 使用工厂函数(如list/dir/set)
- 使用copy模块中的copy()函数
3.简要描述python的垃圾回收机制
答:python中的垃圾回收是以引用计数为主,标记-清除和分代收集为辅。
引用计数:python在内存中存储每个对象的引用计数,如果计数变成0,该对象就会消失,分配给该对象的内存就会释放出来。
标记-清除:一些容器对象,比如说list、dict、tuple、instance等可能会出现引用循环,对于这些循环,垃圾回收器会定时回收这些循环(对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边)。
分代收集:python把内存根据对象存活时间划分为三代,对象创建之后,垃圾回收器会分配它们所属的代。每个对象都会被分配一个代,而被分配更年轻的代是被优先处理的,因此越晚创建的对象越容易被回收。
释:
python里也同java一样采用了垃圾收集机制,不过不一样的是:
python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略
引用计数机制:
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject
typedef struct_object { int ob_refcnt; struct_typeobject *ob_type;} PyObject;
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少
#define Py_INCREF(op) ((op)->ob_refcnt++) //增加计数#define Py_DECREF(op) \ //减少计数 if (--(op)->ob_refcnt != 0) \ ; \ else \ __Py_Dealloc((PyObject *)(op))
当引用计数为0时,该对象生命就结束了。
引用计数机制的优点:
- 简单
- 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数机制的缺点:
- 维护引用计数消耗资源
- 循环引用
list1 = []list2 = []list1.append(list2)list2.append(list1)
list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)
4.什么是lambda函数?它有什么好处?
答:lambda表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。
Python允许你定义一种单行的小函数。定义lambda函数的形式如下(lambda参数:表达式)lambda函数默认返回表达式的值。你也可以将其赋值给一个变量。lambda函数可以接受任意个参数,包括可选参数,但是表达式只有一个。
释:
简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。这一用法跟所谓 λ 演算(题目说明里的维基链接)的关系,有点像原子弹和质能方程的关系,差别其实还是挺大的。
不谈形式化的 λ 演算,只说有实际用途的匿名函数。先举一个普通的 Python 例子:将一个 list 里的每个元素都平方:
map( lambda x: x*x, [y for y in range(10)] )
这个写法要好过
def sq(x): return x * x
map(sq, [y for y in range(10)])
因为后者多定义了一个(污染环境的)函数,尤其如果这个函数只会使用一次的话。而且第一种写法实际上更易读,因为那个映射到列表上的函数具体是要做什么,非常一目了然。如果你仔细观察自己的代码,会发现这种场景其实很常见:你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。Lambda 表达式就可以用来做这件事。
进一步讲,匿名函数本质上就是一个函数,它所抽象出来的东西是一组运算。这是什么意思呢?类比
a = [1, 2, 3]
和
f = lambda x : x + 1
你会发现,等号右边的东西完全可以脱离等号左边的东西而存在,等号左边的名字只是右边之实体的标识符。如果你能习惯 [1, 2, 3] 单独存在,那么 lambda x : x + 1 也能单独存在其实也就不难理解了,它的意义就是给「某个数加一」这一运算本身。
现在回头来看 map() 函数,它可以将一个函数映射到一个可枚举类型上面。沿用上面给出的 a 和 f,可以写:
map(f, a)
也就是将函数 f 依次套用在 a 的每一个元素上面,获得结果 [2, 3, 4]。现在用 lambda 表达式来替换 f,就变成:
map( lambda x : x + 1, [1, 2, 3] )
会不会觉得现在很一目了然了?尤其是类比
a = [1, 2, 3]r = []for each in a: r.append(each+1)
这样的写法时,你会发现自己如果能将「遍历列表,给遇到的每个元素都做某种运算」的过程从一个循环里抽象出来成为一个函数 map,然后用 lambda 表达式将这种运算作为参数传给 map 的话,考虑事情的思维层级会高出一些来,需要顾及的细节也少了一点。Python 之中,类似能用到 lambda 表达式的「高级」函数还有 reduce、filter 等等,很多语言也都有这样的工具(不过这些特性最好不要在 Python 中用太多,原因详见 http://www.zhihu.com/question/19794855/answer/12987428 的评论部分)。这种能够接受一个函数作为参数的函数叫做「高阶函数」(higher-order function),是来自函数式编程(functional programming)的思想。
和其他很多语言相比,Python 的 lambda 限制多多,最严重的当属它只能由一条表达式组成。这个限制主要是为了防止滥用,因为当人们发觉 lambda 很方便,就比较容易滥用,可是用多了会让程序看起来不那么清晰,毕竟每个人对于抽象层级的忍耐 / 理解程度都有所不同。
5.python如何实现单例模式?
答:单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个单例而且该单例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
__new__()在__init__()之前被调用,用于生成实例对象。利用这个方法和累的属性的特点可以实现设计模式的单例模式。单例模式是指创建唯一对象,单例模式设计的类只能实例。
1.使用__new__方法
class Singleton(object): def __new__(cls, *args, **kw): if not hasattr(cls, '_instance'): orig = super(Singleton, cls) cls._instance = orig.__new__(cls, *args, **kw) return cls._instance
class MyClass(Singleton): a = 1
2.共享属性
class Borg(object): _state = {} def __new__(cls, *args, **kw): ob = super(Borg, cls)。__new__(cls, *args, **kw) ob.__dict__ = cls._state return ob
class MyClass2(Borg): a = 1
3.装饰器版本
def singleton(cls, *args, **kw): instances = {} def getinstance(): if cls not in instances: instances[cls] = cls(*args, **kw) return instances[cls] return getinstance@singletonclass MyClass:…
4.import方法
class My_Singleton(object): def foo(self): passmy_singleton = My_Singleton()
# to usefrom mysingleton import my_singleton
my_singleton.foo()
方法拓展:
#方法1,实现__new__方法 #并在将一个类的实例绑定到类变量_instance上, #如果cls._instance为None说明该类还没有实例化过,实例化该类,并返回 #如果cls._instance不为None,直接返回cls._instance class Singleton(object): def __new__(cls, *args, **kw): if not hasattr(cls, '_instance'): orig = super(Singleton, cls) cls._instance = orig.__new__(cls, *args, **kw) return cls._instance class MyClass(Singleton): a = 1 one = MyClass() two = MyClass() two.a = 3 print one.a #3 #one和two完全相同,可以用id(), ==, is检测 print id(one) #29097904 print id(two) #29097904 print one == two #True print one is two #True
#方法2,共享属性;所谓单例就是所有引用(实例、对象)拥有相同的状态(属性)和行为(方法) #同一个类的所有实例天然拥有相同的行为(方法), #只需要保证同一个类的所有实例具有相同的状态(属性)即可 #所有实例共享属性的最简单最直接的方法就是__dict__属性指向(引用)同一个字典(dict) #可参看:http://code.activestate.com/recipes/66531/ class Borg(object): _state = {} def __new__(cls, *args, **kw): ob = super(Borg, cls).__new__(cls, *args, **kw) ob.__dict__ = cls._state return ob class MyClass2(Borg): a = 1 one = MyClass2() two = MyClass2() #one和two是两个不同的对象,id, ==, is对比结果可看出 two.a = 3 print one.a #3 print id(one) #28873680 print id(two) #28873712 print one == two #False print one is two #False #但是one和two具有相同的(同一个__dict__属性),见: print id(one.__dict__) #30104000 print id(two.__dict__) #30104000
#方法3:本质上是方法1的升级(或者说高级)版 #使用__metaclass__(元类)的高级python用法 class Singleton2(type): def __init__(cls, name, bases, dict): super(Singleton2, cls).__init__(name, bases, dict) cls._instance = None def __call__(cls, *args, **kw): if cls._instance is None: cls._instance = super(Singleton2, cls).__call__(*args, **kw) return cls._instance class MyClass3(object): __metaclass__ = Singleton2 one = MyClass3() two = MyClass3() two.a = 3 print one.a #3 print id(one) #31495472 print id(two) #31495472 print one == two #True print one is two #True
#方法4:也是方法1的升级(高级)版本, #使用装饰器(decorator), #这是一种更pythonic,更elegant的方法, #单例类本身根本不知道自己是单例的,因为他本身(自己的代码)并不是单例的 def singleton(cls, *args, **kw): instances = {} def _singleton(): if cls not in instances: instances[cls] = cls(*args, **kw) return instances[cls] return _singleton @singleton class MyClass4(object): a = 1 def __init__(self, x=0): self.x = x one = MyClass4() two = MyClass4() two.a = 3 print one.a #3 print id(one) #29660784 print id(two) #29660784 print one == two #True print one is two #True one.x = 1 print one.x #1 print two.x #1
6.python自省
答:自省就是面向对象的语言所写的程序在运行时,所能知道对象的类型,简单一句就是运行时能够获得对象的类型,比如type(),dir(),getattr(),hasattr(),isinstance()。
a = [1,2,3]b = {'a':1,'b':2,'c':3}c = Trueprint type(a),type(b),type(c) # <type 'list'> <type 'dict'> <type 'bool'>print isinstance(a,list) # True
拓展学习:https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy[官方文档][英文]
7.谈一谈python的装饰器
答:装饰器本质上是一个python函数,它可以让其他函数在不作任何变动的情况下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景。比如:插入日志、性能测试,事务处理、缓存、权限验证等。有了装饰器我们就可以抽离出大量的与函数功能无关的雷同代码进行重用。
释:由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
>>> def now():... print('2015-3-25')...>>> f = now>>> f()2015-3-25
函数对象有一个__name__属性,可以拿到函数的名字:
>>> now.__name__'now'>>> f.__name__'now'
现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func): def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) return wrapper
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@logdef now(): print('2015-3-25')
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now()call now():2015-3-25
把@log放到now()函数的定义处,相当于执行了语句:
now = log(now)
由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
def log(text): def decorator(func): def wrapper(*args, **kw): print('%s %s():' % (text, func.__name__)) return func(*args, **kw) return wrapper return decorator
这个3层嵌套的decorator用法如下:
@log('execute')def now(): print('2015-3-25')
执行结果如下:
>>> now()execute now():2015-3-25
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
>>> now = log('execute')(now)
我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper':
>>> now.__name__'wrapper'
因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:
import functools
def log(func): @functools.wraps(func) def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) return wrapper
或者针对带参数的decorator:
import functools
def log(text): def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): print('%s %s():' % (text, func.__name__)) return func(*args, **kw) return wrapper return decorator
import functools是导入functools模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。
8.什么是鸭子类型?
答:在鸭子类型中,关注的不是对象的类型本身,而是他如何使用的。例如,在不适用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭的对象,并调用它的走和叫方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的走和叫方法。
class duck(): def walk(self): print('I am duck,I can walk…') def swim(self): print('I am duck,I can swim…') def call(self): print('I am duck,I can call…') duck1=duck() duck1.walk() # I am duck,I can walk… duck1.call() # I am duck,I can call…
释:鸭子类型在动态语言中经常使用,非常灵活,使得python不想java那样专门去弄一大堆的设计模式。
下面例子用duck typing来实现多态。
1 #coding=utf-82 class Duck:3 def quack(self):4 print "Quaaaaaack!"56 class Bird:7 def quack(self):8 print "bird imitate duck."910 class Doge:11 def quack(self):12 print "doge imitate duck."1314 def in_the_forest(duck):15 duck.quack()1617 duck = Duck()18 bird = Bird()19 doge = Doge()20 for x in [duck, bird, doge]:21 in_the_forest(x)
再举个栗子,
我们来hack输出流。
1 import sys2 3 sys.stdout = open('stdout.log', 'a') #只要是file-like,不管是什么类型4 print 'foo'5 6 sys.stdout = sys.__stdout__ #恢复7 print 'bar'
这样就把输出流给写入到文件中去了
9.@classmethod和@staticmethod
答:@classmethod修饰符对应的函数不需要实例化,不需要self参数,第一个参数需要是表示自身类的cls参数,cls参数可以用来调用类的属性,类的方法,实例化对象等。@staticmethod返回函数的静态方法,该方法不强制要求传递参数,如下声明一个静态方法:
Class C(object):
@staticmethod
Def f(arg1, arg2,…):
…
以上实例声明了静态方法f,类可以不用实例化就可以调用该方法C.f(),也可以实例化后调用C()。f()。
释:
Python面向对象编程中,类中定义的方法可以是 @classmethod 装饰的类方法,也可以是 @staticmethod 装饰的静态方法,用的最多的还是不带装饰器的实例方法,如果把这几个方法放一块,对初学者来说无疑是一头雾水,那我们该如何正确地使用它们呢?
先来看一个简单示例:
class A(object): def m1(self, n): print("self:", self) @classmethod def m2(cls, n): print("cls:", cls) @staticmethod def m3(n): passa = A()a.m1(1) # self: <__main__.A object at 0x000001E596E41A90>A.m2(1) # cls: <class '__main__.A'>A.m3(1)
我在类中一共定义了3个方法,m1 是实例方法,第一个参数必须是 self(约定俗成的)。m2 是类方法,第一个参数必须是cls(同样是约定俗成),m3 是静态方法,参数根据业务需求定,可有可无。当程序运行时,大概发生了这么几件事(结合下面的图来看)。
第一步:代码从第一行开始执行 class 命令,此时会创建一个类 A 对象(没错,类也是对象,一切皆对象嘛)同时初始化类里面的属性和方法,记住,此刻实例对象还没创建出来。第二、三步:接着执行 a=A(),系统自动调用类的构造器,构造出实例对象 a第四步:接着调用 a.m1(1) ,m1 是实例方法,内部会自动把实例对象传递给 self 参数进行绑定,也就是说, self 和 a 指向的都是同一个实例对象。第五步:调用A.m2(1)时,python内部隐式地把类对象传递给 cls 参数,cls 和 A 都指向类对象。
严格意义上来说,左边的都是变量名,是对象的引用,右边才是真正的对像,为了描述方便,我直接把 a 称为对象,你应该明白我说对象其实是它所引用右边的那个真正的对象。
再来看看每个方法各有什么特性
实例方法
print(A.m1)# A.m1在py2中显示为<unbound method A.m1><function A.m1 at 0x000002BF7FF9A488>
print(a.m1)<bound method A.m1 of <__main__.A object at 0x000002BF7FFA2BE0>>
A.m1是一个还没有绑定实例对象的方法,对于未绑定方法,调用 A.m1 时必须显示地传入一个实例对象进去,而 a.m1是已经绑定了实例的方法,python隐式地把对象传递给了self参数,所以不再手动传递参数,这是调用实例方法的过程。
A.m1(a, 1)# 等价 a.m1(1)
如果未绑定的方法 A.m1 不传实例对象给 self 时,就会报参数缺失错误,在 py3 与 py2 中,两者报的错误不一致,python2 要求第一个参数self是实例对象,而python3中可以是任意对象。
A.m1(1)TypeError: m1() missing 1 required positional argument: 'n'
类方法
print(A.m2)<bound method A.m2 of <class '__main__.A'>>print(a.m2)<bound method A.m2 of <class '__main__.A'>>
m2是类方法,不管是 A.m2 还是 a.m2,都是已经自动绑定了类对象A的方法,对于后者,因为python可以通过实例对象a找到它所属的类是A,找到A之后自动绑定到 cls。
A.m2(1) # 等价 a.m2(1)
这使得我们可以在实例方法中通过使用 self.m2()这种方式来调用类方法和静态方法。
def m1(self, n): print("self:", self) self.m2(n)
静态方法
print(A.m3)<function A.m3 at 0x000002BF7FF9A840>
print(a.m3)<function A.m3 at 0x000002BF7FF9A840>
m3是类里面的一个静态方法,跟普通函数没什么区别,与类和实例都没有所谓的绑定关系,它只不过是碰巧存在类中的一个函数而已。不论是通过类还是实例都可以引用该方法。
A.m3(1) # 等价 a.m3(1) ```
以上就是几个方法的基本介绍。现在把几个基本的概念理清楚了,那么现在来说说几个方法之间的使用场景以及他们之间的优缺点。
### 应用场景
静态方法的使用场景:如果在方法中不需要访问任何实例方法和属性,纯粹地通过传入参数并返回数据的功能性方法,那么它就适合用静态方法来定义,它节省了实例化对象的开销成本,往往这种方法放在类外面的模块层作为一个函数存在也是没问题的,而放在类中,仅为这个类服务。例如下面是微信公众号开发中验证微信签名的一个例子,它没有引用任何类或者实例相关的属性和方法。
```pythonfrom hashlib import sha1import tornado.web
class SignatureHandler(tornado.web.RequestHandler): def get(self): " " " 根据签名判断请求是否来自微信 " " " signature = self.get_query_argument("signature", None) echostr = self.get_query_argument("echostr", None) timestamp = self.get_query_argument("timestamp", None) nonce = self.get_query_argument("nonce", None) if self._check_sign(TOKEN, timestamp, nonce, signature): logger.info("微信签名校验成功") self.write(echostr) else: self.write("你不是微信发过来的请求")
@staticmethod def _check_sign(token, timestamp, nonce, signature): sign = [token, timestamp, nonce] sign.sort() sign = "".join(sign) sign = sha1(sign).hexdigest() return sign == signature
类方法的使用场景有:
作为工厂方法创建实例对象,例如内置模块 datetime.date 类中就有大量使用类方法作为工厂方法,以此来创建date对象。
class date: def __new__(cls, year, month=None, day=None): self = object.__new__(cls) self._year = year self._month = month self._day = day return self
@classmethod def fromtimestamp(cls, t): y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t) return cls(y, m, d) @classmethod def today(cls): t = _time.time() return cls.fromtimestamp(t)
如果希望在方法裡面调用静态类,那么把方法定义成类方法是合适的,因为要是定义成静态方法,那么你就要显示地引用类A,这对继承来说可不是一件好事情。
class A:
@staticmethod def m1() pass @staticmethod def m2(): A.m1() # bad
@classmethod def m3(cls): cls.m1() # good
10.谈一谈python中的元类
答:一般来说,我们都是在代码里定义类,用定义的类来创建实例。而使用元类,步骤又是同,定义元类,用元类创建类,再使用创建出来的类来创建实例。元类的主要目的就是为了当创建类时能够自动地改变类。
拓展学习:
类也是对象,在理解元类之前,你需要先掌握Python中的类。Python中类的概念借鉴于Smalltalk,这显得有些奇特。在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这一点仍然成立,但是,Python中的类还远不止如此。类同样也是一种对象。是的,没错,就是对象。只要你使用关键字class,Python解释器在执行的时候就会创建一个对象。下面的代码段:
1 class ObjectCreator(object):2 pass
将在内存中创建一个对象,名字就是ObjectCreator。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是乎你可以对它做如下的操作:
1. 你可以将它赋值给一个变量
2. 你可以拷贝它
3. 你可以为它增加属性
4. 你可以将它作为函数参数传递
下面是示例:
1 >>>print ObjectCreator # 你可以打印一个类,因为它其实也是一个对象2 <class '__main__.ObjectCreator'>3 >>>def echo(o):4 print(o)5 >>>echo(ObjectCreator) # 你可以将类做为参数传给函数6 <class '__main__.ObjectCreator'>7 >>>print hasattr(ObjectCreator, 'new_attribute')8 False9 >>>ObjectCreator.new_attribute = 'foo' # 你可以为类增加属性10 print hasattr(ObjectCreator, 'new_attribute')11 >>>print ObjectCreator.new_attribute12 foo13 >>>ObjectCreatorMirror = ObjectCreator # 你可以将类赋值给一个变量14 >>>print ObjectCreatorMirror()15 <__main__.ObjectCreator object at 0x8997b4c>
动态的创建类
因为类也是对象,你可以在运行时动态的创建它们,就像其他任何对象一样。首先,你可以在函数中创建类,使用class关键字即可。
1 >>> def choose_class(name):2 … if name == 'foo':3 … class Foo(object):4 … pass5 … return Foo # 返回的是类,不是类的实例6 … else:7 … class Bar(object):8 … pass9 … return Bar10 …11 >>> MyClass = choose_class('foo')12 >>> print MyClass # 函数返回的是类,不是类的实例13 <class '__main__'.Foo>14 >>> print MyClass() # 你可以通过这个类创建类实例,也就是对象15 <__main__.Foo object at 0x89c6d4c>
但这还不够动态,因为你仍然需要自己编写整个类的代码。由于类也是对象,所以它们必须是通过什么东西来生成的才对。当你使用class关键字时,Python解释器自动创建这个对象。但就和Python中的大多数事情一样,Python仍然提供给你手动处理的方法。还记得内建函数type吗?这个古老但强大的函数能够让你知道一个对象的类型是什么,就像这样:
1 >>> print type(1)2 <type 'int'>3 >>> print type("1")4 <type 'str'>5 >>> print type(ObjectCreator)6 <type 'type'>7 >>> print type(ObjectCreator())8 <class '__main__.ObjectCreator'>
这里,type有一种完全不同的能力,它也能动态的创建类。type可以接受一个类的描述作为参数,然后返回一个类。(我知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在Python中是为了保持向后兼容性),type可以像这样工作:
1 type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
比如下面的代码:
1 class MyShinyClass(object):2 pass
可以手动像这样创建:
1 >>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一个类对象2 >>> print MyShinyClass3 <class '__main__.MyShinyClass'>4 >>> print MyShinyClass() # 创建一个该类的实例5 <__main__.MyShinyClass object at 0x8997cec>
你会发现我们使用“MyShinyClass”作为类名,并且也可以把它当做一个变量来作为类的引用。类和变量是不同的,这里没有任何理由把事情弄的复杂。type 接受一个字典来为类定义属性,因此
1 >>> class Foo(object):2 … bar = True
可以翻译为:
1 Foo = type('Foo', (), {'bar':True})
并且可以将Foo当成一个普通的类一样使用:
1 >>> print Foo2 <class '__main__.Foo'>3 >>> print Foo.bar4 True5 >>> f = Foo()6 >>> print f7 <__main__.Foo object at 0x8a9b84c>8 >>> print f.bar9 True
当然,你可以向这个类继承,所以,如下的代码:
1 >>> class FooChild(Foo):2 … pass
就可以写成这样:
1 >>> FooChild = type('FooChild', (Foo,),{})2 >>> print FooChild3 <class '__main__.FooChild'>4 >>> print FooChild.bar # bar属性是由Foo继承而来5 True
最终你会希望为你的类增加方法,只需要定义一个有着恰当签名的函数并将其作为属性赋值就可以了。
1 >>> def echo_bar(self):2 … print self.bar3 …4 >>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})5 >>> hasattr(Foo, 'echo_bar')6 False7 >>> hasattr(FooChild, 'echo_bar')8 True9 >>> my_foo = FooChild()10 >>> my_foo.echo_bar()11 True
你可以看到,在Python中,类也是对象,你可以动态的创建类。这就是当你使用关键字class时Python在幕后做的事情,而这就是通过元类来实现的。
到底什么是元类?
元类就是用来创建类的“东西”。你创建类就是为了创建类的实例对象,不是吗?但是我们已经学习到了Python中的类也是对象。好吧,元类就是用来创建这些类(对象)的,元类就是类的类,你可以这样理解 为:
1 MyClass = MetaClass()2 MyObject = MyClass()
你已经看到了type可以让你像这样做:
1 MyClass = type('MyClass', (), {})
这是因为函数type实际上是一个元类。type就是Python在背后用来创建所有类的元类。现在你想知道那为什么type会全部采用小写形式而不是Type呢?好吧,我猜这是为了和str保持一致性,str是用来创建字符串对象的类,而int是用来创建整数对象的类。type就是创建类对象的类。你可以通过检查__class__属性来看到这一点。Python中所有的东西,注意,我是指所有的东西——都是对象。这包括整数、字符串、函数以及类。它们全部都是对象,而且它们都是从一个类创建而来。
1 >>> age = 352 >>> age.__class__3 <type 'int'>4 >>> name = 'bob'5 >>> name.__class__6 <type 'str'>7 >>> def foo(): pass8 >>>foo.__class__9 <type 'function'>10 >>> class Bar(object): pass11 >>> b = Bar()12 >>> b.__class__13 <class '__main__.Bar'>
现在,对于任何一个__class__的__class__属性又是什么呢?
1 >>> a.__class__.__class__2 <type 'type'>3 >>> age.__class__.__class__4 <type 'type'>5 >>> foo.__class__.__class__6 <type 'type'>7 >>> b.__class__.__class__8 <type 'type'>
因此,元类就是创建类这种对象的东西。如果你喜欢的话,可以把元类称为“类工厂”(不要和工厂类搞混了:D) type就是Python的内建元类,当然了,你也可以创建自己的元类。
__metaclass__属性
你可以在写一个类的时候为其添加metaclass属性。
1 class Foo(object):2 __metaclass__ = something…3 […]
如果你这么做了,Python就会用元类来创建类Foo。小心点,这里面有些技巧。你首先写下class Foo(object),但是类对象Foo还没有在内存中创建。Python会在类的定义中寻找metaclass属性,如果找到了,Python就会用它来创建类Foo,如果没有找到,就会用内建的type来创建这个类。把下面这段话反复读几次。当你写如下代码时 :
1 class Foo(Bar):2 pass
Python做了如下的操作:
Foo中有__metaclass__这个属性吗?如果是,Python会在内存中通过__metaclass__创建一个名字为Foo的类对象(我说的是类对象,请紧跟我的思路)。如果Python没有找到__metaclass__,它会继续在Bar(父类)中寻找__metaclass__属性,并尝试做和前面同样的操作。如果Python在任何父类中都找不到__metaclass__,它就会在模块层次中去寻找__metaclass__,并尝试做同样的操作。如果还是找不到__metaclass__,Python就会用内置的type来创建这个类对象。
现在的问题就是,你可以在__metaclass__中放置些什么代码呢?答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东东都可以。
自定义元类
元类的主要目的就是为了当创建类时能够自动地改变类。通常,你会为API做这样的事情,你希望可以创建符合当前上下文的类。假想一个很傻的例子,你决定在你的模块里所有的类的属性都应该是大写形式。有好几种方法可以办到,但其中一种就是通过在模块级别设定metaclass。采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式就万事大吉了。
幸运的是,__metaclass__实际上可以被任意调用,它并不需要是一个正式的类(我知道,某些名字里带有‘class’的东西并不需要是一个class,画画图理解下,这很有帮助)。所以,我们这里就先以一个简单的函数作为例子开始。
1 # 元类会自动将你通常传给‘type’的参数作为自己的参数传入2 def upper_attr(future_class_name, future_class_parents, future_class_attr):3 '''返回一个类对象,将属性都转为大写形式'''4 # 选择所有不以'__'开头的属性5 attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
1 # 将它们转为大写形式2 uppercase_attr = dict((name.upper(), value) for name, value in attrs)3 4 # 通过'type'来做类对象的创建5 return type(future_class_name, future_class_parents, uppercase_attr)6 7 __metaclass__ = upper_attr # 这会作用到这个模块中的所有类89 class Foo(object):10 # 我们也可以只在这里定义__metaclass__,这样就只会作用于这个类中11 bar = 'bip'
1 print hasattr(Foo, 'bar')2 # 输出: False3 print hasattr(Foo, 'BAR')4 # 输出:True5 6 f = Foo()7 print f.BAR8 # 输出:'bip'
现在让我们再做一次,这一次用一个真正的class来当做元类。
1 # 请记住,'type'实际上是一个类,就像'str'和'int'一样2 # 所以,你可以从type继承3 class UpperAttrMetaClass(type):4 # __new__ 是在__init__之前被调用的特殊方法5 # __new__是用来创建对象并返回之的方法6 # 而__init__只是用来将传入的参数初始化给对象7 # 你很少用到__new__,除非你希望能够控制对象的创建8 # 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__9 # 如果你希望的话,你也可以在__init__中做些事情10 # 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用11 def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):12 attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))13 uppercase_attr = dict((name.upper(), value) for name, value in attrs)14 return type(future_class_name, future_class_parents, uppercase_attr)
但是,这种方式其实不是OOP。我们直接调用了type,而且我们没有改写父类的__new__方法。现在让我们这样去处理:
1 class UpperAttrMetaclass(type):2 def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):3 attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))4 uppercase_attr = dict((name.upper(), value) for name, value in attrs)56 # 复用type.__new__方法7 # 这就是基本的OOP编程,没什么魔法8 return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)
你可能已经注意到了有个额外的参数upperattr_metaclass,这并没有什么特别的。类方法的第一个参数总是表示当前的实例,就像在普通的类方法中的self参数一样。当然了,为了清晰起见,这里的名字我起的比较长。但是就像self一样,所有的参数都有它们的传统名称。因此,在真实的产品代码中一个元类应该是像这样的:
1 class UpperAttrMetaclass(type): 2 def __new__(cls, name, bases, dct): 3 attrs = ((name, value) for name, value in dct.items() if not name.startswith('__') 4 uppercase_attr = dict((name.upper(), value) for name, value in attrs) 5 return type.__new__(cls, name, bases, uppercase_attr)
如果使用super方法的话,我们还可以使它变得更清晰一些,这会缓解继承(是的,你可以拥有元类,从元类继承,从type继承)
1 class UpperAttrMetaclass(type):2 def __new__(cls, name, bases, dct):3 attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))4 uppercase_attr = dict((name.upper(), value) for name, value in attrs)5 return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)
就是这样,除此之外,关于元类真的没有别的可说的了。使用到元类的代码比较复杂,这背后的原因倒并不是因为元类本身,而是因为你通常会使用元类去做一些晦涩的事情,依赖于自省,控制继承等等。确实,用元类来搞些“黑暗魔法”是特别有用的,因而会搞出些复杂的东西来。但就元类本身而言,它们其实是很简单的:
1. 拦截类的创建
2. 修改类
3. 返回修改之后的类
为什么要用metaclass类而不是函数?
由于__metaclass__可以接受任何可调用的对象,那为何还要使用类呢,因为很显然使用类会更加复杂啊?这里有好几个原因:
1) 意图会更加清晰。当你读到UpperAttrMetaclass(type)时,你知道接下来要发生什么。
2) 你可以使用OOP编程。元类可以从元类中继承而来,改写父类的方法。元类甚至还可以使用元类。
3) 你可以把代码组织的更好。当你使用元类的时候肯定不会是像我上面举的这种简单场景,通常都是针对比较复杂的问题。将多个方法归总到一个类中会很有帮助,也会使得代码更容易阅读。
4) 你可以使用__new__, __init__以及__call__这样的特殊方法。它们能帮你处理不同的任务。就算通常你可以把所有的东西都在__new__里处理掉,有些人还是觉得用__init__更舒服些。
5) 哇哦,这东西的名字是metaclass,肯定非善类,我要小心!
究竟为什么要使用元类?
现在回到我们的大主题上来,究竟是为什么你会去使用这样一种容易出错且晦涩的特性?好吧,一般来说,你根本就用不上它:
“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python界的领袖 Tim Peters
元类的主要用途是创建API。一个典型的例子是Django ORM。它允许你像这样定义:
1 class Person(models.Model):2 name = models.CharField(max_length=30)3 age = models.IntegerField()
但是如果你像这样做的话:
1 guy = Person(name='bob', age='35')2 print guy.age
这并不会返回一个IntegerField对象,而是会返回一个int,甚至可以直接从数据库中取出数据。这是有可能的,因为models.Model定义了__metaclass__, 并且使用了一些魔法能够将你刚刚定义的简单的Person类转变成对数据库的一个复杂hook。Django框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的API将其化简,通过这个API重新创建代码,在背后完成真正的工作。首先,你知道了类其实是能够创建出类实例的对象。好吧,事实上,类本身也是实例,当然,它们是元类的实例。
1 >>>class Foo(object): pass2 >>> id(Foo)3 142630324
Python中的一切都是对象,它们要么是类的实例,要么是元类的实例,除了type。type实际上是它自己的元类,在纯Python环境中这可不是你能够做到的,这是通过在实现层面耍一些小手段做到的。其次,元类是很复杂的。
1) Monkey patching
2) class decorators
当你需要动态修改类时,99%的时间里你最好使用上面这两种技术。当然了,其实在99%的时间里你根本就不需要动态修改类。
如果你在面试中也曾遇到过一些让你“眼前一亮”的经典题目,或者,那些年你也有关于面试的难忘回忆,欢迎留言讨论!