类与对象


In [1]:
# 多行结果输出支持
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

自定义字符串的格式化

  • 为了自定义字符串的格式化,我们需要在类上面定义 format() 方法

In [2]:
_formats = {
    'ymd' : '{d.year}-{d.month}-{d.day}',
    'mdy' : '{d.month}/{d.day}/{d.year}',
    'dmy' : '{d.day}/{d.month}/{d.year}'
    }

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __format__(self, code):
        if code == '':
            code = 'ymd'
        fmt = _formats[code]
        return fmt.format(d=self)

In [5]:
d = Date(2012, 12, 21)
d
format(d)
format(d, 'mdy')
'The date is {:ymd}'.format(d)
'The date is {:mdy}'.format(d)


Out[5]:
<__main__.Date at 0x7fba0c5e6668>
Out[5]:
'2012-12-21'
Out[5]:
'12/21/2012'
Out[5]:
'The date is 2012-12-21'
Out[5]:
'The date is 12/21/2012'

让对象支持上下文管理协议

  • 为了让一个对象兼容 with 语句,你需要实现 __enter__()__exit__() 方法
  • 编写上下文管理器的主要原理是你的代码会放到 with 语句块中执行。 当出现 with 语句的时候,对象的 __enter__() 方法被触发, 它返回的值(如果有的话)会被赋值给 as 声明的变量。然后,with 语句块里面的代码开始执行。 最后,__exit__() 方法被触发进行清理工作
  • 不管 with 代码块中发生什么,上面的控制流都会执行完,就算代码块中发生了异常也是一样的

在类中封装属性名

  • Python不去依赖语言特性去封装数据,而是通过遵循一定的属性和方法命名规约来达到这个效果。
  • 任何以单下划线_开头的名字都应该是内部实现(私有的)
  • Python并不会真的阻止别人访问内部名称。但是如果你这么做肯定是不好的,可能会导致脆弱的代码
  • 使用下划线开头的约定同样适用于模块名和模块级别函数
  • 使用两个下划线(__)开头的命名表示private,会被重新命名 有时候你定义的一个变量和某个保留关键字冲突,这时候可以使用单下划线作为后缀

In [26]:
class A:
    def __init__(self):
        self._internal = 0 # An internal attribute
        self.public = 1 # A public attribute
    
#     @classmethod
    def public_method(self):
        '''
        A public method
        '''
        print(2)

    def _internal_method(self):
       print(3)

In [27]:
a = A()
a.public
# Python不会阻止对内部属性或者方法的访问,但是并不建议这么做
a._internal
a.public_method()
a._internal_method()


Out[27]:
1
Out[27]:
0
2
3

In [28]:
class B:
    def __init__(self):
        self.__private = 0

    def __private_method(self):
        pass

    def public_method(self):
        pass
        self.__private_method()

使用双下划线开始会导致访问名称变成其他形式。 比如,在前面的类B中,私有属性会被分别重命名为 _B__private_B__private_method 。 这时候你可能会问这样重命名的目的是什么,答案就是继承——这种属性通过继承是无法被覆盖的


In [29]:
class C(B):
    def __init__(self):
        super().__init__()
        self.__private = 1 # Does not override B.__private

    # Does not override B.__private_method()
    def __private_method(self):
        pass

这里,私有名称 __private__private_method 被重命名为 _C__private_C__private_method ,这个跟父类B中的名称是完全不同的


In [31]:
# 为了避免关键字之间的冲突问题
lambda_ = 2.0 # Trailing _ to avoid clash with lambda keyword

创建可管理的属性

  • 给某个实例attribute增加除访问与修改之外的其他处理逻辑,比如类型检查或合法性验证
  • 自定义某个属性的一种简单方法是将它定义为一个property

In [35]:
class Person:
    def __init__(self, first_name):
        self.first_name = first_name

    # Getter function
    @property
    def first_name(self):
        return self._first_name

    # Setter function
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function (optional)
    @first_name.deleter
    def first_name(self):
        raise AttributeError("Can't delete attribute")
  • 上述代码中有三个相关联的方法,这三个方法的名字都必须一样
  • 第一个方法是一个 getter 函数,它使得 first_name 成为一个属性。 其他两个方法给 first_name 属性添加了 setter 和 deleter 函数。 需要强调的是只有在 first_name 属性被创建后, 后面的两个装饰器 @first_name.setter 和 @first_name.deleter 才能被定义
  • property的一个关键特征是它看上去跟普通的attribute没什么两样, 但是访问它的时候会自动触发 getter 、setter 和 deleter 方法

In [36]:
a = Person('autuanliu')
a.first_name


Out[36]:
'autuanliu'

In [40]:
# 设置的时候会执行一个类型检查的方法
a.first_name = 43


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-40-ddbf28aa0b86> in <module>()
      1 # 设置的时候会执行一个类型检查的方法
