Python 24 面向对象编程进阶 其他类内置方法

Python 24 面向对象编程进阶 其他类内置方法

旧博客Python系列

__items__系列方法

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

    def __getitem__(self, item):
        print(self.__dict__[item])

    def __setitem__(self, key, value):
        self.__dict__[key]=value
    def __delitem__(self, key):
        print('del obj[key]时,我执行')
        self.__dict__.pop(key)
    def __delattr__(self, item):
        print('del obj.key时,我执行')
        self.__dict__.pop(item)

f1=Foo('sb')
f1['age']=18
f1['age1']=19
del f1.age1
del f1['age']
f1['name']='alex'
print(f1.__dict__)

items系列和attr系列,动词对应的行为是一样的.但是截获的方法不同.
从结果可以看到,用对象.属性的方法的时候,都触发attr系列方法,而使用对象[key]的方式操作属性的时候,都触发items系列方法.不过__setitem__的内部不会再因为设置属性字典的方式而导致触发循环.

改变对象的字符串显示

在使用内置属性的时候,比如 l = list('hello'),print(l)的时候显示的是值;但是自定义的类产生的对象如果print只会得到一个对象,这是为什么呢?是因为这些对象使用了特定的方法

str

class Foo:

    def __str__(self):
        return '__str__ is triggered.'

f = Foo()
print(f)  # print(f)---->str(f)---->f.__str__()
# __str__ is triggered.

如果不使用__str__,则系统返回的就是默认的那种对象和内存地址的组合名称.print(对象)就是在调用对象的str(对象),而str(对象)就是去触发对象的__str__方法,所以__str__一定要有个字符串返回值.

repr

# repr实际上是在交互式解释器环境下,直接打对象名,会返回的东西.如果不设置,会返回默认的对象和内存地址的组合名称.
class Foo:

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

    def __repr__(self):
        return '对象的名字是{}'.format(self.name)

f = Foo('test')
print(f)   # print(f) --> str(f) --> f.__str__ --> f.__repr__

结果是触发__repr__,是因为print如果找不到__str__,就会找__repr__方法替代.
注意,str和repr的函数必须写return 而且返回值必须是字符串,否则会报错.

format
可以为对象定制一个format方式.

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



    def __format__(self, format_spec):
        format_dic = {
            'ymd': '{0.year}{0.mon}{o.day}',
            'mdy': '{0.mon}-{0.day}-{0.year}',
            'ymd:': '{0.year}-{0.mon}-{o.day}'
        }
        # format_spec就是自定义格式的关键
        return format_dic.get(format_spec,'{0.year}{0.mon}{0.day}').format(self)

d1 = Date(2018,6,1)

x = '{0.year}{0.mon}{0.day}'.format(d1)
print(x)
# 传统情况下需要每次修改字符串内容

print(format(d1,'mdy'))

只需要传入控制参数,即可采取不同的显示格式,可扩展性强而且易于修改.

__slots__

__slots__是一个类数据属性.字典会占用大量内存,正常的类和对象会为类和每一个对象生成一个属性字典保存在内存中,如果只是一个属性很少的类,但是实例很多,为了避免大量重复的属性字典内的内容,可以使用__slots__取代__dict__ 定义__slots__之后,无法给对象添加新的属性,只能使用在__slots__里定义的属性.此外定义了__slots__的类不再支持多继承.一般只有在用作数据结构的类上使用__slots__方法,因为这种类属性不多,但是实例可能有几百万个. __slots__定义之后,对象就没有__dict__属性了,就不用使用哈希表,而是使用了一个数组来存放实例的数据属性和类对象指针,这样大大节省了内存空间,但由于是数组,所以无法任意修改长度,也就无法添加新属性. 另外__slots__经常被当做防止用户给对象增加新属性的手段,但实际上这并非初衷,使得修改和扩展也比较困难.__slots__最好是用来作为一个内存优化工具,避免滥用. __slots__是一个类的数据属性,所以可以是字符串(单个属性名),列表,元组,可迭代对象都可以.

单个属性的情况:

