Python世界里的魔术方法(一)

序言

传说中,Python对象天生具有一些神奇的方法,它们具有一些魔力,因此它们总被双下划线所包围着。这些方法统称为魔术方法。在特定的操作下,这些魔术方法会被自动调用,并且表现出许多神奇的现象。

它们是Python面向对象下智慧的结晶。作为Python使用者,了解它们是我们的职责,在某些情况下,我们甚至能改变它们的魔力。

本文主要介绍下这些魔术方法中主要的部分,并且说明它们每个的作用和神奇之处。

构造和析构方法

__new__

魔力:实例的构造方法。一个对象实例化时,调用的第一个方法。

参数:第一个参数是这个类,其他参数则传递给__init__方法

__new__方法决定是否要使用该__init__方法,因为它可以调用其他类的构造方法或者直接返回别的实例对象作为本类的实例。

如果__new__没有返回实例对象,则__init__不会被调用。

class Foo:
    pass

class Bar(Foo):
    def __new__(cls):
        print("__new__方法被执行")
        return super().__new__(cls) # 返回值就是所谓的self

    def __init__(self): # self被传递
        print("__init__方法被执行")
        
# 输出
__new__方法被执行
__init__方法被执行

__new__方法传入类(cls),而__init__传入类的实例化对象(self)。

__new__返回的值是一个实例化对象,如果返回None时则不执行__init__,并且返回值只能调用父类中的__new__方法,而不能调用毫无关系的类的__new__方法。

class CapStr():
    def __new__(*string):
        cls,*args = string
        print(cls,*args) # <class '__main__.CapStr'> I love China!
        self_in_init = super(CapStr,cls).__new__(cls)
        print(id(self_in_init))
        return self_in_init

    def __init__(self, string):
        print(id(self))


a = CapStr("I love China!")
print(id(a))

上例可以看到,一个对象实例化后,它的参数会被当做new方法第二个参数接收,并作为init方法的第二个参数接收。
并且,new方法必须调用super()方法,返回其实例。

创世之初,宇宙一片混
沌。盘古(Python类)开天辟地,使用__new__的魔力,创造了世界(返回了Python实例化对象)。此时新世界诞生,占据无垠宇宙中的一席之地(内存地址)。于是,天地万物从这里开始。

宇宙之外,造物者以这种方式无时无刻在创造着新世界。

__init__

构造方法。

魔力:构造器。在构建实例时,自动执行此方法。如果不定义,则实例化后隐式继承调用。

参数:__new__的返回值self,其他可选参数

注意:__init__()不能有返回值,即它只能返回None。

类实例化后,会获得一个实例对象。

class World:
    def __init__(self, mountain, river, human, light):
        self.mountain = mountain
        self.river = river
        self.human = human
        self.light = light

    def begin(self):
        print('{} is born'.format(self.human))
        
    def create_light(self):
        print('要有{}'.format(self.light))

world = World('mountain','river','human','光')
world.begin()  # 输出  human is born
world.create_light()  # 输出  要有光

实例对象self会绑定方法,即每个方法都需要和此实例self绑定。Python会把方法的调用者,作为第一个参数self的实参传入。上例中,world即self。

世界的模子被创建出来之后,上帝开始在模子上构建世界的初貌。Ta施展了__init__方法的魔力,说:要有光(传递参数)。于是,世界有了光。

注意:

world = World('mountain','river','human','光')
earth = World('mountain','river','human','光')

print(id(world.begin)) # 2140756327624
print(id(earth.begin)) # 2140756327624

尽管创造了不同的实例对象,但由于它们同源,因此它们的方法本质上是一致的。尽管,方法是绑定在各自的实例对象上。

小结:__new____init__相配合,是Python世界中的构造器。

__del__

析构方法。

使用场景:析构方法。销毁类的实例时可调用,以释放占用的资源。其中就放些清理资源的代码,比如释放连接。

注意:此方法不能引起对象的真正销毁,只是对象销毁时会自动调用它。

使用del语句删除实例时,引用计数会减1。当引用计数为0时,会自动调用__del__方法。

由于Python实现了垃圾回收机制,无法确定对象何时执行垃圾回收。

class World:

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

    def __del__(self):
        print('delete {}'.format(self.name))

