最近以为 Python 太“简单了”,因而在朋友面前放肆了一把:“我以为 Python 是世界上最简单的语言!”。朋友嘴角闪过了一丝轻蔑的微笑(心里 OS:Naive!,做为一个 Python 开发者,我必需要给你一点人生经验,否则你不知道天高地厚!)因而朋友给我了一份满分 100 分的题,而后这篇文章就是记录下作这套题所踩过的坑。express
描述app
下面的代码会报错,为何?ide
class A(object): x = 1 gen = (x for _ in xrange(10)) # gen=(x for _ in range(10)) if __name__ == "__main__": print(list(A.gen))
答案函数
这个问题是变量做用域问题,在 gen=(x for _ in xrange(10)) 中 gen 是一个 generator ,在 generator 中变量有本身的一套做用域,与其他做用域空间相互隔离。所以,将会出现这样的 NameError: name 'x' is not defined 的问题,那么解决方案是什么呢?答案是:用 lambda 。ui
class A(object): x = 1 gen = (lambda x: (x for _ in xrange(10)))(x) # gen=(x for _ in range(10)) if __name__ == "__main__": print(list(A.gen))
或者this
class A(object): x = 1 gen = (A.x for _ in xrange(10)) # gen=(x for _ in range(10)) if __name__ == "__main__": print(list(A.gen))
补充code
感谢评论区几位提出的意见,这里我给一份官方文档的说明吧:
The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope. This means that the following will fail:对象
class A: a = 42 b = list(a + i for i in range(10))
参考连接 Python2 Execution-Model:Naming-and-Binding , Python3 Execution-Model:Resolution-of-Names。听说这是 PEP 227 中新增的提案,我回去会进一步详细考证。再次拜谢评论区 @没头脑很着急 @涂伟忠 @Cholerae 三位的勘误指正。继承
描述ip
我想写一个类装饰器用来度量函数/方法运行时间
import time class Timeit(object): def __init__(self, func): self._wrapped = func def __call__(self, *args, **kws): start_time = time.time() result = self._wrapped(*args, **kws) print("elapsed time is %s " % (time.time() - start_time)) return result
这个装饰器可以运行在普通函数上:
@Timeit def func(): time.sleep(1) return "invoking function func" if __name__ == '__main__': func() # output: elapsed time is 1.00044410133
可是运行在方法上会报错,为何?
@Timeit def func(): time.sleep(1) return "invoking function func" if __name__ == '__main__': func() # output: elapsed time is 1.00044410133
若是我坚持使用类装饰器,应该如何修改?
答案
使用类装饰器后,在调用 func 函数的过程当中其对应的 instance 并不会传递给 call 方法,形成其 mehtod unbound ,那么解决方法是什么呢?描述符赛高
class Timeit(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print('invoking Timer') def __get__(self, instance, owner): return lambda *args, **kwargs: self.func(instance, *args, **kwargs)
描述
咱们知道 call 方法能够用来重载圆括号调用,好的,觉得问题就这么简单?Naive!
class A(object): def __call__(self): print("invoking __call__ from A!") if __name__ == "__main__": a = A() a() # output: invoking __call__ from A
如今咱们能够看到 a() 彷佛等价于 a.__call__() ,看起来很 Easy 对吧,好的,我如今想做死,又写出了以下的代码,
a.__call__ = lambda: "invoking __call__ from lambda" a.__call__() # output:invoking __call__ from lambda a() # output:invoking __call__ from A!
请大佬们解释下,为何 a() 没有调用出 a.__call__() (此题由 USTC 王子博前辈提出)
答案
缘由在于,在 Python 中,新式类( new class )的内建特殊方法,和实例的属性字典是相互隔离的,具体能够看看 Python 官方文档对于这一状况的说明
For new-style classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary. That behaviour is the reason why the following code raises an exception (unlike the equivalent example with old-style classes):
同时官方也给出了一个例子:
class C(object): pass c = C() c.__len__ = lambda: 5 len(c) # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # TypeError: object of type 'C' has no len()
回到咱们的例子上来,当咱们在执行 a.__call__=lambda:"invoking call from lambda" 时,的确在咱们在 a.__dict__ 中新增长了一个 key 为 call 的 item,可是当咱们执行 a() 时,由于涉及特殊方法的调用,所以咱们的调用过程不会从 a.__dict__ 中寻找属性,而是从 tyee(a).__dict__ 中寻找属性。所以,就会出现如上所述的状况。
我想写一个 Exam 类,其属性 math 为 [0,100] 的整数,若赋值时不在此范围内则抛出异常,我决定用描述符来实现这个需求。
class Grad(object): def __init__(self): self._grade_pool = {} def __get__(self, instance, owner): return self._grade_pool.get(instance, None) def __set__(self, instance, value): if 0 <= value <= 100: _grade_pool = self.__dict__.setdefault('_grade_pool', {}) _grade_pool[instance] = value else: raise ValueError("fuck")
答案
1.第一个问题的其实很简单,若是你再运行一次 print(niche.math) 你就会发现,输出值是 75 ,那么这是为何呢?这就要先从 Python 的调用机制提及了。咱们若是调用一个属性,那么其顺序是优先从实例的 dict 里查找,而后若是没有查找到的话,那么一次查询类字典,父类字典,直到完全查不到为止。好的,如今回到咱们的问题,咱们发现,在咱们的类 Exam 中,其 self.math 的调用过程是,首先在实例化后的实例的 dict 中进行查找,没有找到,接着往上一级,在咱们的类 Exam 中进行查找,好的找到了,返回。那么这意味着,咱们对于 self.math 的全部操做都是对于类变量 math 的操做。所以形成变量污染的问题。那么该则怎么解决呢?不少同志可能会说,恩,在 set 函数中将值设置到具体的实例字典不就好了。
那么这样可不能够呢?答案是,很明显不得行啊,至于为何,就涉及到咱们 Python 描述符的机制了,描述符指的是实现了描述符协议的特殊的类,三个描述符协议指的是 get , ‘set‘ , delete 以及 Python 3.6 中新增的 set_name 方法,其中实现了 get 以及 set / delete / set_name 的是 Data descriptors ,而只实现了 get 的是 Non-Data descriptor 。那么有什么区别呢,前面说了, 咱们若是调用一个属性,那么其顺序是优先从实例的 dict 里查找,而后若是没有查找到的话,那么一次查询类字典,父类字典,直到完全查不到为止。 可是,这里没有考虑描述符的因素进去,若是将描述符因素考虑进去,那么正确的表述应该是咱们若是调用一个属性,那么其顺序是优先从实例的 dict 里查找,而后若是没有查找到的话,那么一次查询类字典,父类字典,直到完全查不到为止。其中若是在类实例字典中的该属性是一个 Data descriptors ,那么不管实例字典中存在该属性与否,无条件走描述符协议进行调用,在类实例字典中的该属性是一个 Non-Data descriptors ,那么优先调用实例字典中的属性值而不触发描述符协议,若是实例字典中不存在该属性值,那么触发 Non-Data descriptor 的描述符协议。回到以前的问题,咱们即便在 set 将具体的属性写入实例字典中,可是因为类字典中存在着 Data descriptors ,所以,咱们在调用 math 属性时,依旧会触发描述符协议。
2.通过改良的作法,利用 dict 的 key 惟一性,将具体的值与实例进行绑定,可是同时带来了内存泄露的问题。那么为何会形成内存泄露呢,首先复习下咱们的 dict 的特性,dict 最重要的一个特性,就是凡可 hash 的对象皆可为 key ,dict 经过利用的 hash 值的惟一性(严格意义上来说并非惟一,而是其 hash 值碰撞概率极小,近似认定其惟一)来保证 key 的不重复性,同时(敲黑板,重点来了),dict 中的 key 引用是强引用类型,会形成对应对象的引用计数的增长,可能形成对象没法被 gc ,从而产生内存泄露。那么这里该怎么解决呢?两种方法
第一种:
class Grad(object): def __init__(self): import weakref self._grade_pool = weakref.WeakKeyDictionary() def __get__(self, instance, owner): return self._grade_pool.get(instance, None) def __set__(self, instance, value): if 0 <= value <= 100: _grade_pool = self.__dict__.setdefault('_grade_pool', {}) _grade_pool[instance] = value else: raise ValueError("fuck")
weakref 库中的 WeakKeyDictionary 所产生的字典的 key 对于对象的引用是弱引用类型,其不会形成内存引用计数的增长,所以不会形成内存泄露。同理,若是咱们为了不 value 对于对象的强引用,咱们可使用 WeakValueDictionary 。
第二种:在 Python 3.6 中,实现的 PEP 487 提案,为描述符新增长了一个协议,咱们能够用其来绑定对应的对象:
class Grad(object): def __get__(self, instance, owner): return instance.__dict__[self.key] def __set__(self, instance, value): if 0 <= value <= 100: instance.__dict__[self.key] = value else: raise ValueError("fuck") def __set_name__(self, owner, name): self.key = name
这道题涉及的东西比较多,这里给出一点参考连接,invoking-descriptors , Descriptor HowTo Guide , PEP 487 , what`s new in Python 3.6 。
描述
试求出如下代码的输出结果。
class Init(object): def __init__(self, value): self.val = value class Add2(Init): def __init__(self, val): super(Add2, self).__init__(val) self.val += 2 class Mul5(Init): def __init__(self, val): super(Mul5, self).__init__(val) self.val *= 5 class Pro(Mul5, Add2): pass class Incr(Pro): csup = super(Pro) def __init__(self, val): self.csup.__init__(val) self.val += 1 p = Incr(5) print(p.val)
答案
输出是 36 ,具体能够参考 New-style Classes , multiple-inheritance
描述
我写了一个经过重载 new 方法来实现单例模式的类。
class Singleton(object): _instance = None def __new__(cls, *args, **kwargs): if cls._instance: return cls._instance cls._isntance = cv = object.__new__(cls, *args, **kwargs) return cv sin1 = Singleton() sin2 = Singleton() print(sin1 is sin2) # output: True
如今我有一堆类要实现为单例模式,因此我打算照葫芦画瓢写一个元类,这样可让代码复用:
class SingleMeta(type): def __init__(cls, name, bases, dict): cls._instance = None __new__o = cls.__new__ def __new__(cls, *args, **kwargs): if cls._instance: return cls._instance cls._instance = cv = __new__o(cls, *args, **kwargs) return cv cls.__new__ = __new__ class A(object): __metaclass__ = SingleMeta a1 = A() # what`s the fuck
哎呀,好气啊,为啥这会报错啊,我明明以前用这种方法给 getattribute 打补丁的,下面这段代码可以捕获一切属性调用并打印参数
class TraceAttribute(type): def __init__(cls, name, bases, dict): __getattribute__o = cls.__getattribute__ def __getattribute__(self, *args, **kwargs): print('__getattribute__:', args, kwargs) return __getattribute__o(self, *args, **kwargs) cls.__getattribute__ = __getattribute__ class A(object): # Python 3 是 class A(object,metaclass=TraceAttribute): __metaclass__ = TraceAttribute a = 1 b = 2 a = A() a.a # output: __getattribute__:('a',){} a.b
试解释为何给 getattribute 打补丁成功,而 new 打补丁失败。
若是我坚持使用元类给 new 打补丁来实现单例模式,应该怎么修改?
答案
其实这是最气人的一点,类里的 new 是一个 staticmethod 所以替换的时候必须以 staticmethod 进行替换。答案以下:
class SingleMeta(type): def __init__(cls, name, bases, dict): cls._instance = None __new__o = cls.__new__ @staticmethod def __new__(cls, *args, **kwargs): if cls._instance: return cls._instance cls._instance = cv = __new__o(cls, *args, **kwargs) return cv cls.__new__ = __new__ class A(object): __metaclass__ = SingleMeta print(A() is A()) # output: True
感谢师父大人的一套题让我开启新世界的大门,恩,博客上无法艾特,只能传递心意了。说实话 Python 的动态特性可让其用众多 black magic 去实现一些很舒服的功能,固然这也对咱们对语言特性及坑的掌握也变得更严格了,愿各位 Pythoner 没事阅读官方文档,早日达到装逼如风,常伴吾身的境界。