Python中面向对象你应该知道的事

关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github:https://github.com/hylinux1024
微信公众号:终身开发者(angrycode)

0x00 is==

==运算符是比较两个对象的内容是否相等,默认情况是调用对象的__eq__方法进行比较;而is是比较两个对象是否一样,它比较的两个对象的id,即它们的内存地址是否相同。

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True
# a和b是否是同一个对象
>>> a is b
False
# a和b的地址其实是不一样的
>>> id(a)
4498717128
>>> id(b)
4446861832

在比较时但也有例外。Python对一些常用的值进行缓存优化,例如在区间[-5,256]的整数,它们在创建时,无论创建多少个对象,它们的id是一样的,即它们在底层中只保存一份内存。

>>> a = -5
>>> b = -5
>>> a == b
True
>>> a is b
True
>>> a = -6
>>> b = -6
>>> a == b
True
>>> a is b
False

对一些短的字符串也是如此,因此并不是所有字符串都会创建新的实例

>>> a='123'
>>> b='123'
>>> a==b
True
>>> a is b
True
>>> id(a)
4446903800
>>> id(b)
4446903800
>>> x = 'long char'
>>> y = 'long char'
>>> x == y
True
>>> x is y
False

0x01 __repr____str__

每个类都应该提供一个__repr__方法。__repr__方法和__str__方法有什么不一样呢?

简单的说,__repr__可以反映一个对象的类型以及包含的内容,而__str__主要是用于打印一个对象的内容。例如看一下Python中的日期类datetime

import datetime
>>> today = datetime.date.today()
>>> today
datetime.date(2019, 7, 7)
>>> print(today)
2019-07-07
>>> str(today)
'2019-07-07'
>>> repr(today)
'datetime.date(2019, 7, 7)'

__str__在字符串连接,打印等操作会用到,而__repr__主要是面向开发者,它能反馈的信息比较多,例如在交互环境下输入today这个变量会打印出datetime.date(2019, 7, 7),不仅可以看出today代表的是今天的日期信息,还可以看出它的类型信息。更重要的是你可以直接复制这段打印出来的信息,直接构造一个“相同”的对象出来。
例如

>>> now = datetime.date(2019, 7, 7)
>>> now
datetime.date(2019, 7, 7)

0x02 对象复制

对象的复制或说对象拷贝可以分为浅拷贝和深拷贝

浅拷贝与深拷贝

我们通过代码来说明,就很好理解

如果要拷贝的对象是基本数据类型,那么深拷贝和浅拷贝的区别不是很大。

>>> a = [1,2,3]
>>> b = list(a)
>>> a[1]=200
>>> a
[1, 200, 3]
>>> b
[1, 2, 3]

修改a中的元素并不会影响到b

但如果要拷贝的对象包含了另一个对象,那么就要考虑深拷贝和浅拷贝的问题了。

>>> a = [[1,2,3],[4,5,6],[7,8,9]]
>>> b = list(a)
>>> a == b
True
>>> a is b
False

这里有一个列表a,里面有三个子列表,即列表里包含的是对象。
我们使用list工厂方法创建了一个a的拷贝b,这个b就是a的浅拷贝,为什么呢?

>>> a[1][2]='x'
>>> a
[[1, 2, 3], [4, 5, 'x'], [7, 8, 9]]
>>> b
[[1, 2, 3], [4, 5, 'x'], [7, 8, 9]]

a[1][2]的元素修改成了x,这时候b列表中也响应了相同的修改。所以这是浅拷贝,因为没有把子对象进行拷贝,只是拷贝了指向子对象的引用

知道浅拷贝,那么深拷贝就很好理解了。执行拷贝之后,拷贝对象和原对象是完全独立的,修改任何一个对象都不会影响到另一个对象

如何深拷贝一个对象

这时候就需要copy模块了,该模块有两个重要的方法deepcopycopy。不错,就是分别代表深拷贝和浅拷贝。

>>> import copy
>>> a = [[1,2,3],[4,5,6],[7,8,9]]
>>> b = copy.deepcopy(a)
>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> b
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> a[1][2]='change'
>>> a
[[1, 2, 3], [4, 5, 'change'], [7, 8, 9]]
>>> b
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

执行深拷贝之后,对a的修改并不会影响到b

0x03 Abstract Base Classes(ABC)

抽象基类的使用

为了说明为什么要使用ABC,我们先看下不使用ABC的情况

# 定义了基类Base
>>> class Base:
    def foo(self):
        raise NotImplemented
    def bar(self):
        raise NotImplemented

# 定义实现类
>>> class Concrete(Base):
    def foo(self):
        print('called foo')
    
    # 实现类并没有实现bar方法
    
>>> c = Concrete()
>>> c.foo()
called foo
>>> c.bar()
Traceback (most recent call last):
  File "<pyshell#391>", line 1, in <module>
    c.bar()
  File "<pyshell#384>", line 5, in bar
    raise NotImplemented
TypeError: exceptions must derive from BaseException

可以看到没有实现基类bar方法的Concrete类,依然可以使用,并没有在第一时间抛出错误。

要解决这个问题,就要用到我们abc模块了。