def delete():
    world = World('earth')
    world.__del__()
    world.__del__()

delete()
# 打印三次delete earth,因为最后一次对象销毁,引用计数为0

由于垃圾回收对象销毁时,才会真正清理对象。因此还会在之前自动调用__del__方法。一般不手动调用。关于Python的垃圾回收机制,在这里暂不作讨论。

类和对象属性

__class__

对象属性。

魔力:存储类名

world = World('mountain', 'river', 'human', '光')
print(world.__class__) # <class '__main__.World'>

类似的有类的魔术方法:

属性含义
__name__类、函数、方法等的名字
__module__类定义所在的模块名
__qualname__对象或类所属的类
print(World.__qualname__) # World
print(World.__name__) # World

__dict__

类属性和对象属性

魔力:存储了属性字典,分为对象属性字典和类属性字典。

print(world.__dict__)  # 实例属性
# 输出 {'mountain': 'mountain', 'river': 'river', 'human': 'human', 'light': '光'}

print(World.__dict__)  # 类属性
# 输出 {'__module__': '__main__', '__init__': <function World.__init__ at 0x000001B81AEE8620>, 'begin': <function World.begin at 0x000001B81AEE86A8>, 'create_light': <function World.create_light at 0x000001B81AEE8730>, '__dict__': <attribute '__dict__' of 'World' objects>, '__weakref__': <attribute '__weakref__' of 'World' objects>, '__doc__': None}

实例字典,只保存实例的属性。实例属性和实例self绑定,为实例独占。

类字典,保存了类的属性,包括了__init__,实例方法,类变量等等。所有实例可共享此类属性,但不包括各个实例属性。

当访问一个实例的属性时,实例属性的查找顺序:

先查找自己的属性字典,如果没有则通过属性__class__找到自己的类,再到类的属性字典中找。

注意:如果实例使用__dict__[变量名]访问变量,则不会按照上述的访问顺序查找变量。这是指明使用字典的key查找,而不是属性查找。

一般而言,类变量使用全大写命名。类变量相当于类的常量,为实例准备的常量。

__bases__

类属性

使用场景:类的基类的元组,顺序为它们在基类的列表中出现的顺序。

class World:

    def __init__(self):
        self.name = 'world'


class Human(World):

    def __init__(self):
        self.name = 'earth';

class Person(Human,World):

    def __init__(self):
        self.name = 'jaywin'

print(Person.__bases__) 
# (<class '__main__.Human'>, <class '__main__.World'>)	

__doc__

类属性。

作用:类、函数的文档字符串。如果没有定义则为None

__mro__

类属性。

作用:Method Resolution Order。类的mroclass.mro()返回的结果保存在__mro__中。它

class World:

    def __init__(self):
        self.name = 'world'


class Human(World):

    def __init__(self):
        self.name = 'earth';

class Person(Human,World):

    def __init__(self):
        self.name = 'jaywin'

print(Person.mro()) 
# [<class '__main__.Person'>, <class '__main__.Human'>, <class '__main__.World'>, <class 'object'>]

关于Python的继承,这里暂不讨论。

收集属性

__dir__

方法。

作用:返回类或者对象的所有成员名称列表。如果需要查看类或对象的属性,则可使用dir()

dir()函数就是调用__dir__

如果提供此方法,则返回属性的列表。否则会尽量从此属性中收集信息。

  • 如果对象是模块对象,返回的列表包含模块的属性名
  • 如果对象是类型或者类对象,返回的列表包含类的属性名,以及它的基类的属性名。
  • 否则,返回列表包含对象的属性名,它的类的属性名和类的基类的属性名。

哈希和等值方法

__hash__

作用:内建函数hash()调用时的返回值,返回一个整数。如果定义此方法,该类的实例就可以可hash。

__eq__

作用:对应==操作符,判断2个对象是否相等,返回bool值。

这两个魔术方法可以一起来理解,看以下例子。

class Foo:
    def __init__(self, name='Tom', age=18):
        self.name = name
        self.age = age

    def __hash__(self):
        return 1

    def __repr__(self):
        return "<Foo name={},age={}>".format(self.name,self.age)


print(hash(Foo('tom'))) # 输出 1

