OO & Class

Someone2021年9月5日大约 14 分钟

Class

作用域和命名空间

首先来看一个例子,参考文献 P1.1:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    # After local assignment: test spam
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    # After nonlocal assignment: nonlocal spam
    do_global()
    print("After global assignment:", spam)
    # After global assignment: nonlocal spam
    # 这时候还未修改是因为还在执行 scope_test 内部

scope_test()
print("In global scope:", spam)
# In global scope: global spam

附上官方的解释:

请注意 局部 赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定。 nonlocal 赋值会改变 scope_test 对 spam 的绑定,而 global 赋值会改变模块层级的绑定。

您还可以发现在 global 赋值之前没有 spam 的绑定。

上述代码的理解应该包括一下几点:

  1. 当内部作用域想修改外部作用域的变量时,就要用到 globalnonlocal 关键字了。如 do_local() 中的 nolocal 关键字可以成功修改 spam("test spam") 的值。

    举例而言:

    #!/usr/bin/python3
    
    def outer():
        num = 10
        def inner():
            nonlocal num   # nonlocal关键字声明
            num = 100
            print(num)     # 100, nonlocal 关键字修改了函数 outer 内部的 num 变量
        inner()
        print(num)         # 100
    outer()
    
  2. global 关键字一般是用来修改函数外部的变量(全局变量)。

    举例而言:

    #!/usr/bin/python3
    
    num = 1
    def fun1():
        global num  # 需要使用 global 关键字声明
        print(num)  # 取到全局变量 1
        num = 123
        print(num)  # 123 成功给全局变量赋值
    fun1()
    print(num)      # 123 全局变量值被修改
    

    上面的 scope_test() 执行后,才修改到了函数外部的全局变量。

LEGB

虽然作用域是静态地确定的,但它们会被动态地使用。 在执行期间的任何时刻,会有 3 或 4 个命名空间可被直接访问的嵌套作用域:

  • Local: 最先搜索的最内部作用域包含局部名称
  • Encrosing: 从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
  • Global: 倒数第二个作用域包含当前模块的全局名称
  • Built-in: 最外面的作用域(最后搜索)是包含内置名称的命名空间

self

方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用 x.f() 其实就相当于 MyClass.f(x)。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。

方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。

给类添加迭代器

定义一个 __iter__() 方法来返回一个带有 __next__() 方法的对象。 如果类已定义了 __next__(),则 __iter__() 可以简单地返回 self:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

更优雅的方式是定义一个生成器:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

类方法 classmethod

如果我们想通过类来调用方法,而不是通过实例,那应该怎么办呢?

Python 提供了 classmethod 装饰器让我们实现上述功能,看下面的例子:

class A(object):
    bar = 1
    @classmethod
    def class_foo(cls):
        print 'Hello, ', cls
        print cls.bar

>>> A.class_foo()   # 直接通过类来调用方法
Hello,  <class '__main__.A'>
1

classmethod 装饰的方法由于持有 cls 参数,因此我们可以在方法里面调用类的属性、方法,比如 cls.bar

如果在类中增加 __init__ 方法,可以看到类直接是无法调用到 __init__ 中的属性的:

class A(object):
    bar = 1

    def __init__(self):
        self.lis = [1, 2, 3]

    @classmethod
    def class_foo(cls):
        print('Hello, ', cls)
        print(cls.bar)
        print(cla.lis)


if __name__ == '__main__':
    A.class_foo()

>>> AttributeError: type object 'A' has no attribute 'lis'

静态方法 staticmethod

在类中往往有一些方法跟类有关系,但是又不会改变类和实例状态的方法,这种方法是静态方法,我们使用 staticmethod 来装饰。

Why @staticmethod?

静态方法没有 selfcls 参数,可以把它看成是一个普通的函数,我们当然可以把它写到类外面,但这是不推荐的,因为这不利于代码的组织和命名空间的整洁。

class A(object):
    bar = 1

    @staticmethod
    def static_foo():
        print('Hello, ', A.bar)


if __name__ == '__main__':
    a = A()
    a.static_foo()
    A.static_foo()

