0x00 is与==
==运算符是比较两个对象的内容是否相等,默认情况是调用对象的__eq__方法进行比较;而is是比较两个对象是否一样,它比较的两个对象的id,即它们的内存地址是否相同。
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> 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是一样的,即它们在底层中只保存一份内存。
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> a = - 5
>>> b = - 5
>>> a = = b
True
>>> a is b
True
>>> a = - 6
>>> b = - 6
>>> a = = b
True
>>> a is b
False
|
对一些短的字符串也是如此,因此并不是所有字符串都会创建新的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
>>> 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
1
2
3
4
5
6
7
8
9
10
|
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代表的是今天的日期信息,还可以看出它的类型信息。更重要的是你可以直接复制这段打印出来的信息,直接构造一个“相同”的对象出来。
例如
1
2
3
|
>>> now = datetime.date( 2019 , 7 , 7 )
>>> now
datetime.date( 2019 , 7 , 7 )
|
0x02 对象复制
对象的复制或说对象拷贝可以分为浅拷贝和深拷贝。
浅拷贝与深拷贝
我们通过代码来说明,就很好理解
如果要拷贝的对象是基本数据类型,那么深拷贝和浅拷贝的区别不是很大。
1
2
3
4
5
6
7
|
>>> a = [ 1 , 2 , 3 ]
>>> b = list (a)
>>> a[ 1 ] = 200
>>> a
[ 1 , 200 , 3 ]
>>> b
[ 1 , 2 , 3 ]
|
修改a中的元素并不会影响到b
但如果要拷贝的对象包含了另一个对象,那么就要考虑深拷贝和浅拷贝的问题了。
1
2
3
4
5
6
|
>>> 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的浅拷贝,为什么呢?
1
2
3
4
5
|
>>> 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模块了,该模块有两个重要的方法deepcopy和copy。不错,就是分别代表深拷贝和浅拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> 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的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# 定义了基类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模块了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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使用起来非常方便
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> 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' >
|
还可以使用它内部的一些工具方法,在实际的编码当中也是非常实用的。
1
2
3
4
5
6
7
8
9
|
# 转化为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中对象的属性类型有实例变量和类变量。
类变量是属于类的,它存储在“类的内存空间”里,并能够被它的各个实例对象共享。而实例变量是属于某个特定实例的,它不在“类的内存空间”中,它是独立于各个实例存在的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
>>> 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,这个是各个实例共享的。
如果对类变量进行,那么其它实例也会同步修改。而对某个实例对修改,并不会影响都类变量。
1
2
3
4
5
6
7
8
|
>>> 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 实例方法、类方法和静态方法
为了更好区分,我们还是来看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
>>> 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对象实例方法的调用,需要传递一个实例对象到实例方法中,以下两种方法的调用是等价的。
1
2
3
4
|
>>> mc.method()
instance method at <__main__.MyClass object at 0x10910ada0 >
>>> MyClass.method(mc)
instance method at <__main__.MyClass object at 0x10910ada0 >
|
类方法的调用和静态方法的调用都是使用ClassName.methodName()的方式。
1
2
3
4
|
>>> MyClass. classmethod ()
classmethod at < class '__main__.MyClass' >
>>> MyClass. staticmethod ()
staticmethod
|
类方法和静态方法有什么区别呢?
- 首先在前面可以看到类方法和静态方法的对象是不一样的,一个是bound method,一个是function。
- 其次类方法可以访问到类对象MyClass,而静态方法不能。
- 最后静态方法其实跟一个普通的function对象一样,只不过它是属于类命名空间的。
0x07 总结一下
本文主要对Python中一些常见的面向对象的相关的一些特性进行了说明。包括对象的比较、输出、拷贝等操作,以及推荐使用namedtuple定义数据类。最后对类变量和实例变量以及类方法、实例方法和静态方法的不同作了分析。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://juejin.im/post/5d21c268e51d45108126d2c9