print(Foo('tom'),Foo('tom'))
# 输出: <Foo name=tom,age=18> <Foo name=tom,age=18>

print([Foo('tom'),Foo('tom')])
# 列表: [<Foo name=tom,age=18>, <Foo name=tom,age=18>]

print({Foo('tom'),Foo('tom')})
# 集合: {<Foo name=tom,age=18>, <Foo name=tom,age=18>}
# 集合中竟然有两个相同的元素?

在Python中,对于集合对象里元素的要求,必须是可hash的。也就是说,会对每个元素进行hash()。

在这里,对hash(Foo(‘tom’))的结果是1,因此,最后一个的输出结果中,不应该出现重复的元素。

那这里为什么无法去重了?不是说集合里不允许相同的元素出现吗?

此时,需要了解__eq__魔术方法。

在集合或者字典中(字典中的key也可看做集合的元素,支持集合的与或非等操作。不过它还映射一个值罢了),元素的第一步是hash。如果hash后的结果相同,则第二步是检查是否相等,相等则去重。

第一步的hash中,__hash__方法只返回一个hash值,作为key。

第二步判断两个对象是否相等,则通过__eq__方法。

因此,上例可解释为,元素的hash值相等,hash冲突。但这两个对象并不是同一个对象。

而一般要求是,只要hash相同, 那么它们应该相等。

因此,是相等的步骤上出现了问题:

    def __eq__(self,other):
        return self.age == other.age # other为第二个对象
        
 print({Foo('tom'),Foo('tom')})
 # 输出: {<Foo name=tom,age=18>}

一般地,提供了__hash__方法,是为了作为set或者dict的key,为了去重,还需要提供 eq方法。

即使是不同的实例,如果__eq__的结果返回True,则表示二者相等。

不可hash对象:isinstance(p1, collections.Hashable)一定为False。如此判断对象是否可hash。

list类实例为什么不可hash?

源码中,有一句:__hash__= None。所有类都继承object,而object具有此方法 的。如果希望一个类不可hash,则将此方法设为None即可。

注意:

  • __hash__仅仅return整型,而hash()可接受任意类型的可哈希的值
  • 如果定义了__hash__,就应该定义__eq__

布尔值方法

__bool__

作用:内建函数bool(),或者对象放在逻辑表达式的位置,调用这个函数返回布尔值。没有定义__bool__,就找__len__返回长度,非0为真。

如果__ len__也没有定义,则所有实例都返回真。

class A: pass


print(bool(A())) # True
if A():
    print('Read A')

class B:
    def __bool__(self):
        return False

print(bool(B)) # True
print(bool(B())) # False

class C:
    def __len__(self):
        return 0

print(bool(C())) # False

此时,我们知道了为什么空列表,空元组,空集合,空字典的bool()都为False了。

可视化

__repr__

__str__

__bytes__

方法意义
_repr_内建函数repr()对一个对象获取字符串表达。调用__repr__方法返回字符串表达。如果它没有定义,就直接返回object的定义,即显示内存地址信息。
_str_str()函数,内建函数format()和print()函数调用,需要返回对象的字符串表达。如果没有定义,就去调用_repr_ 方法返回字符串表达。如果__repr__没有定义,则直接返回对象的内存地址信息。
__bytes__bytes()函数调用,返回一个对象的bytes表达,返回bytes对象。
class A:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

    def __repr__(self):
        return 'repr: {},{}'.format(self.name,self.age)

    def __str__(self):
        return 'str: {},{}'.format(self.name,self.age)

    def __bytes__(self):
        import json
        return json.dumps(self.__dict__).encode() # 返回bytes

a = A('tom')
print(a) # 调用__str__
print([a]) # []使用__str__, 但[]内部使用__repr__
print([str(a)]) # []使用__str__,内部也使用__str__
print(bytes(a)) # 输出 b'{"name": "tom", "age": 18}'

运算符重载

operator模块提供以下的特殊方法,可以将类的实例,使用下面的操作符来操作。