>>> Hello,  1
>>> Hello,  1

举一反三,我们对 A 中的 bar 属性能否进行修改呢?从下面例子中可以看出类属性被修改了

if __name__ == '__main__':
    A.bar = 3
    a = A()
    a.static_foo()

    A.bar = 2
    A.static_foo()

>>> Hello,  3
>>> Hello,  2

3. 继承与多态

函数继承

  1. 如果子类没有定义自己的初始化函数,那么父类的初始化函数会被默认调用;但是如果这种情况下实例化子类的对象,应该传入父类的初始化参数,否则会报错;

  2. 如果子类定义了自己的初始化函数,并且没有显式调用父类的初始化函数,则父类的属性不会被初始化;

    如果子类定义了自己的初始化函数,并且显式调用了父类的初始化函数,则子类和父类的属性都会被初始化;

  3. 如果在子类中需要父类的构造方法就需要显式地调用父类的构造方法,或者不重写父类的构造方法。

    子类不重写 init,实例化子类时,会自动调用父类定义的 init

    class Father(object):
        def __init__(self, name):
            self.name=name
            print ( "name: %s" %( self.name) )
        def getName(self):
            return 'Father ' + self.name
    
    class Son(Father):
        def getName(self):
            return 'Son '+self.name
    
    if __name__=='__main__':
        son=Son('runoob')
        print ( son.getName() )
    
    # name: runoob
    # Son runoob
    
  4. 如果重写了**init** 时,实例化子类,就不会调用父类已经定义的 init,语法格式如下:

    class Father(object):
        def __init__(self, name):
            self.name=name
            print ( "name: %s" %( self.name) )
        def getName(self):
            return 'Father ' + self.name
     
    class Son(Father):
        def __init__(self, name):
            print ( "hi" )
            self.name =  name
        def getName(self):
            return 'Son '+self.name
     
    if __name__=='__main__':
        son=Son('runoob')
        print ( son.getName() )
        
    # hi
    # Son runoob
    
  5. 如果重写了**init** 时,要继承父类的构造方法,可以使用 super 关键字:super(子类,self).__init__(参数1,参数2,....) 或者 父类名称.__init__(self,参数1,参数2,...)

    class Father(object):
        def __init__(self, name):
            self.name=name
            print ( "name: %s" %( self.name))
        def getName(self):
            return 'Father ' + self.name
    
    class Son(Father):
        def __init__(self, name):
            super(Son, self).__init__(name)
            print ("hi")
            self.name =  name
        def getName(self):
            return 'Son '+self.name
    
    if __name__=='__main__':
        son=Son('runoob')
        print ( son.getName() )
    
    # name: runoob
    # hi
    # Son runoob
    

    拓展:如下代码可以看出来,子类也通过 super 继承了父类的属性:

    class Father(object):
        def __init__(self, name):
            self.name = name
            self.age = 10
            print("name: %s" % (self.name))
    
        def getName(self):
            return 'Father ' + self.name
    
    
    class Son(Father):
        def __init__(self, name):
            super(Son, self).__init__(name)
            print("hi")
            self.name = name
    
        def getName(self):
            return 'Son ' + self.name + str(self.age)
    
    
    if __name__ == '__main__':
        son = Son('runoob')
        print(son.getName())
    
    
    """
    name: runoob
    hi
    Son runoob10
    """
    

    在super机制里,可以保证公共父类仅被执行一次,至于执行的顺序,是按照**MRO(Method Resolution Order)open in new window**方法解析顺序 进行的。

    简单理解,MRO顺序就是代码中的书写顺序

QA

❓❓❓ 子类继承父类时,实例化子类,会调用父类的 init 方法吗?

子类与父类的init

这是我经常混淆的点,可以通过下述的例子来观察,最终的结论是:不会

除非在子类中显式调用 super().__init__, 但是在这种情况下也需要注意 MRO 列表问题。

总结来说:如果子类和父类都有 __init__ 初始化方法,子类其实是重写了父类的 __init__ 方法,如果不显式调用父类的 __init__ 方法,父类的 __init__ 方法就不会被执行!

