听说你会Python?做几道题呗

作者:manjusaka

原文链接:http://manjusaka.itscoder.com/2016/11/18/Someone-tell-me-that-you-think-Python-is-simple/

前言

最近觉得 Python 太"简单了",于是在师父川爷面前放肆了一把:"我觉得 Python 是世界上最简单的语言!"。于是川爷嘴角闪过了一丝轻蔑的微笑(内心 OS:Naive,作为一个 Python 开发者,我必须要给你一点人生经验,不然你不知道天高地厚!)于是川爷给我了一份满分 100 分的题,然后这篇文章就是记录下做这套题所踩过的坑。

1.列表生成器

描述

下面的代码会报错,为什么?

  1. class A(object):

  2.    x = 1

  3.    gen = (x for _ in xrange(10))  # gen=(x for _ in range(10))

  4. if __name__ == "__main__":

  5.    print(list(A.gen))

答案

这个问题是变量作用域问题,在 gen =( x for _ in xrange (10)) 中 gen 是一个 generator ,在 generator 中变量有自己的一套作用域,与其余作用域空间相互隔离。因此,将会出现这样的 NameError:name ' x ' is not defined 的问题,那么解决方案是什么呢?答案是:用 lambda 。

  1. class A(object):

  2.    x = 1

  3.    gen = (lambda x: (x for _ in xrange(10)))(

  4.        x)  # gen=(x for _ in range(10))

  5. if __name__ == "__main__":

  6.    print(list(A.gen))

或者这样

  1. class A(object):

  2.    x = 1

  3.    gen = (A.x for _ in xrange(10))  # gen=(x for _ in range(10))

  4. if __name__ == "__main__":

  5.    print(list(A.gen))

补充

感谢评论区几位提出的意见,这里我给一份官方文档的说明吧:

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 :

  1. class A:

  2.    a = 42

  3.    b = list(a + i for i in range(10))

参考链接 Python 2 Execution-Model:Naming-and-Binding , Python 3 Execution-Model:Resolution-of-Names。据说这是 PEP 227 中新增的提案,我回去会进一步详细考证。再次拜谢评论区 @没头脑很着急 @涂伟忠 @ Cholerae 三位的勘误指正。

2.装饰器

描述

我想写一个类装饰器用来度量函数/方法运行时间

  1. import time

  2. class Timeit(object):

  3.    def __init__(self, func):

  4.        self._wrapped = func

  5.    def __call__(self, *args, **kws):

  6.        start_time = time.time()

  7.        result = self._wrapped(*args, **kws)

  8.        print("elapsed time is %s " % (time.time() - start_time))

  9.        return result

这个装饰器能够运行在普通函数上:

  1. @Timeit

  2. def func():

  3.    time.sleep(1)

  4.    return "invoking function func"

  5. if __name__ == '__main__':

  6.    func()  # output: elapsed time is 1.00044410133

但是运行在方法上会报错,为什么?

  1. class A(object):

  2.    @Timeit

  3.    def func(self):

  4.        time.sleep(1)

  5.        return 'invoking method func'

  6. if __name__ == '__main__':

  7.    a = A()

  8.    a.func()  # Boom!

如果我坚持使用类装饰器,应该如何修改?

答案

使用类装饰器后,在调用 func 函数的过程中其对应的 instance 并不会传递给 __call__ 方法,造成其 mehtod unbound ,那么解决方法是什么呢?描述符赛高

  1. class Timeit(object):

  2.    def __init__(self, func):

  3.        self.func = func

  4.    def __call__(self, *args, **kwargs):

  5.        print('invoking Timer')

  6.    def __get__(self, instance, owner):

  7.        return lambda *args, **kwargs: self.func(instance, *args, **kwargs)

3. Python 调用机制

描述

我们知道 __call__ 方法可以用来重载圆括号调用,好的,以为问题就这么简单? Naive !

  1. class A(object):

  2.    def __call__(self):

  3.        print("invoking __call__ from A!")

  4. if __name__ == "__main__":

  5.    a = A()

  6.    a()  # output: invoking __call__ from A

现在我们可以看到 () 似乎等价于 a.__call__ () ,看起来很 Easy 对吧,好的,我现在想作死,又写出了如下的代码,

  1. a.__call__ = lambda: "invoking __call__ from lambda"

  2. a.__call__()

  3. # output:invoking __call__ from lambda

  4. a()

  5. # output:invoking __call__ from 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\ ):