运算符特殊方法含义
<,<=,==,>,>=,!=__lt__, __le__,__eq__,__gt__,__ge__,__ne__比较运算符
+,-,*,/,%,//,**,divmod__add__,__sub__,__mul__,__truediv__,__mod__,__floordiv__,__pow__,__divmod__算数运算符,移位运算符,位运算符
+=,-=,*=,/=,%=,//=,**=iadd,isub,imul,iteruediv,imod,ifloordiv,ipow
class A:
    def __init__(self,name,age=18):
        self.name = name
        self.age = age

    def __sub__(self,other): # 减法
        return self.age - other.age

    def __isub__(self,other): # 减等
        return A(self.name, self-other)

tom = A('tom')
jerry = A('jerry', 16)
print(tom - jerry) # 18 - 16 = 2,输出2
print(jerry - tom, jerry.__sub__(tom)) #  输出 -2 -2 
#谁调用,self就是谁

print(id(tom))  # 1710930572792
tom -= jerry  # 返回新对象,也能就地修改实例
print(tom.age,id(tom)) # 2 1710930572904

#  就地修改实例
    def __isub__(self,other):
        self.age = self.age - other.age
        return self

运算符重载应用场景

往往是用面向对象实现的类,需要做大量的运算。而运算符的这种运算是在数学上最常见的表达方式,并且可以重新定义+的表达。

提供运算符重载,比直接提供加法方法要更加适合该领域内使用者的习惯。比如Path类,/变成了路径分隔符,而不是除法。因此这个符号,运算符进行了重载了。

因此,根据业务的要求,可以应用到运算符重载,比如坐标+坐标。

int类,几乎实现了所有操作符,可以作为参考。

@functools.total_ordering 装饰器

__lt__, __le__,__eq__,__gt__,__ge__,__ne__ 是比较大小时必须实现的方法,但是要全部写完太麻烦了。使用此装饰器可大大简化代码。

但是,要求__eq__必须实现,而其他方法实现其一,即可支持全部的比较大小的方法。

from functools import total_ordering

@total_ordering
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self,other):
        return self.age == other.age

    def __gt__(self,other):
        return self.age > other.age

tom = Person('tom',20)
jerry = Person('jerry',16)
print(tom > jerry) # True
print(tom >= jerry) # True

上例大大简化代码,但是一般而言比较实现等于或者小于方法就够,其他可以不实现。因此此装饰器可能会带来性能问题,建议需要什么方法自己创建,少用这个装饰器。

容器相关方法

关于Python的容器类型,可参考这篇文章

https://blog.csdn.net/qchl2015/article/details/105073207

__len__

作用:内建函数len(),返回对象的长度。一个对象必须定义__len__方法才能使用len()函数去获取其长度。

如果把对象当做容器类型看,就如同list或者dict。

bool()函数调用时,如果没有__bool__方法,则看此方法是否存在,存在返回非0为真。

如果是Python内置的类型,比如列表,字符串,字节序列等。那么CPython会抄个近路,__len__实际上会直接返回PyVarObject里的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个属性比调用一个方法要快得多。

迭代和成员关系判断

__iter__

作用: 迭代容器时,调用。返回一个新的迭代器对象。定义了此方法的对象,为可迭代对象。

__contains__

作用:in,not in 成员关系判断运算符,使用此运算符时会调用此方法。没有实现时就调用__iter__方法遍历。

class Foo:
    def __init__(self,value):
        self.data = value

    def __contains__(self, item):
        print('contains: ',end='')
        return item in self.data


f = Foo([1,2,3,4])
print(1 in f) # contains: True
print(10 in f) # contains: False
__getitem__

作用:使用self[key]访问时,调用此方法。

如果类把某个属性定义为序列时,使用此方法。比如说,实现一个类似列表或者字典的类。

key接受整数为索引,或者切片。对于set和dict,key必须hashable。key不存在时引发KeyError异常。

class Bar:
    def __init__(self, my_list ):
        self.list = my_list

    def __getitem__(self, item):
        return self.list[item]

b = Bar([0,1,2,3,4,5,6,7,8,9])
print(b[4]) # 4
print(b[4:]) # 切片 [4, 5, 6, 7, 8, 9]

c = Bar({'name':'tom','age':18,'gender':'female'})
print(c['name']) # tom

总结:

class Bar:
    def __init__(self, my_list ):
        self.list = my_list

    def __contains__(self, item):
        print('call contains')
        return item in self.list

    def __iter__(self):
        print('call iter')
        for i in self.list:
            yield i


    def __getitem__(self, item):
        print('call getitem')
        return self.list[item]


c = Bar({'name':'tom','age':18,'gender':'female'})
print('name' in c)
# call contains
# True

print(c['name'])
# call getitem
# tom

for i in c:
    print(i)
# call iter
# name
# age
# gender
  • 迭代对象时的方法查找优先级:__iter__ > __getitem__

  • 成员关系判断的方法查找优先级: __contains__ > __iter__ > __getitem__

__setitem__

__getitem__ 的访问类似,是设置值的方法。

__missing__

字典或其子类使用__getitem__ ()调用时,key不存在执行此方法。

class A(dict):
    def __missing__(self,key):
        print('Missing key:',key)
        return 0

a = A()
print(a['k'])

# 输出
Missing key: k
0

可调用对象

__call__

作用:如果实现了此方法的对象,则为可调用对象。类中定义一个该方法,实例就可以像函数一样调用

Python中一切皆对象,函数也不例外。函数之所以能够被调用,是因为内部实现了__call__魔术方法。

def foo():
    print(foo.__module__,foo.__name__)


foo() # 输出  __main__ foo
foo.__call__() # 输出 __main__ foo
print(dir(foo)) # 收集属性,包含'__call__'
class Point:
    def __init__(self,x,y):
        self.x = x
        self.y = y

    def __call__(self,*args,**kwargs):
        return '<Point {}:{}>'.format(self.x,self.y)

p = Point(4,5)
print(p()) # <Point 4:5>

class Adder:
    def __call__(self,*args):
        ret = 0
        for x in args:
            ret += x
        self.ret = ret
        return ret

adder = Adder()
print(adder(4,5,6)) # 15
print(adder.ret) # 15
# 

类实例调用和函数调用相比,实例调用的优点:

实例就算定义完成,调用完毕,结果也会记录在属性中。下次可以继续累加。

而函数调用完毕,值就会丢失了。除非形成闭包,由外层函数记录。

因此,根据此特性,斐波那契数列的类的形式为:

class Fib():

    def __init__(self):
        self.items = [0,1,1]

    def __len__(self):
        return len(self.items)

    def __call__(self,num):
        l = len(self.items)
        if num < 0:
            raise IndexError()
        elif num < l:  # 如果第n项小于3,则无需计算,输出初始值
            return self.items[num]

        for i in range(l,num+1):
            x = self.items[i-1] + self.items[i-2]
            self.items.append(x)
            return x

    def __iter__(self):
        return iter(self.items)

    def __getitem__(self, index):
        return self.items[index]


fib = Fib()
print(fib(2))
print(fib.items)
for i,v in enumerate(fib):
    print(i,v)

其他

__slots__

字典为了提升查询效率,必须用空间换时间。

一般而言一个对象,属性多一点,都存储在字典中便于查询,问题不大。但是如果数百个对象,那么字典占据的空间就有点大了。

是否,可以把属性字典__dict__省了?

Python提供了__slots__

class A:
    X = 1
    __slots__ = ('y','z')

    def __init__(self):
        self.y = 5
        self.z = 6

    def show(self):
        print(self.X,self.y)

a = A()
a.show()  # 1 5
print(A.__dict__) # {'__module__': '__main__', 'X': 1, '__slots__': ('y', 'z'), '__init__': <function A.__init__ at 0x0000029C0C3B87B8>, 'show': <function A.show at 0x0000029C0C3B8B70>, 'y': <member 'y' of 'A' objects>, 'z': <member 'z' of 'A' objects>, '__doc__': None}

print(a.__dict__) # 异常 AttributeError: 'A' object has no attribute '__dict__'
print(a.__slots__) # ('y', 'z')
print(a.X) # 1 

__slots__告诉解释器,实例的属性都叫什么。一般而言,为了节约内存,因此使用元组比较合适。

一旦类提供了此属性,就阻止实例产生__dict__来保存实例的属性。

如果为实例动态增加属性,会异常。而且实例定义的属性,必须在__slots__中定义。

如果为类动态增加属性,是可以的。

注意:__slots__不影响子类实例,不会继承下去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值