class Animal(object):
    def __init__(self, name):
        print('__init__Animal')
        self.name = name

    def greet(self):
        print('Hello, I am %s.' % self.name)


class Dog(Animal):
    def __init__(self, name):
        print('__init__Dog')
        self.name = name

    def greet(self):
        print('WangWang.., I am %s. ' % self.name)


if __name__ == '__main__':
    dog = Dog('dog')
    dog.greet()


""""
>>> __init__Dog
>>> WangWang.., I am dog. 
"""

继承易错知识点

看以下代码:

class A:
    def __init__(self):
        print('A')
        pass


class B(A):
    def __init__(self):
        print('B')
        A.__init__


class C(A):
    def __init__(self):
        print('C')
        A.__init__


if __name__ == '__main__':
    B()
    C()

# B
# C

我们可以看到,A.__init__ 并没有达到调用 A 的效果。正常的调用如下所示:

class A:
    def __init__(self):
        print('A')
        pass


class B(A):
    def __init__(self):
        print('B')
        A.__init__(self)


class C(A):
    def __init__(self):
        print('C')
        A.__init__(self)


if __name__ == '__main__':
    B()
    C()

# B
# A
# C
# A

如果使用 super 类的方式调用父类初始化方法,这种写法不与父类类名绑定,且可以保证菱形继承场景下,创建一个子类对象仅调用顶层父类初始化函数一次。

举例如下:

class A:
    def __init__(self):
        print('A')
        pass


class B(A):
    def __init__(self):
        print('B')
        A.__init__(self)


class C(A):
    def __init__(self):
        print('C')
        A.__init__(self)


class D(B, C):
    def __init__(self):
        print('D')
        B.__init__(self)
        C.__init__(self)


"""
以下是正确示例
"""


class A1:
    def __init__(self):
        print('A')
        pass


class B1(A1):
    def __init__(self):
        print('B')
        super(B1, self).__init__()


class C1(A1):
    def __init__(self):
        print('C')
        super(C1, self).__init__()


class D1(B1, C1):
    def __init__(self):
        print('D')
        super(D1, self).__init__()


if __name__ == '__main__':
    print('---D---')
    D()
    print('---D1---')
    D1()

"""
---D---
D
B
A
C
A
---D1---
D
B
C
A
"""

super()

在类的继承中,如果重定义某个方法,该方法会覆盖父类的同名方法,但有时,我们希望能同时实现父类的功能,这时,我们就需要调用父类的方法了,可通过使用 super 来实现,比如:

class Animal(object):
    def __init__(self, name):
        self.name = name

    def greet(self):
        print('name is', self.name)


class Dog(Animal):
    def greet(self):
        super().greet()
        print('WangWang...')


if __name__ == '__main__':
    d = Dog("wang_cai")
    d.greet()

#>>> name is wang_cai
#>>> WangWang...

super 的一个最常见用法可以说是在子类中调用父类的初始化方法了,比如:

class Base(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b


class A(Base):
    def __init__(self, a, b, c):
        super(A, self).__init__(a, b)  # Python3 可使用 super().__init__(a, b)
        self.c = c


if __name__ == '__main__':
    test = A(1, 2, 3)
    print(test.a, test.b, test.c)

#>>> 1 2 3

❗❗❗

super 其实和父类没有实质性的关联,MRO 列表

在多重继承的场景下会这样。对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序。 可以使用 C.mro() 查看。

class Base(object):
    def __init__(self):
        print "enter Base"
        print "leave Base"

class A(Base):
    def __init__(self):
        print "enter A"
        super(A, self).__init__()
        print "leave A"

class B(Base):
    def __init__(self):
        print "enter B"
        super(B, self).__init__()
        print "leave B"

class C(A, B):
    def __init__(self):
        print "enter C"
        super(C, self).__init__()
        print "leave C"

其对应的输出是:

>>> c = C()
enter C
enter A
enter B
enter Base
leave Base
leave B
leave A
leave C

4. 魔法方法 magic method

在 Python 中,我们可以经常看到以双下划线 __ 包裹起来的方法,比如最常见的 __init__,这些方法被称为魔法方法(magic method)特殊方法(special method)。 简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 __init__ 方法可以对实例属性进行初始化。

完整的特殊方法列表可在这里open in new window查看。

5 __new__

QA

❓❓❓ 为什么 new 的第一个参数是 cls 而不是 self?

注意

因为调用 __new__ 的时候,实例对象还没有被创建,__new__ 是一个静态方法。第一个参数 cls 表示当前的 class

❓❓❓ 如何理解 object.__new__的 object?

注意

__new__ 方法如果返回 cls 的对象(return super().__new__(cls)),则对象的 __init__ 方法将自动被调用。

只要调用父类的 __new__ 方法,__init__ 方法就默认被调用,object 类是最大的父类。

❓❓❓ 我们可以只使用 __new___ 来实例化对象实例吗?

注意

可以,但是不建议!还是建议使用 __init__

5.1 __new__

在 Python 中,当我们创建一个类的实例时,类会先调用 __new__(cls[, ...]) 来创建实例,然后 __init__ 方法再对该实例(self)进行初始化。

关于 __new____init__ 有几点需要注意:

  • __new__ 是在 __init__ 之前被调用的;
  • __new__ 是类方法,__init__ 是实例方法;
  • 重载 __new__ 方法,需要返回类的实例;

为什么我们一般在创建类的时候没有重载 __new__ 呢?

一般情况下,我们不需要重载 __new__ 方法。但在某些情况下,我们想控制实例的创建过程,这时可以通过重载 __new__ 方法来实现。

举例而言:

class A(object):
    _dict = dict()

    def __new__(cls):
        if 'key' in A._dict:
            print("EXISTS")
            print("A._dict['key']", A._dict['key'])
            return A._dict['key']
        else:
            print("__NEW__")
            return object.__new__(cls)

    def __init__(self):
        print("__INIT__")
        A._dict['key'] = 'aaa'


if __name__ == '__main__':
    a1 = A()
    a2 = A()
    a3 = A()

其对应的输出如下所示:

__NEW__
__INIT__
EXISTS
A._dict['key'] aaa
EXISTS
A._dict['key'] aaa

我们可以观察到:

  1. __init__ 方法始终被调用了;
  2. object.__new__(cls) 可以实例化对象。

🍉🍉🍉 关于 `object.__new__(cls)`

可以使用 object.__new__(cls) 实现单例(即一个类只有一个实例,例子如上面例子)

5.2 实例化的本质 newinit

本小节通过分析 __new____init__ 的关系总结实例化本质。

💘 💘 💘 先看例子:这是一个正确的示例

class Person():
    def __new__(cls, age):
        print('__new__, age:', age)
        # return super(Person, cls).__new__(cls) # ok
        # return object.__new__(cls) # ok
        return super().__new__(cls)

    def __init__(self, age):
        print('__init__, age:', age)
        self.age = age


if __name__ == '__main__':
    Person(100)

# >>> __new__, age: 100
# >>> __init__, age: 100
  1. 我们可以使用多种方式来实现 __new__
  2. __new____init__ 方法共享同名的参数,除了第一个从 cls 变成了 self
  3. 如果 __new__ 没有返回实例对象,则 __init__ 方法不会被调用

❌❌❌ 如果 __init__ 传入的参数比 __new__ 多的话会发生什么呢?

class Person():
    def __new__(cls, age, name):
        print('__new__, age:', age)
        return super().__new__(cls)

    def __init__(self, age):
        print('__init__, age:', age)
        self.age = age


if __name__ == '__main__':
    Person(100)

#>>> TypeError: __new__() missing 1 required positional argument: 'name'

❌❌❌ 如果 __init__ 传入的参数比 __new__ 少的话会发生什么呢?

class Person():
    def __new__(cls, age):
        print('__new__, age:', age)
        return super().__new__(cls)

    def __init__(self, age, name):
        print('__init__, age:', age)
        self.age = age


if __name__ == '__main__':
    Person(100)

#>>> TypeError: __init__() missing 1 required positional argument: 'name'

❗❗❗ 实例化的本质

✨✨✨ 实例化的本质

实例初始化本质是向 __new__ 中传参!

💘 💘 💘 我们常用的定义类的写法,最标准的写法参考如下:

class Person():
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls)

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


if __name__ == '__main__':
    p = Person(100, "zhanshen")

我们如果在创建实例的时候加入判断,可以分别如下:

  • __new__ 中判断参数。此时对象不会创建,即 __init__ 不会被调用;
  • ___init__ 中判断参数。此时对象会创建。

举例如下:

class Person():
    def __new__(cls, age):
        print('__new__')
        if age < 100:
            print('not created!')
            return cls
        return super().__new__(cls)

    def __init__(self, age):
        print('__init__')
        self.age = age


if __name__ == '__main__':
    p = Person(10)

#>>> __new__
#>>> not created!

可以看出,__init__ 未被调用,对象也未创建。如果使用 __init__ 判断的话,可以看到,对象被创建了。

class Person():
    def __new__(cls, age):
        print('__new__')
        return super().__new__(cls)

    def __init__(self, age):
        if age < 100:
            print('__init__')
            print('wrong!')
        self.age = age


if __name__ == '__main__':
    p = Person(10)

#>>> __new__
#>>> __init__
#>>> wrong!

5.3 __new__ 返回其他实例

我们还可以通过 __new__ 返回其他类的实例:如 return object.__new__(Person)

class Person(object):
    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)

    def __init__(self, age):
        self.age = age


class Test(object):
    def __new__(cls, *args, **kwargs):
        return object.__new__(Person)


if __name__ == '__main__':
    p = Test(100)
    p.age = 10
    print(type(p), p.age)

#>>> <class '__main__.Person'> 10

6. __str__

重写 __str__ 以达到打印的目的:

class Foo(object):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        print('__str__', self.name)
        return 'name is ' + self.name

    # def __repr__(self):
    #     print('__repr__', self.name)
    #     return 'name is ' + self.name
    __repr__ = __str__


if __name__ == '__main__':
    print(Foo('zhanshen'))

#>>> __str__ zhanshen
#>>> name is zhanshen

7. __call__

我们一般使用 obj.method() 来调用对象的方法,那能不能直接在实例本身上调用呢?在 Python 中,只要我们在类中定义 __call__ 方法,就可以对实例进行调用,比如下面的例子:

class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __call__(self, z):
        return self.x + self.y + z

使用方法如下:

>>> p = Point(3, 4)
>>> callable(p)     # 使用 callable 判断对象是否能被调用
True
>>> p(6)            # 传入参数,对实例进行调用,对应 p.__call__(6)
13                  # 3+4+6

8. __slot__

在 Python 中,我们在定义类的时候可以定义属性和方法。当我们创建了一个类的实例后,我们还可以给该实例绑定任意新的属性和方法。

class Point(object):    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

>>> p = Point(3, 4)
>>> p.z = 5    # 绑定了一个新的属性
>>> p.z
5
>>> p.__dict__
{'x': 3, 'y': 4, 'z': 5}

因此,为了不浪费内存,可以使用 __slots__ 来告诉 Python 只给一个固定集合的属性分配空间,对上面的代码做一点改进,如下:

class Point(object):
    __slots__ = ('x', 'y')       # 只允许使用 x 和 y

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

>>> p = Point(3, 4)
>>> p.z = 5
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'

使用 __slots__ 有一点需要注意的是,__slots__ 设置的属性仅对当前类有效,对继承的子类不起效,除非子类也定义了 __slots__,这样,子类允许定义的属性就是自身的 slots 加上父类的 slots。

9. 元类 metaclass

9.1 什么是元类

类是实例对象的模板,元类是类的模板

+----------+             +----------+             +----------+
|          |             |          |             |          |
|          | instance of |          | instance of |          |
| instance +------------>+  class   +------------>+ metaclass|
|          |             |          |             |          |
|          |             |          |             |          |
+----------+             +----------+             +----------+

P. 参考文献

  1. Python 之旅open in new window

  2. Pyton 作用域与命名空间,官方文档open in new window

Loading...