同时官方也给出了一个例子:

  1. class C(object):

  2.    pass

  3. c = C()

  4. c.__len__ = lambda: 5

  5. len(c)

  6. # Traceback (most recent call last):

  7. #  File "<stdin>", line 1, in <module>

  8. # TypeError: object of type 'C' has no len()

回到我们的例子上来,当我们在执行 a.__call__ = lambda :" invoking __call__ from lambda " 时,的确在我们在 a.__dict__ 中新增加了一个 key 为 __call__ 的 item ,但是当我们执行 () 时,因为涉及特殊方法的调用,因此我们的调用过程不会从 a.__dict__ 中寻找属性,而是从 tyee ( a ). __dict__ 中寻找属性。因此,就会出现如上所述的情况。

4.描述符

描述

我想写一个 Exam 类,其属性 math 为 [ 0,100 ] 的整数,若赋值时不在此范围内则抛出异常,我决定用描述符来实现这个需求。

  1. class Grade(object):

  2.    def __init__(self):

  3.        self._score = 0

  4.    def __get__(self, instance, owner):

  5.        return self._score

  6.    def __set__(self, instance, value):

  7.        if 0 <= value <= 100:

  8.            self._score = value

  9.        else:

  10.            raise ValueError('grade must be between 0 and 100')

  11. class Exam(object):

  12.    math = Grade()

  13.    def __init__(self, math):

  14.        self.math = math

  15. if __name__ == '__main__':

  16.    niche = Exam(math=90)

  17.    print(niche.math)

  18.    # output : 90

  19.    snake = Exam(math=75)

  20.    print(snake.math)

  21.    # output : 75

  22.    snake.math = 120

  23.    # output: ValueError:grade must be between 0 and 100!

看起来一切正常。不过这里面有个巨大的问题,尝试说明是什么问题 为了解决这个问题,我改写了 Grade 描述符如下:

  1. class Grad(object):

  2.    def __init__(self):

  3.        self._grade_pool = {}

  4.    def __get__(self, instance, owner):

  5.        return self._grade_pool.get(instance, None)

  6.    def __set__(self, instance, value):

  7.        if 0 <= value <= 100:

  8.            _grade_pool = self.__dict__.setdefault('_grade_pool', {})

  9.            _grade_pool[instance] = value

  10.        else:

  11.            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 ,从而产生内存泄露。那么这里该怎么解决呢?两种方法 第一种:

  1. class Grad(object):

  2.    def __init__(self):

  3.        import weakref

  4.        self._grade_pool = weakref.WeakKeyDictionary()

  5.    def __get__(self, instance, owner):

  6.        return self._grade_pool.get(instance, None)

  7.    def __set__(self, instance, value):

  8.        if 0 <= value <= 100:

  9.            _grade_pool = self.__dict__.setdefault('_grade_pool', {})

  10.            _grade_pool[instance] = value

  11.        else:

  12.            raise ValueError("fuck")

weakref 库中的 WeakKeyDictionary 所产生的字典的 key 对于对象的引用是弱引用类型,其不会造成内存引用计数的增加,因此不会造成内存泄露。同理,如果我们为了避免 value 对于对象的强引用,我们可以使用 WeakValueDictionary 。

第二种:在 Python 3.6 中,实现的 PEP 487 提案,为描述符新增加了一个协议,我们可以用其来绑定对应的对象:

  1. class Grad(object):

  2.    def __get__(self, instance, owner):

  3.        return instance.__dict__[self.key]

  4.    def __set__(self, instance, value):

  5.        if 0 <= value <= 100:

  6.            instance.__dict__[self.key] = value

  7.        else:

  8.            raise ValueError("fuck")

  9.    def __set_name__(self, owner, name):

  10.        self.key = name