from abc import ABC
# 定义基类,继承于ABC
>>> class Base(ABC):
    @abstractmethod
    def foo(self):
        pass
    @abstractmethod
    def bar(self):
        pass

    
>>> class Concrete(Base):
    def foo(self):
        print("called foo")
    # 实现类并没有实现bar方法

>>> c = Concrete()
Traceback (most recent call last):
  File "<pyshell#357>", line 1, in <module>
    c = Concrete()
TypeError: Can't instantiate abstract class Concrete with abstract methods bar

可以看到,在使用Concrete构造方法的时候,就立即抛出TypeError了。

0x04 使用namedtuple的好处

关于namedtuple的用法在前面的文章《如何在Python中表示一个对象》 也有提到。
简单的说namedtuple是一个可以命名的tuple,他是对tuple的扩展,它有跟tuple一样不可变的属性。

对于一些数据类的定义,namedtuple使用起来非常方便

>>> from collections import namedtuple
>>> Point = namedtuple('Point','x y z')
>>> p = Point(1,3,5)
>>> p
Point(x=1, y=3, z=5)
>>> p.x
1
>>> p.y = 3.5
AttributeError: can't set attribute
# 可以看出通过namedtuple定义对象,就是一个class类型的
>>> type(p)
<class '__main__.Point'>

还可以使用它内部的一些工具方法,在实际的编码当中也是非常实用的。

# 转化为dict
>>> p._asdict()
OrderedDict([('x', 1), ('y', 3), ('z', 5)])
# 更新或替换某个属性值
>>> p._replace(x=111)
Point(x=111, y=3, z=5)
# 使用_make创建新对象
>>> Point._make([333,666,999])
Point(x=333, y=666, z=999)

0x05 类变量和实例变量

Python中对象的属性类型有实例变量和类变量

类变量是属于类的,它存储在“类的内存空间”里,并能够被它的各个实例对象共享。而实例变量是属于某个特定实例的,它不在“类的内存空间”中,它是独立于各个实例存在的。

>>> class Cat:
    num_legs = 4
    
>>> class Cat:
    num_legs = 4
    def __init__(self,name):
        self.name = name

>>> tom = Cat('tom')
>>> jack = Cat('jack')
>>> tom.name,jack.name
('tom', 'jack')
>>> tom.num_legs,jack.num_legs
(4, 4)
>>> Cat.num_legs
4

这里定义了一个猫类,它有一个实例变量name,还有一个类变量num_legs,这个是各个实例共享的。

如果对类变量进行,那么其它实例也会同步修改。而对某个实例对修改,并不会影响都类变量。

>>> Cat.num_legs = 6
>>> tom.num_legs,jack.num_legs
(6, 6)
>>> tom.num_legs = 2
>>> tom.num_legs,jack.num_legs
(2, 6)
>>> Cat.num_legs
6

tom.num_legs = 2这个语句都作用其实对tom这个实例增加了一个属性,只不过这个属性名称跟类属性的名称是一致的。

0x06 实例方法、类方法和静态方法

为了更好区分,我们还是来看代码

>>> class MyClass:
    def method(self):
        print(f"instance method at {self}" )
    @classmethod
    def classmethod(cls):
        print(f'classmethod at {cls}')
    @staticmethod
    def staticmethod():
        print('staticmethod')

        
>>> mc = MyClass()
>>> mc.method
<bound method MyClass.method of <__main__.MyClass object at 0x10c280b00>>
>>> mc.classmethod
<bound method MyClass.classmethod of <class '__main__.MyClass'>>
>>> mc.staticmethod
<function MyClass.staticmethod at 0x1090d4378>

可以看到在MyClass中分别定义实例方法(method)、类方法(classmethod)和静态方法(staticmethod)

Python中一切都是对象,所以我打印来一下各个方法的__repr__输出。

对于实例方法method是绑定在MyClass的具体实现对象中的,而类方法classmethod是绑定在MyClass中的,而静态方法staticmethod既不绑定在实例里,也不绑定在类中,它就是一个function对象

实例方法的调用,需要传递一个实例对象到实例方法中,以下两种方法的调用是等价的。

>>> mc.method()
instance method at <__main__.MyClass object at 0x10910ada0>
>>> MyClass.method(mc)
instance method at <__main__.MyClass object at 0x10910ada0>

类方法的调用和静态方法的调用都是使用ClassName.methodName()的方式。

>>> MyClass.classmethod()
classmethod at <class '__main__.MyClass'>
>>> MyClass.staticmethod()
staticmethod

类方法和静态方法有什么区别呢?

  • 首先在前面可以看到类方法和静态方法的对象是不一样的,一个是bound method,一个是function
  • 其次类方法可以访问到类对象MyClass,而静态方法不能。
  • 最后静态方法其实跟一个普通的function对象一样,只不过它是属于类命名空间的。

0x07 总结一下

本文主要对Python中一些常见的面向对象的相关的一些特性进行了说明。包括对象的比较、输出、拷贝等操作,以及推荐使用namedtuple定义数据类。最后对类变量和实例变量以及类方法、实例方法和静态方法的不同作了分析。

0x08 学习资料

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

推荐阅读更多精彩内容