class Foo:
    __slots__ = 'test'  # 字符串被认为是单个值,所以这个类只能有一个属性叫做test

bar = Foo()

setattr(bar,test,1) # 报错,因为没有了__dict__属性,内置函数失效

继续测试其他内容:

print(bar.__slots)  # test
bar.age = 1 # 报错,无法创建新属性

多个属性的情况:

class Foo:
    __slots__ = ['name', 'age']  # 这种含有多个元素的序列,里边每个元素就是可以使用的属性名,所以这相当于定义了两个属性

bar = Foo()

print(bar.__slots__)  # ['name', 'age']

slots__由于取消了对象的属性字典,会导致所有使用属性字典的方法和内置函数失效,这点要注意.由于哈希表的性能和数组在数据量不大的情况下几乎无差异,因此不是特别性能优化和确定不继承的情况下,无须特意使用__slots

小知识:foo 和 bar 是什么

__doc__

这个属性就是类的文档字符串.唯一要注意的是这个属性无法继承.

class Foo:
    '''This is doc_string'''
    pass

class Bar(Foo):
    pass

f = Foo()

print(f.__doc__) # This is doc_string
print(Foo.__doc__) # This is doc_string
print(Bar.__doc__) # None,不继承,类里也没写,就是None

__module__和__class__

__module__ 表示当前操作的对象在哪个模块 __class__ 表示当前操作的对象的类是什么

__del__

这是python的析构函数,就是对象在内存中被释放(删除)的时候,自动触发执行. 注意,程序里本身只有在删除整个实例的时候才会触发析构函数. 如果没有删除实例,最后程序结束执行的时候解释器自动回收的时候,也会触发析构函数
class Foo:
    '''This is doc_string'''

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

    def __del__(self):
        print('Obj is deleted')

f1 = Foo('test')

del f1
print('-'*30)
# 结果是:

Obj is deleted          

如果不删除实例,等着程序运行结束:

class Foo:
    '''This is doc_string'''

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

    def __del__(self):
        print('Obj is deleted')

f1 = Foo('test')

print('-'*30)
# 结果是
------------------------------
Obj is deleted

所以在使用析构函数上,要做的应该是逻辑工作,避免输出结果影响到程序的输出.

__call__

这个表示用对象名后边加括号,来触发__call__方法.
class Foo:
    '''This is doc_string'''

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


f1 = Foo('test')  # 类的call

f1() # 对象的call,报错:'Foo' object is not callable

可见只要对象变成可调用,就可以正常操作了.

class Foo:
    '''This is doc_string'''

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

    def __call__(self, *args, **kwargs):
        print('__call__ is triggered')

f1 = Foo('test')

f1() # __call__ is triggered

这里要注意的是,__call__方法是针对对象来说的,也就是Foo类实例化出f1对象,Foo类里定义的__call__决定了f1对象能否call,而不是Foo类本身.(观察两段程序可以发现,即使没有__call__方法,也不会影响call Foo实例化对象)
那么为什么调用Foo类名(f1 = Foo('test')会得到一个对象呢,只可能Foo的调用方法写在了比Foo更高一级的对象里,Foo类的__call__方法从何而来,答案是元类,就是python里所有的新式类默认继承的元类.

实现类的迭代功能

类的迭代功能的实现,就是要让类符合迭代协议.根据python的迭代协议,需要在对象内部定义一个__next__方法.此外,针对for等内置功能,有必要定义一个__iter__方法来返回一个可迭代对象,如果对象定义了__next__方法,只要返回对象本身即可. 当然,迭代器的惰性求值编程是比较复杂的,目前的知识可以解决一些简单的问题,只要用尾递归的方式将递归与next方法结合起来即可.
# 用__next__方法写一个斐波那契数列的类
class Fib:

    def __init__(self):
        self.a = 1
        self.b = 1
        self.count = 0
    def __iter__(self):
        return self

    def __next__(self):
        if self.count == 100:
            raise StopIteration
        self.a,self.b = self.b,self.a+self.b
        self.count += 1
        return self.b
LICENSED UNDER CC BY-NC-SA 4.0
Comment