本文总结一下python oop的features,包含基础oop和高阶oop部分.
OOP 三要素
封装,继承以及多态.python编程语言中这三要素和一般的编译型语言(JAVA)还是有所不同的.
封装(Encapsulation)
封装定义:将数据和方法封装到类中,通过访问控制(如私有属性)隐藏内部实现,暴露必要的属性和方法给外部调用.简单概括控制访问,隐藏实现细节.然而python中的访问控制不像JAVA一样是强制的,它是一种约定.python也有所谓的公有,受保护(命名以单下划线开头)和私有变量(命名以双下划线开头)或方法.公有,受保护的属性或方法可以在类外直接通过对象.属性/方法访问.针对私有属性或者方法,python会将其改名为_类名__属性/方法,防止类外使用对象.属性/方法直接使用,虽然类外可以对象._类名__属性/方法访问,但这种方式一定程度上实现了弱私有机制.
python还提供了@property这样的装饰器来修饰普通方法,让此方法表示类中一种只读的公有属性,
@property修饰的类方法有如下的几点要求:
- 方法除了self参数以外不可有其它方法
- 方法必须是普通(公有)方法,不可以是协程
- 修饰的方法变成属性,无论类中还是类外都直接是当属性访问,不可以加括号当方法调用.
- 方法的含义本质上是"公开的只读属性",方法应该要尽量简单,尽量不要包含复杂耗时的逻辑,
样例:
class Student:
def __init__(self, name, age, gender):
self.name = name
self._age = age
self.__gender = gender
@property
def gender(self):
return self.__gender if self.__gender else 'unknown'
def test():
student = Student('a', 12, 'f')
print(student.name)
print(student._age)
# 报错 'Student' object has no attribute '__gender'
# print(student.__gender)
# 直接访问改名的私有属性,不推荐这么做
print(student._Student__gender)
print(student.gender)
# 报错: 'str' object is not callable. gender已经是属性不能再视作方法进行调用
#print(student.gender())
# 报错 property 'gender' of 'Student' object has no setter
#student.gender = 'dssd'
if __name__ == '__main__':
test()
输出:
a
12
f
f
类属性
前面涉及的属性本质上是实例属性,因为它们是在类构建对象时候初始化.python中类也有属性,且必须在声明的时候初始化,类属性可以认为是所有类实例对象共有属性,所有类实例对象都可以读取类属性,但是不能修改类属性.看似修改类属性的操作其实是当前对象中创建了一个同名的对象属性,对其进行操作,实际并未影响到类的属性.举个例子:
class C1:
# 只声明一个类属性不初始化会报错
# m
cls_var = 'c1'
def __init__(self, name):
self.name = name
# 只声明一个对象属性不初始化会报错
#self.c
def test(self):
# 类私有属性在类内部访问
print(self.__private_var)
print(C1.__private_var)
def test1():
c = C1('c instance')
d = C1('d instance')
# 结果为True
print(id(c.cls_var) == id(d.cls_var))
# 类
C1.cls_var = 'c1 update once'
# 输出 c1 update once
print(c.cls_var)
print(d.cls_var)
# 类实例对象尝试去修改类属性 只影响当前对象,相当于对象中增加了一个同名的属性,后续操作都在这个同名属性,类属性不受影响
c.cls_var = 'obj update'
# 输出 obj update
print(c.cls_var)
# 输出 c1 update once
print(d.cls_var)
C1.cls_var = 'c1 update twice'
# 输入 obj update
print(c.cls_var)
# 输出 c1 update twice
print(d.cls_var)
# 类私有访问
c.test()
if __name__ == '__main__':
test1()
输出:
True
c1 update once
c1 update once
obj update
c1 update once
obj update
c1 update twice
abc
abc
总之,类属性是所有类实例对象访问的公共属性,只有类能修改它,类对象尝试去修改它只是操作同名的对象属性,不会影响类的属性.
类属性也有私有共有说法,但是一般是公有属性使用较多.
继承(Inheritance)
定义:子类继承父类的属性和方法,实现代码复用,并可扩展或重写功能.简单概括代码复用,构建类层次
python的继承和其它面向对象语言(JAVA)不同的地方是它支持多继承.但是这种语法实际应用场景比较少见.多数场景使用的还是单继承.
super用法
python用super()表示父类,子类的构造器和常规方法都可以使用super()访问父类.
class Base:
def __init__(self, name):
self.name = name
def test_print(self):
print(self.name)
class Child(Base):
def __init__(self, name, age):
super().__init__(name)
self.age = age
def test_print(self):
super().test_print()
print(self.age)
MRO
python的继承不能忽略的一个概念则是MRO(Method Resolution Order),表示多继承中方法解析查找顺序.当子类调用一个方法时,python会根据类的MRO从类和父类中查找方法并调用,比如下面菱形继承的场景.
class A:
def run(self):
print('A runs')
class B(A):
def run(self):
print('B runs')
super().run()
class C(A):
def run(self):
print('C runs')
super().run()
class D(B, C):
def run(self):
print('D runs')
super().run()
def test3():
d = D()
print(D.__mro__)
print(D.mro())
print('*'*10)
d.run()
if __name__ == '__main__':
test3()
这个时候打印子类MRO可以查看解析顺序:
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
**********
D runs
B runs
C runs
A runs
查看类的MRO有三种方法:1. Class.__mro__2. Class.mro() 3. help(class)
多态(Polymorphism)
定义:不同类的对象对同一方法调用做出不同的响应,提高代码的灵活性和可扩展性.简单概括同一接口,不同实现.python中多态是"鸭子类型"(如果它走起来像鸭子,叫起来像鸭子那么它就是鸭子)的极致体现.python中的多态本身是不依赖继承,只要对象实现了对应方法即可,强调的是行为而非类型.这个对比其它的编译类编程语言(比如JAVA)依赖继承实现多态方式会更加灵活.比如下面一个简单例子:
class Dog:
def walk(self):
print('dog walk')
def run(self):
print('dog run')
class Duck:
def walk(self):
print('duck walk')
def run(self):
print('duck run')
def run(arg):
arg.walk()
arg.run()
def test_run():
dog = Dog()
duck = Duck()
run(dog)
run(duck)
if __name__ == '__main__':
test_run()
这个例子能体现python的鸭子类型多态.其中run方法只要求输入参数类型有walk和run方法,任何实现了这两个方法的类型对象都能被函数调用,不需要这些类型有任何继承关系.JAVA在这种场景就不同,因为JAVA是强类型语言,这个场景下只能是实现了某一接口的类的对象才可以传递进来进行调用.
metaclass 元类
元类是控制类创建的行为,是python一切皆为对象的极致体现.可以认为它是类的类,面向对象的编程里面实例对象是由类创建,类是由元类创建.
python中type是默认的元类,比如下面的例子,内置类型list的元类:
>>> type(list)
<class 'type'>
>>> instance(list, type)
True
type是默认的元类,它就可以用来创建类,样例如下:
def __init__(self, name):
self.name = name
def display(self):
print(self.name)
# 元类动态创建新的类
Person = type(
'Person',
(object,),
{
'__init__': __init__,
'display': display
}
)
def test():
p = Person('abc')
print(Person.__bases__)
print(p.__dict__)
print(type(p))
p.display()
if __name__ == '__main__':
test()
输出:
(<class 'object'>,)
{'name': 'abc'}
<class '__main__.Person'>
abc
可见这里使用type(name,bases,dict)创建了新的类型,在此之前更多用法是type(obj) is class来判断某个实例对象是否为某个类型.下面详细讲解一下type(name,bases,dict)这种用法.
- name: 新的类名
- bases: 新的类的父类,取值元祖类型可以通过
新类.__bases__获取到此类型的父类信息. - dict:新的类的属性命名空间,类型是一个字典,里面存放新类的
属性和方法,可以通过实例.__dict__获取.
自定义元类则是定义一个类继承type,比如写抽象类使用到的abc.ABCMeta,其本身就是继承type:
class ABCMeta(type):
"""Metaclass for defining Abstract Base Classes (ABCs).
...
比如自定义元类的例子,自定义元类给以此元类构建的新类自动加上test方法.
def test_print(self, x):
print(x)
# 自定义元类
class MyMeta(type):
def __new__(cls, name, bases, namespace, **kwargs):
print('meta runs')
# 添加方法
namespace['test'] = test_print
return super().__new__(cls, name, bases, namespace, **kwargs)
class MyClass(metaclass=MyMeta):
...
def test_meta():
obj = MyClass()
obj.test('123')
输出:
meta runs
123
类中重要的装饰器
@staticmethod vs @classmethod
静态方法:@staticmethod装饰器修饰的方法,没有self参数,只能访问类变量,不可访问对象变量.
类方法.@classmethod装饰器修饰的方法,第一个参数cls是类本身,只能访问类变量,不可访问对象变量.
静态方法本身是可以跟类无关的,但是在OOP规范里面,所有的东西都属于类,所以才有静态方法需要定义在类中.
类方法是跟类紧密相关的,它的第一个参数一定是类本身,这是它跟静态方法最大的区别点.其次由于类方法第一个参数传递类本身,因此类方法完全可以用静态方法替代,只要静态方法第一个参数传递这个类即可.
class C2:
test = 'test val'
def __init__(self, name):
self.name = name
@staticmethod
def test_static(a, *args, **kwargs):
# 只能访问静态变量
print(f'static: {C2.test}')
print(f'static input: {a} {args} {kwargs}')
@classmethod
def test_class(cls, *args, **kwargs):
# 第一个参数是类本身
print(f'cls: {C2.test}')
print(f'{cls}')
print(f'cls input: {args} {kwargs}')
def test2():
obj = C2('abc')
# 对象可以调用静态方法,但是不推荐
obj.test_static(a=1, b=2)
# 类可以调用静态方法
C2.test_static(a=1, b=2)
# 类对象可以调用类方法,但是不推荐
obj.test_class(b=2)
# 类本身可以调用类方法
C2.test_class(b=2)
# 静态方法可以代替类方法
C2.test_static(C2)
两个方法的具体用途分析:
@abstractmethod
抽象方法装饰器,来自于模块abc比前两个装饰器复杂.此装饰器要求修饰的方法所在类元类是abc.ABCMeta或者是继承自元类是abc.ABCMeta的子类(如abc.ABC)这样这个装饰器才生效.装饰器修饰的方法被视作是抽象方法,其所在的类自然是抽象类,本身不能实例化,必须是继承此抽象类的子类且实现了这个抽象方法,子类才能实例化(不实现此抽象方法,子类依然是抽象类,不可以实例化).子类可以正常使用super调用父类方法.举个正常的例子.
class AbsNormal(ABC):
# 抽象方法的访法体应该是...表示待实现
# 方法体里面用pass或者完整的逻辑代码块也不会报错只是...是更规范的语法
@abstractmethod
def test(self):
...
class SubNormal(AbsNormal):
# 如果子类不实现父类抽象方法会被视作抽象类也不能实例化
def test(self):
print('sub test')
def run(self):
print('runs')
def test3():
# 抽象类不可实例化
# normal = AbsNormal()
# normal.test()
sub = SubNormal()
sub.test()
if __name__ == '__main__':
test3()
一旦把父类中继承自(ABC)这一部分去掉,@abstractmethod的约束将不生效.
class AbsNormal:
@abstractmethod
def test(self):
...
class SubNormal(AbsNormal):
def run(self):
print('runs')
def test3():
normal = AbsNormal()
normal.test()
sub = SubNormal()
sub.test()
if __name__ == '__main__':
test3()
整个代码正常运行.可见@abstractmethod一定要结合ABC抽象类或者ABCMeta元类一起使用才会生效.
类中重要的魔术方法和魔术属性
魔术方法
保留给 Python 解释器在特定场景下调用,且开发者不应随意定义以__xxx__形式命名的特殊方法,本身具有特殊意义的方法.魔术方法是python 实现运算符重载,对象行为定制以及协议支持的核心机制,官方文档链接:魔术方法.魔术方法可以分为以下几类:
对象生命周期与构造
此类魔术方法包含__new__(cls),__init__(self),__del__(self).
__new__方法是创建类对象,它本质是一个类方法(不用@classmethod修饰),第一个参数是类本身, 返回对象本身, __init__方法是对象方法,负责初始化对象,其中的self参数就是__new__方法创建的对象,这里负责初始化,返回值是None.__init__方法是在__new__方法之后执行的.而__del__方法是对象被销毁时调用,类似于析构函数.举个完整例子:
class Test:
def __new__(cls, *args, **kwargs):
print('__new__ runs')
return super().__new__(cls)
def __init__(self, name):
print('__init__ runs')
self.name = name
def display(self):
print(self.name)
def __del__(self):
print('__del__ runs')
if __name__ == '__main__':
test = Test(name='123')
test.display()
输出:
__new__ runs
__init__ runs
123
__del__ runs
这里细节是对象创建时__new__是先于__init__运行,且它接收的参数会传递给__init__所以要保证__new__参数和__init__一致,所以一般情况下定义__new__的参数都是可以接收任意参数的.
格式化输出
主要是__str__(self)和__repr__两个魔术方法.两者区别是前者是用户友好字符串,通过str(obj)或者print(obj)或者f'{obj!s}'调用.第二个魔术方法是返回开发者友好的字符串,通过repr(obj)或者f'{obj!r}'打印.举个例子:
class Format:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f'({self.x}, {self.y})'
def __repr__(self):
return f'Format(x={self.x},y={self.y})'
if __name__ == '__main__':
format = Format(x=1, y=2)
print(format)
print(str(format))
print(repr(format))
print(f'{format!r}')
输出:
(1, 2)
(1, 2)
Format(x=1,y=2)
Format(x=1,y=2)
运算符重载
__add__,__eq__等.
class Number:
def __init__(self, val: int):
self.val = val
def __add__(self, val: int):
return Number(self.val + val)
def __eq__(self, val: 'Number'):
if isinstance(val, Number):
return self.val == val.val
else:
return False
def __str__(self):
return f'{self.val}'
if __name__ == '__main__':
number = Number(1)
other = number + 2
print(other)
print(other == 3)
输出
3
False
属性访问控制
__getattr__和__getattribute__.两者区别是,前者是找不到属性时会调用的方法,后者是访问属性时都会调用的方法但是容易导致无限递归,使用此方法需要慎重__setattr__是改变属性值的时候都会调用.举个例子:
class AttrTest():
def __init__(self, name, age=10, *args, **kwargs):
self.name = name
self.age = age
def __getattr__(self, name):
print(f'getattr: {name}')
return self.__dict__.get(name, None)
def __setattr__(self, name, value):
print(f'setattr for attr: {name} and val: {value}')
self.__dict__[name] = value
def test_attr():
attr = AttrTest(name='123')
print(attr.name)
attr.age = 15
print(attr.age)
if __name__ == '__main__':
test_attr()
输出:
setattr for attr: name and val: 123
setattr for attr: age and val: 10
123
setattr for attr: age and val: 15
15
容器协议
关联的魔术方法__getitem__,__setitem__,__contains__,__iter__,__len__等.主要是容器类型需要关注的魔术方法.
__getitem__: 根据key读取对应的value, obj[key[操作需要调用.
__setitem__: 根据key修改元素,obj[key]==value 需要调用.
__contains__: 判断成员是否存在, item in obj 时需要调用.
__iter__:返回一个新的迭代器对象. for item in obj时候需要调用.
__len__;返回内部元素数量.
举个例子:
class MyContainer:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
print('__getitem__ runs')
return self.items[key]
def __setitem__(self, key, val):
print('__setitem__ runs')
self.items[key] = val
def __len__(self):
print('__len__ runs')
return len(self.items)
def __iter__(self):
print('__iter__ runs')
return iter(self.items)
def __contains__(self, key):
print('__contains__ runs')
return key in self.items
def test_4():
c = MyContainer({'a': 1, 'b': 2})
print(len(c))
print('m' in c)
c['a'] = 4
print(c['a'])
for item in c:
print(item)
if __name__ == '__main__':
test_4()
输出:
迭代器协议
__iter__和__next__方法,参考笔者文章python异步编程中前置知识点迭代器部分,此处不再赘述.
上下文管理器协议
__enter__和__exit__方法,参考笔者文章python异步编程中前置知识点上下文管理器部分,此处不再赘述.
__eq__和 is
首先是明白python中==和is的区别.在python中==是比较两个变量取值是否相等,会调用对应类的__eq__方法,而is是判断两个变量是否是同一对象的引用,它们是否指向同一块内存空间,本质上相当于id(a) == id(b)计算两个变量id比较它们是否指向同一块内存空间.举个例子:
def test_2():
l = [1,2,3]
b = [1,2,3]
c = b
print(l == b)
print(l is b)
print(c == b)
print(c is b)
if __name__ == '__main__':
test_2()
输出:
True
False
True
True
而is在编码过程中使用更多的是和单例类型对象(True,False,None)比较,所以规范的代码里面基本都是if obj is None:,if obj is True:这种语法,而不是``if obj == None这种不规范的语法.
自定义类型要实现==比较自然需要重写__eq__方法.由于自定义类型默认都是继承自object,所以默认用object的__eq__方法,举个例子:
class Person:
def __init__(self, gender):
self.gender = gender
class Human(Person):
...
def test_3():
p1 = Human(gender='f')
p2 = Human(gender='f')
print(p1 == p2)
if __name__ == '__main__':
test_3()
输出False.这里其实是比较gender属性值是否相等即可,所以需要重写__eq__方法,修改后代码如下:
class Person:
def __init__(self, gender):
self.gender = gender
class Human(Person):
def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return self.gender == value.gender
def test_3():
p1 = Human(gender='f')
p2 = Human(gender='f')
print(p1 == p2)
if __name__ == '__main__':
test_3()
输出True
__hash__ 和 __eq__探究
此魔术方法作用是生成一个整数,表示对象的hash值.一般用在将对象存入set以及对象作为dict的key的时候或者在对象上使用内置hash函数的时候需要调用此方法.默认情况下都是继承object类型的__hash__方法,特殊定制化场景需要在自定义类型中重写__hash__.
当自定义类型重写了__eq__最好是同时重写__hash__且保证两个对象值相等的时候对应的hash值也相等.在对象只比较值相等的情况下确实只需要重写__eq__即可,但是重写__eq__时,对象的__hash__就会被设置为None.这个时候此对象实例如果需要放入set中或者作为dict的key就会报错,因为这两个过程比较是否相等需要用到对象的hash值,举个例子:
class Person:
def __init__(self, gender):
self.gender = gender
class Human(Person):
def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return self.gender == value.gender
def test_3():
p1 = Human(gender='f')
# 这里不是调用object的__hash__, __hash__直接变为None
#print(p1.__hash__())
#print(hash(p1))
print(p1.__hash__)
# 直接报错,自定义类型不可hash
#s = set([p1])
if __name__ == '__main__':
test_3()
输出None. 所以正常情况下当重写__eq__后强烈要求重写__hash__且值相等对象的__hash__值也必须相等,这里需要保持一致.避免自定义类型在存入集合或者作为字典的key时因为无法hash报错.纠正后的例子如下:
class Person:
def __init__(self, gender):
self.gender = gender
class Human(Person):
def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return self.gender == value.gender
def __hash__(self):
return hash(self.gender)
def __str__(self):
return f'gender: {self.gender}'
def test_3():
p1 = Human(gender='f')
print(p1.__hash__())
print(hash(p1))
print(p1.__hash__)
s = set([p1])
for item in s:
print(item)
if __name__ == '__main__':
test_3()
输出:
-2331081927929139103
-2331081927929139103
<bound method Human.__hash__ of <__main__.Human object at 0x773dbc71dfd0>>
gender: f
总之最佳实践:重写了__eq__后一定要重写__hash__且保证值相等时,两个对象__hash__返回结果也一样相等.除非是完全确定自定义类型不会存入set不会作为dict的key,那么可以不用重写__hash__,这种情况下自定义类型的__hash__将会是None.
__call__
让实例对象变得像函数一样可调用.如下例子:
class MyCall:
def __init__(self, name):
self.name = name
def __call__(self, *args, **kwds):
print('__call__ runs')
return self.name
def test_call():
obj = MyCall('abc')
# callable判断对象是否可调用
print(callable(obj))
print(callable([1,2,3]))
print(callable(lambda x: print(x)))
if __name__ == '__main__':
test_call()
输出:
True
False
True
__call__ runs
abc
重要用途:
- 有状态函数:
__call__让对象变得像函数一样可以调用,同时它也具备面向对象中的有状态的特性,是有状态函数的实现方式之一.
举例如下:
class StateFunc:
def __init__(self):
self.count = 0
def __call__(self, *args, **kwds):
self.count += 1
print(f'call runs {self.count} times')
def test_state():
state_func = StateFunc()
state_func()
state_func()
if __name__ == '__main__':
test_state()
输出:
call runs 1 times
call runs 2 times
- 类装饰器,这是
__call__魔术方法比较重要的使用场景
比如当我们要计算函数的运行时间,我们一般会定义一个函数装饰,如下代码所示:
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
duration = (time.perf_counter() - start)*1000
print(f"{func.__name__} duration is {duration:.3f} ms")
return res
return wrapper
使用:
@timer
def test_duration():
time.sleep(2)
return 'hello'
def main():
test_duration()
if __name__ == '__main__':
main()
输出结果:
test_duration duration is 2000.093 ms
函数装饰器是无状态的,而类装饰器是有状态的.把上面的函数装饰器改为类装饰器还可以统计被修饰的函数调用次数,代码如下:
class Timer:
def __init__(self, func):
self.count = 0
self.func = func
def __call__(self, *args, **kwargs):
self.count += 1
start = time.perf_counter()
res = self.func(*args, **kwargs)
duration = (time.perf_counter() - start)*1000
print(f"{self.func.__name__} duration is {duration:.3f} ms and runs {self.count} times")
return res
使用:
@Timer
def test_duration():
time.sleep(2)
return 'hello'
def main():
test_duration()
test_duration()
if __name__ == '__main__':
main()
输出:
test_duration duration is 2000.059 ms and runs 1 times
test_duration duration is 2000.096 ms and runs 2 times
- 函数工厂或者策略模式
创建可参数化的可调用对象,比如下面的例子:
class Power:
def __init__(self, exponent):
self.exponent = exponent
def __call__(self, base):
return base ** self.exponent
def calculate():
square = Power(2)
cube = Power(3)
print(square(4))
print(cube(3))
if __name__ == '__main__':
calculate()
输出:
16
27
可见这里一个类型的不同可调用对象实现不同的函数功能.
魔术属性
__dict__和__slots__
类似魔术方法,定义也是__xxx__的形式命名的特殊类属性.这里主要是罗列两个属性:__dict__ 和 __slots__它们是 Python 中用于管理对象属性存储的两种机制,它们在 内存使用、性能、灵活性 上有显著区别.
__dict__:对象的属性字典,默认存储对象所有动态属性.
__slots__:类属性信息(元祖/列表).用于限制实例对象属性,让对象无法动态添加属性.如果定义了__slots__则对象不会存在__dict__属性:
class TestDS:
def __init__(self, name):
self.name = name
def test_ds():
obj = TestDS('abc')
# 不存在__slots__属性
obj.age = 10
print(obj.__slots__)
print(obj.__dict__)
if __name__ == '__main__':
test_ds()
输出:
{'name': 'abc', 'age': 10}
如果类中定义了__slots__限制对象的属性,那么对象就无法动态添加属性.如下例子:
class TestDS:
__slots__ = ['name']
def __init__(self, name):
self.name = name
def test_ds():
obj = TestDS('abc')
print(obj.__slots__)
# __slots__定义后 对象中就没有__dict__属性
#print(obj.__dict__)
# __slots__限制对象属性,不可动态添加
#obj.age = 10
if __name__ == '__main__':
test_ds()
__slots相比于__dict__它可以限制对象的属性,因为不需要用一个字典来存储属性,它直接将属性直接存储在固定内存槽中,提升性能,节省内存.当需要严格限制对象属性且需要高性能的场景使用__slots__.同时会也丧失灵活性,不能动态添加属性.如果要支持动态添加属性可以把__dict__添加到__slots__中实现.举个例子:
class C1:
__slots__ = ['name', '__dict__']
def __init__(self, name):
self.name = name
def test_c1():
c = C1('abc')
c.age = 10
print(c.__slots__)
print(c.__dict__)
if __name__ == '__main__':
test_c1()
输出:
['name', '__dict__']
{'age': 10}
这里可以看出对象能添加新的属性,且新的属性会被添加到__dict__中.这种用法本质上是在__slots__中添加部分特殊的属性以恢复部分默认功能.
继承问题:继承时子类必须定义__slots__,父类的__slots__不会继承到子类.举个例子:
class Base:
__slots__ = ['name']
def __init__(self, name):
self.name = name
class Sub(Base):
def __init__(self, name):
super().__init__(name)
def test():
sub = Sub('abc')
print(sub.__slots__)
print(sub.__dict__)
sub.age = 10
print(sub.__dict__)
输出:
['name']
{}
{'age': 10}
可见父类的__slots__不会在子类生效,这里也无法约束子类对象添加新的属性,这个时候需要在子类中定义自己的__slots__,修改后代码:
class Base:
__slots__ = ['name']
def __init__(self, name):
self.name = name
class Sub(Base):
__slots__ = ['name']
def __init__(self, name):
super().__init__(name)
def test():
sub = Sub('abc')
print(sub.__slots__)
#print(sub.__dict__)
#sub.age = 10
if __name__ == '__main__':
test()
__class__
返回对象的类,一般在类方法中进行类型比较,比如重写__eq__方法中比较是否为同一类:
def __eq__(self, val):
if type(val) is self.__class__:
...
isinstance(obj,class) vs type(obj) is class 类型比较
类型比较的两个重要方式,区别在于isinstance比较会考虑继承关系,如果比较的class类型是obj对应的类或者其父类那么都会返回true.而type(obj)(等价于obj.__class__)是返回对象的精确类型,不会考虑集成关系type(obj) is class只要不是class的实例对象都会返回false,举例如下:
class Top:
def __init__(self, name):
self.name = name
class Sub(Top):
...
def test_type():
sub = Sub(name='abc')
print(isinstance(sub, Top))
print(type(sub) is Top)
print(type(sub) is Sub)
if __name__ == '__main__':
test_type()
输出结果:
True
False
True
总之需要考虑继承关系的类型比较用isinstance(obj, class),而需要精确类型比较的场景用type(obj) is class.
694

被折叠的 条评论
为什么被折叠?