----> 2 a.first_name = 43

<ipython-input-35-7bf675128d65> in first_name(self, value)
     12     def first_name(self, value):
     13         if not isinstance(value, str):
---> 14             raise TypeError('Expected a string')
     15         self._first_name = value
     16 

TypeError: Expected a string

In [41]:
a.first_name = "autuan"

In [42]:
del a.first_name


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-42-155a1abee7c3> in <module>()
----> 1 del a.first_name

<ipython-input-35-7bf675128d65> in first_name(self)
     18     @first_name.deleter
     19     def first_name(self):
---> 20         raise AttributeError("Can't delete attribute")

AttributeError: Can't delete attribute
  • Properties还是一种定义动态计算attribute的方法。 这种类型的attributes并不会被实际的存储,而是在需要的时候计算出来
  • 可以把 方法 变为 属性,这样在执行构造函数之后,就可以按照访问属性的方式进行访问方法了,而只有在访问的时候才会被计算出来

In [43]:
import math
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

    @property
    def perimeter(self):
        return 2 * math.pi * self.radius
  • 通过使用properties,将所有的访问接口形式统一起来, 对半径、直径、周长和面积的访问都是通过属性访问,就跟访问简单的attribute是一样的。 如果不这样做的话,那么就要在代码中混合使用简单属性访问和方法调用

In [48]:
# 实例化一个圆的对象
xy = Circle(5.2)

In [51]:
# 可以直接通过属性的方式进行访问
xy.area
xy.diameter
xy.perimeter


Out[51]:
84.94866535306801
Out[51]:
10.4
Out[51]:
32.67256359733385

调用父类方法

  • 想在子类中调用父类的某个已经被覆盖的方法,可以使用 super() 函数
  • super() 函数的一个常见用法是在 init() 方法中确保父类被正确的初始化了
  • super() 的另外一个常见用法出现在覆盖Python特殊方法的代码中

In [53]:
class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()

In [54]:
b = B()

In [55]:
b.spam()


B.spam
A.spam

In [60]:
class A:
    def __init__(self):
        self.x = 0

class B(A):
    def __init__(self):
        super().__init__()
        self.y = 1

In [61]:
b = B()
b.x
b.y


Out[61]:
0
Out[61]:
1

使用延迟计算属性

  • 想将一个只读属性定义成一个property,并且只在访问的时候才会计算结果。 但是一旦被访问后,你希望结果值被缓存起来,不用每次都去计算
  • 定义一个延迟属性的一种高效方法是通过使用一个描述器类

简化数据结构的初始化

  • 你写了很多仅仅用作数据结构的类,不想写太多烦人的 __init__() 函数, 可以在一个基类中写一个公用的 __init__() 函数, r然后继承这个基类

定义接口或者抽象基类

  • 你想定义一个接口或抽象类,并且通过执行类型检查来确保子类实现了某些特定的方法, 使用 abc 模块可以很轻松的定义抽象基类
  • 抽象类的一个特点是它不能直接被实例化
  • 抽象类的目的就是让别的类继承它并实现特定的抽象方法
  • @abstractmethod 还能注解静态方法、类方法和 properties 。 你只需保证这个注解紧靠在函数定义前即可

In [65]:
from abc import ABCMeta, abstractmethod

# 抽象类
class IStream(metaclass=ABCMeta):
    @abstractmethod
    def read(self, maxbytes=-1):
        pass

    @abstractmethod
    def write(self, data):
        pass

In [66]:
# 抽象类不能直接被实例化
a = IStream()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-66-9630b89eab0f> in <module>()
      1 # 抽象类不能直接被实例化
----> 2 a = IStream()

TypeError: Can't instantiate abstract class IStream with abstract methods read, write

In [67]:
class ss(IStream):
    def read(self, maxbytes=-1):
        print('read')
    
    def write(self, data):
        print('write')

In [69]:
aa = ss()
aa.read()
aa.write(data=1)


read
write

In [71]:
# @abstractmethod 还能注解静态方法、类方法和 properties 。 你只需保证这个注解紧靠在函数定义前即可
class A(metaclass=ABCMeta):
    @property
    @abstractmethod
    def name(self):
        pass

    @name.setter
    @abstractmethod
    def name(self, value):
        pass

    @classmethod
    @abstractmethod
    def method1(cls):
        pass

    @staticmethod
    @abstractmethod
    def method2():
        pass

In [73]:
# 静态方法实例
import math
class Circle1:
    def __init__(self, radius):
        self.radius = radius

    @staticmethod
    def area(self):
        return math.pi * self.radius ** 2

In [79]:
aaa = Circle1(3)
Circle1.area(aaa)


Out[79]:
28.274333882308138

In [ ]: