专栏首页天澄技术杂谈剖析 Python 面试知识点(一): 魔法方法、闭包/自省、装饰器/生成器
原创

剖析 Python 面试知识点(一): 魔法方法、闭包/自省、装饰器/生成器

知识点整理基于 Python3.

1. Python 魔法方法

在Python中用双下划线__包裹起来的方法被成为魔法方法,可以用来给类提供算术、逻辑运算等功能,让这些类能够像原生的对象一样用更标准、简洁的方式进行这些操作。 下面介绍常常被问到的几个魔法方法。

1.1 __init__

__init__方法做的事情是在对象创建好之后初始化变量。很多人以为__init__是构造方法,其实不然,真正创建实例的是__new__方法,下面会讲它,先来看看__init__方法。

class Person(object):
    def __init__(self, name, age):
        print("in __init__")
        self.name = name
        self.age = age 

p = Person("TianCheng", 27) 
print("p:", p)

输出:

in __init__
p: <__main__.Person object at 0x105a689e8>

明白__init__负责初始化工作,平常也是我们经常用到的。。

1.2 __new__

构造方法: __new__(cls, […]) __new__是Python中对象实例化时所调用的第一个函数,在__init__之前被调用。__new__将class作为他的第一个参数, 并返回一个这个class的 instance。而__init__是将 instance 作为参数,并对这个 instance 进行初始化操作。每个实例创建时都会调用__new__函数。下面来看一个例子:

class Person(object):
    def __new__(cls, *args, **kwargs):
        print("in __new__")
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print("in __init__")
        self._name = name
        self._age = age

p = Person("TianCheng", 27)
print("p:", p)

输出结果:

in __new__
in __init__
p: <__main__.Person object at 0x106ed9c18>

可以看到先执行 new 方法创建对象,然后 init 进行初始化。假设将__new__方法中不返还该对象,会有什么结果了?

class Person(object):
    def __new__(cls, *args, **kwargs):
        print("in __new__")
        instance = super().__new__(cls)
        #return instance

    def __init__(self, name, age):
        print("in __init__")
        self._name = name
        self._age = age

p = Person("TianCheng", 27)
print("p:", p)

# 输出:
in __new__
p: None

发现如果 new 没有返回实例化对象,init 就没法初始化了。

如何使用 new 方法实现单例(高频考点):

class SingleTon(object):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            cls._instance = cls.__new__(cls, *args, **kwargs)
        return cls._instance

s1 = SingleTon()
s2 = SingleTon()
print(s1)
print(s2)

输出结果:

<__main__.SingleTon object at 0x1031cfcf8>
<__main__.SingleTon object at 0x1031cfcf8>

s1, s2 内存地址一致,实现单例效果。

1.3 __call__

__call__ 方法,先需要明白什么是可调用对象,平时自定义的函数、内置函数和类都属于可调用对象,但凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数 callable。举例如下:

class A(object):
    def __init__(self):
        print("__init__ ")
        super(A, self).__init__()

    def __new__(cls):
        print("__new__ ")
        return super(A, cls).__new__(cls)

    def __call__(self):  # 可以定义任意参数
        print('__call__ ')

a = A()
a()
print(callable(a))  # True

输出:

__new__
__init__
__call__
True

执行 a() 才会打印出 __call__。 a是一个实例化对象,也是一个可调用对象。

1.4 __del__

__del__ 析构函数,当删除一个对象时,则会执行此方法,对象在内存中销毁时,自动会调用此方法。举例:

class People:
    def __init__(self,name,age):
        self.name=name
        self.age=age

    def __del__(self): # 在对象被删除的条件下,自动执行
        print('__del__')

obj=People("Tiancheng", 27)
#del obj #obj.__del__()      #先删除的情况下,直接执行__del__

输出结果:

__del__

2. 闭包 和 自省

2.1 闭包

2.1.1 什么闭包

简单的说,如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。来看一个简单的例子:

>>>def addx(x):
>>>    def adder(y): return x + y
>>>    return adder
>>> c =  addx(8)
>>> type(c)
<type 'function'>
>>> c.__name__
'adder'
>>> c(10)
18

其中 adder(y) 函数就是闭包。

2.1.2 实现一个闭包并可以修改外部变量

def foo():
    a = 1
    def bar():
        a = a + 1
        return a
    return bar
c = foo()
print(c())

有上面一个小例子,目的是每次执行一次,a 自增1,执行后是否正确了?显示会报下面错误。

local variable 'a' referenced before assignment

原因是bar()函数中会把a作为局部变量,而bar中没有对a进行声明。 如果面试官问你,在 Python2 和 Python3 中如何修改 a 的值了。 Python3 中只需引入 nonlocal 关键字即可:

def foo():
    a = 1
    def bar():
        nonlocal a
        a = a + 1
        return a
    return bar
c = foo()
print(c()) # 2

而在 Python2 中没有 nonlocal 关键字,该如何实现了:

def foo():
    a = [1]
    def bar():
        a[0] = a[0] + 1
        return a[0]
    return bar
c = foo()
print(c()) # 2

需借助可变变量实现,比如dict和list对象。

闭包的一个常用场景就是 装饰器。 后面会讲到。

2.2 自省(反射)

自省,也可以说是反射,自省在计算机编程中通常指这种能力:检查某些事物以确定它是什么、它知道什么以及它能做什么。 与其相关的主要方法:

  • hasattr(object, name) 检查对象是否具体 name 属性。返回 bool.
  • getattr(object, name, default) 获取对象的name属性。
  • setattr(object, name, default) 给对象设置name属性
  • delattr(object, name) 给对象删除name属性
  • dir([object]) 获取对象大部分的属性
  • isinstance(name, object) 检查name是不是object对象
  • type(object) 查看对象的类型
  • callable(object) 判断对象是否是可调用对象
>>> class A:
...   a = 1
...
>>> hasattr(A, 'a')
True
>>> getattr(A, 'a')
1
>>> setattr(A, 'b', 1)
>>> getattr(A, 'b')
1
>>> delattr(A, 'b')
>>> hasattr(A, 'b')
False
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> isinstance(1, int)
True
>>> type(A)
<class 'type'>
>>> type(1)
<class 'int'>
>>> callable(A)
True

3. 装饰器 和 迭代器

3.1 装饰器

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能(设计模式中的装饰器模式),装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。

3.1.1 简单装饰器

先来看一个之前闭包的例子:

def my_logging(func):

    def wrapper():
        print("{} is running.".format(func.__name__))
        return func()  # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
    return wrapper

def foo():
    print("this is foo function.")

foo = my_logging(foo)  # 因为装饰器 my_logging(foo) 返回的时函数对象 wrapper,这条语句相当于  foo = wrapper
foo() # 执行foo相当于执行wrapper

但在Python里有@语法糖,则可以直接这样做:

def my_logging(func):

    def wrapper():
        print("{} is running.".format(func.__name__))
        return func()
    return wrapper

@my_logging
def foo():
    print("this is foo function.")

foo()

上面二者都会有如下打印结果:

foo is running.
this is foo function.

my_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 my_logging 装饰了一样 my_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程(AOP)。

如果 foo 带有参数,如何将参数带到 wrapper 中了?

def my_logging(func):

    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    print("this is foo function.")
    return x + y

print(foo(1, 2))

可以通过 *args, **kwargs 接收参数,然后带入func中执行,上面执行结果为:

foo is running.
this is foo function.
3

3.1.2 带参数的装饰器

装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样就大大增加了灵活性,比如在日志告警场景中,可以根据不同的告警定告警等级: info/warn等。

def my_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "info":
                print("{} is running. level: ".format(func.__name__), level)
            elif level == "warn":
                print("{} is running. level: ".format(func.__name__), level)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@my_logging(level="info")
def foo(name="foo"):
    print("{} is running".format(name))

@my_logging(level="warn")
def bar(name="bar"):
    print("{} is running".format(name))

foo()
bar()

结果输出:

foo is running. level:  info
foo is running
bar is running. level:  warn
bar is running

上面的 my_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当使用@my_logging(level="info")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。 @my_logging(level="info")等价于@decorator

3.1.3 类装饰器

装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class MyLogging(object):

    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print("class decorator starting.")
        a = self._func(*args, **kwargs)
        print("class decorator end.")
        return a

@MyLogging
def foo(x, y):
    print("foo is running")
    return x + y

print(foo(1, 2))

输出结果:

class decorator starting.
foo is running
class decorator end.
3

3.1.4 functools.wraps

Python 中还有一个装饰器的修饰函数 functools.wraps,先来看看它的作用是什么?先来看看有一个问题存在,因为原函数被装饰函数装饰后,发生了一下变化:

def my_logging(func):

    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    """
    add function
    """
    print("this is foo function.")
    return x + y

print(foo(1, 2))
print("func name:", foo.__name__)
print("doc:", foo.__doc__)

打印结果:

foo is running.
this is foo function.
3
func name: wrapper
doc: None

问题出来了,func name 应该打印出 foo 才对,而且 doc 也不为None。由此发现原函数被装饰函数装饰之后,元信息发生了改变,这明显不是我们想要的,Python里可以通过functools.wraps来解决,保持原函数元信息。

from functools import wraps

def my_logging(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    """
    add function
    """
    print("this is foo function.")
    return x + y

print(foo(1, 2))
print("func name:", foo.__name__)
print("doc:", foo.__doc__)

输出结果:

foo is running.
this is foo function.
3
func name: foo
doc:
    add function

3.1.5 多个装饰器的执行顺序

@a
@b
@c
def f ():
    pass

执行顺序为 f = a(b(c(f)))

3.2 迭代器 VS 生成器

先来看一个关系图:

3.2.1 container(容器)

container 可以理解为把多个元素组织在一起的数据结构,container中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中。在Python中,常见的container对象有:

list, deque, ....
set, frozensets, ....
dict, defaultdict, OrderedDict, Counter, ....
tuple, namedtuple, …
str

举例:

>>> assert 1 in [1, 2, 3]      # lists
>>> assert 4 not in [1, 2, 3]
>>> assert 1 in {1, 2, 3}      # sets
>>> assert 4 not in {1, 2, 3}
>>> assert 1 in (1, 2, 3)      # tuples
>>> assert 4 not in (1, 2, 3)

3.2.2 可迭代对象(iterables) vs 迭代器(iterator)

大部分的container都是可迭代对象,比如 list or set 都是可迭代对象,可以说只要是可以返回一个迭代器的都可以称作可迭代对象。下面看一个例子:

>>> x = [1, 2, 3]
>>> y = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>

可见, x 是可迭代对象,这里也叫container。y 则是迭代器,且实现了__iter____next__ 方法。它们之间的关系是:

那什么是迭代器了?上面例子中有2个方法 iter and next。可见通过iter方法后就是迭代器。 它是一个带状态的对象,调用next方法的时候返回容器中的下一个值,可以说任何实现了__iter__和next方法的对象都是迭代器,__iter__返回迭代器自身,next返回容器中的下一个值,如果容器中没有更多元素了,则抛异常。 迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。

3.2.3 生成器(generator)

生成器一定是迭代器,是一种特殊的迭代器,特殊在于它不需要再像上面的__iter__()和next方法了,只需要一个yiled关键字。下面来看一个例子: 用生成器实现斐波拉契:

# content of test.py
def fib(n):
    prev, curr = 0, 1
    while n > 0:
        yield curr
        prev, curr = curr, curr + prev
        n -= 1

到终端执行fib函数:

>>> from test import fib
>>> y = fib(10)
>>> next(y)
1
>>> type(y)
<class 'generator'>
>>> next(y)
1
>>> next(y)
2

fib就是一个普通的python函数,它特殊的地方在于函数体中没有return关键字,函数的返回值是一个生成器对象(通过 yield 关键字)。当执行f=fib()返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。 假设有千万个对象,需要顺序调取,如果一次性加载到内存,对内存是极大的压力,有生成器之后,可以需要的时候去生成一个,不需要的则也不会占用内存。

平常可能还会遇到一些生成器表达式,比如:

>>> a = (x*x for x in range(10))
>>> a
<generator object <genexpr> at 0x102d79a20>
>>> next(a)
0
>>> next(a)
1
>>> a.close()
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

这些小技巧也是非常有用的。close可以关闭生成器。生成器中还有一个send方法,其中send(None)与next是等价的。

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> generator = double_inputs()
>>> generator.send(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> generator.send(None)
>>> generator.send(10)
20
>>> next(generator)
>>> generator.send(20)
40

从上面的例子中可以看出,生成器可以接收参数,通过send(value)方法,且第一次不能直接send(value),需要send(None)或者next()执行之后。也就是说调用send传入非None值前,生成器必须处于挂起状态,否则将抛出异常。

3.2.4 迭代器和生成器的区别

可能你看完上面的,有点好奇到底他们二者有什么区别了?

  • 迭代器是一个更抽象的概念,任何对象,如果它有next方法(next python3,python2 是 __next__方法)和__iter__方法,则可以称作迭代器。
  • 每个生成器都是一个迭代器,但是反过来不行。通常生成器是通过调用一个或多个yield表达式构成的函数s生成的。同时满足迭代器的定义。
  • 生成器能做到迭代器能做的所有事,而且因为自动创建了iter()和 next()方法,生成器显得特别简洁,而且生成器也是高效的。

『剖析Python面试知识点』完整内容请查看 :

https://gitbook.cn/gitchat/activity/5c8e364590020a6262806c8d

更多精彩文章请关注公众号: 『天澄技术杂谈』

天澄技术杂谈

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 剖析 Python 面试知识点(二)- 内存管理和垃圾回收机制

    Python 中一切皆对象,对象又可以分为可变对象和不可变对象。二者可以通过原地修改,如果修改后地址不变,则是可变对象,否则为不可变对象,地址信息可以通过id(...

    天澄技术杂谈
  • 『Microservices & Nameko』Python 微服务实践

    微服务最近一二年非常热门,谈论也比较多,简单的说,微服务将单一应用程序作为由众多小型服务构成之套件加以开发的方式,其中各项服务都拥有自己的进程并利用轻量化机制(...

    天澄技术杂谈
  • 『MySQL』深入理解事务的来龙去脉

    距离上一篇MySQL的文章已经过去一个月了,终于有时间来写写关于MySQL的事务了。本文内容默认是针对 MySQL InnoDB 引擎。

    天澄技术杂谈
  • Python3 与 C# 扩展之~装饰器专栏

    终于期末考试结束了,聪明的小明同学现在当然是美滋滋的过暑假了,左手一只瓜,右手一本书~正在给老乡小张同学拓展他研究多日的知识点

    逸鹏
  • Python之路【第七篇】:Python

    py3study
  • 面向对象进阶

    Wisdom is knowing what to do next , virtue is doing it .

    GH
  • python 函数(3)

    py3study
  • Python 强化训练:第八篇

    谢伟
  • PySpark工作原理

    Spark是一个开源的通用分布式计算框架,支持海量离线数据处理、实时计算、机器学习、图计算,结合大数据场景,在各个领域都有广泛的应用。Spark支持多种开发语言...

    Fayson
  • PrivaZer 2020 捐助者版 3.0

    PrivaZer 2020 捐赠者版是一个非常有用的应用程序,将使您能够清除你系统上的任何应用程序的互联网浏览痕迹。此实用程序可以进行深度扫描,安全地...

    萌海无涯

扫码关注云+社区

领取腾讯云代金券