这道题涉及的东西比较多,这里给出一点参考链接, invoking-descriptors , Descriptor HowTo Guide , PEP 487 , what`s new in Python 3.6 。

5. Python 继承机制

描述

试求出以下代码的输出结果。

  1. class Init(object):

  2.    def __init__(self, value):

  3.        self.val = value

  4. class Add2(Init):

  5.    def __init__(self, val):

  6.        super(Add2, self).__init__(val)

  7.        self.val += 2

  8. class Mul5(Init):

  9.    def __init__(self, val):

  10.        super(Mul5, self).__init__(val)

  11.        self.val *= 5

  12. class Pro(Mul5, Add2):

  13.    pass

  14. class Incr(Pro):

  15.    csup = super(Pro)

  16.    def __init__(self, val):

  17.        self.csup.__init__(val)

  18.        self.val += 1

  19. p = Incr(5)

  20. print(p.val)

答案

输出是 36 ,具体可以参考 New-style Classes , multiple-inheritance

6. Python 特殊方法

描述

我写了一个通过重载 * new * 方法来实现单例模式的类。

  1. class Singleton(object):

  2.    _instance = None

  3.    def __new__(cls, *args, **kwargs):

  4.        if cls._instance:

  5.            return cls._instance

  6.        cls._isntance = cv = object.__new__(cls, *args, **kwargs)

  7.        return cv

  8. sin1 = Singleton()

  9. sin2 = Singleton()

  10. print(sin1 is sin2)

  11. # output: True

现在我有一堆类要实现为单例模式,所以我打算照葫芦画瓢写一个元类,这样可以让代码复用:

  1. class SingleMeta(type):

  2.    def __init__(cls, name, bases, dict):

  3.        cls._instance = None

  4.        __new__o = cls.__new__

  5.        def __new__(cls, *args, **kwargs):

  6.            if cls._instance:

  7.                return cls._instance

  8.            cls._instance = cv = __new__o(cls, *args, **kwargs)

  9.            return cv

  10.        cls.__new__ = __new__

  11. class A(object):

  12.    __metaclass__ = SingleMeta

  13. a1 = A()  # what`s the fuck

哎呀,好气啊,为啥这会报错啊,我明明之前用这种方法给 __getattribute__ 打补丁的,下面这段代码能够捕获一切属性调用并打印参数

  1. class TraceAttribute(type):

  2.    def __init__(cls, name, bases, dict):

  3.        __getattribute__o = cls.__getattribute__

  4.        def __getattribute__(self, *args, **kwargs):

  5.            print('__getattribute__:', args, kwargs)

  6.            return __getattribute__o(self, *args, **kwargs)

  7.        cls.__getattribute__ = __getattribute__

  8. # Python 3 是 class A(object,metaclass=TraceAttribute):

  9. class A(object):

  10.    __metaclass__ = TraceAttribute

  11.    a = 1

  12.    b = 2

  13. a = A()

  14. a.a

  15. # output: __getattribute__:('a',){}

  16. a.b

试解释为什么给 * getattribute * 打补丁成功,而 * new * 打补丁失败。 如果我坚持使用元类给 * new * 打补丁来实现单例模式,应该怎么修改?

答案

其实这是最气人的一点,类里的 __new__ 是一个 staticmethod 因此替换的时候必须以 staticmethod 进行替换。答案如下:

  1. class SingleMeta(type):

  2.    def __init__(cls, name, bases, dict):

  3.        cls._instance = None

  4.        __new__o = cls.__new__

  5.        @staticmethod

  6.        def __new__(cls, *args, **kwargs):

  7.            if cls._instance:

  8.                return cls._instance

  9.            cls._instance = cv = __new__o(cls, *args, **kwargs)

  10.            return cv

  11.        cls.__new__ = __new__

  12. class A(object):

  13.    __metaclass__ = SingleMeta

  14. print(A() is A())  # output: True

结语

感谢师父大人的一套题让我开启新世界的大门,恩,博客上没法艾特,只能传递心意了。说实话 Python 的动态特性可以让其用众多 black magic 去实现一些很舒服的功能,当然这也对我们对语言特性及坑的掌握也变得更严格了,愿各位 Pythoner 没事阅读官方文档,早日达到装逼如风,常伴吾身的境界。


题图:pexels,CC0 授权。


点击阅读原文,查看更多 Python 教程和资源。




阅读原文:http://mp.weixin.qq.com/s?__biz=MzAwNDc0MTUxMw==&mid=2649639851&idx=1&sn=74581e6ab370ba0bbb233da1a585ad28&chksm=833dab4db44a225bd11154f8e18d698b27b981ad520efd37d53165af1d7fb7df2210131cf936#rd
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 定义类并创建实例 在Python中,类通过 class 关键字定义。以 Person 为例,定义一个Person类...
    绩重KF阅读 3,872评论 0 13
  • 内置函数Python解释器内置了许多功能和类型,总是可用的。他们是按字母顺序列在这里。 abs(x)返回一个数的绝...
    uangianlap阅读 1,177评论 0 0
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,397评论 0 6
  • 看到几个群都在分享,跳出来的都是自己的不开心不如意。虽然我知道大家来说肯定就是因为有事儿。但仍然跳出来了...
    福娃婧阅读 110评论 0 0
  • 小赵是我的女同学 ,长得很漂亮。在读中学的时候和一位男生谈恋爱。两个人感情非常好,毕业后,有一次她还把他约到家里。...
    温暖鞍山阅读 215评论 0 1