1

与其他语言相比,Python中的类提供了很多双下划线开头和结尾__xxx__的方法,这些方法是Python运行的基础,很多功能背后都是通过调用这些内置方法来实现的。如len()函数调用对象的__len__方法;print(obj)函数调用对象的__str__方法,for item in iterable_obj调用对象的__next____iter__方法。

注:由于所有类内置方法都在双下划线开头和结尾,下文描述中,为描述简洁,在无歧义情况下,部分描述中会去掉方法前后双下划线。

__new____init__

new和init这两个方法很容易混淆,平时定义类时,通常使用的都是init方法,很少用到new方法,但他们是有着截然不同的功能的。
new是静态(@staticmethod)方法,用于创建实例对象,方法必须返回一个对象;而init是实例方法,执行实例初始化,在new返回的对象中执行。

__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.
init called after the instance has been created (by __new__()), but before it is returned to the caller.

正常类的实例化过程如下。

class Obj(object):
    def __new__(cls):
        print("__new__ in <Obj>")
        return object.__new__(Obj)
    def __init__(self):
        print("__init__ in <Obj>")
        
obj = Obj()
print(type(obj))

# 执行输出
>> __new__ in <Obj>
>> __init__ in <Obj>
>> <class '__main__.Obj'>

如果我们在类的new方法中,返回成其他类型对下,则最终得到的会是新类型。

class OldObj(object):
    def __new__(cls):
        print("__new__ in <OldObj>")
        return object.__new__(NewObj)
    def __init__(self):
        print("__init__ in <OldObj>")
        
class NewObj(object):
    def __init__(self):
        print("__init__ in <NewObj>")
        
obj = OldObj()
print(type(obj))

# 执行输出
>> __new__ in <OldObj>
>> <class '__main__.NewObj'>
这里有个疑问,为什么new执行之后,既没执行OldObj的init方法,也没执行NewObj的init方法。
Python Doc里的说明是:If __new__() does not return an instance of cls, then the new instance’s __init__() method will not be invoked.

在应用上,可以通过覆写new方法,来实现单例模式。

class Singleton(object):
    instance = None
    def __new__(cls):
        if not cls.instance:
            cls.instance = object.__new__(cls)
        return cls.instance
        
s1 = Singleton()
s2 = Singleton()

print(s1 == s2) # 输出:True

__str____repr__

str和repr都返回一个对象的字符串描述,不过两者的用途不同,str可以理解是给人阅读的,而repr是给程序使用的,官网对repr的描述如下:

repr(obj)
Return the canonical string representation of the object.
For many object types, including most builtins, eval(repr(obj)) == obj.
即:很多情况下,通过repr(obj)输出的数据,可以重建obj对象。

print(obj)str(obj)方法调用对象的str方法;交互式CLI、repr(obj)、gdb调试时查看对象值返回的是repr,不过和多情况下程序员把str和repr设置为一样__str__ == __repr__

__call__

call方法把一个对象变成为可调用对象,即通过obj()直接调用对象时,系统执行的是对象的call方法,实现call方法的对象,callable(obj)返回为True。

class CallObj(object):
    def __call__(self, *args, **kwargs):
        print("__call__")
        
obj = CallObj()
print(callable(obj))
obj() # 调用了对象的__call__方法

应用方面,可以通过call语法糖,简化对象的调用;也可以用户实现call方法的对象替代基于函数的装饰器,简化代码结构。

__iter____next__

在Java等强类型的语言中,对象的功能特性必须通过继承或实现接口来实现,比如可迭代的类,必须继承自Iterator或实现Iterable接口,并实现相关的方法。而对于动态语言的Python来说,它属于 鸭子类型,只要一个类实现了iter和next方法,它就是一个可迭代对象。

鸭子类型(英语:duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念的名字来源于由詹姆斯·惠特科姆·莱利提出的鸭子测试.
“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
class Iter:
    def __init__(self, max):
        self.max = max
        self.current = 0
 def __iter__(self):
        return self
 def __next__(self):
        if self.current < self.max:
            self.current += 1
 return self.current
        else:
            raise StopIteration("out-of-bounds")
            
for i in Iter(10):
    print(i)

__getitem____setitem____delitem__

这三个方法,主要服务于类list、dict类型的数据结构的中括号[]数据操作中,包括下标操作、切片操作、Key值操作等,经过简单测试,发现Python作为一个合格的“鸭子类型”语言,相关方法能接收的参数类型比list、dict提供的更丰富,为展示相关方法可以使用的参数,以下示例只对方法参数进行打印,不实现具体功能逻辑:

class OpItem:
    def __getitem__(self, item):
        print("getitem, args:", item)
    def __setitem__(self, key, value):
        print("setitem, args:", key, value)
    def __delitem__(self, key):
        print("delitem, args:", key)
        
item_test = OpItem()

getitem操作:

# 下标读取操作
item_test[1] # >> getitem, args: 1
# 切片读取操作
item_test[1:10] # >> getitem, args: slice(1, 10, None)
# 带step的切片读取操作
item_test[1:10:2] # >> getitem, args: slice(1, 10, 2)
# 通过key值读取操作
item_test["str_key"] # >> getitem, args: str_key
# 切片为key值,非标操作,方法会忠实的传递参数
item_test["key1":"key2"] # >> getitem, args: slice('key1', 'key2', None)

setitem操作:

# 下标赋值操作
item_test[1] = 99 # >> setitem, args: 1 99
# 切片替换操作
item_test[1:2] = [10, 20] # >> setitem, args: slice(1, 2, None) [10, 20]

delitem操作:

# 下标删除操作
del item_test[5] # >> delitem, args: 5
# 切片删除操作
del item_test[5:10] # >> delitem, args: slice(5, 10, None)

__getattr____setattr____delattr__

这组方法是在框架开发中必备神器。
当访问对象中不存在的属性时,系统会去调用对象的getattr方法,通过对此方法的处理,可以给系统凭空创造出原来不支持的功能。如访问obj.attr1,而obj中不存在attr1时,会触发getattr方法。
setattr在向对象属性设值时触发,如obj.attr1 = 100;delattr在删除属性时触发,如del obj.attr1

比如在一个ORM的模型中,我们需要把数据库字段相关的属性保存在对象内的dict型字段fields中,实现和对象的其他字段隔离,就可以通过以上三个方法来实现。

class User:
    def __init__(self):
        # 保存数据库表映射字段
        self.__dict__['fields'] = dict()
        # 数据库字段列表
        self.__dict__["fields_list"] = ["name", "age", "address"]
    def __setattr__(self, key, value):
        # 实现通过user.name = xxx 赋值 
        if key in self.fields_list:
            self.__dict__["fields"][key] = value
            return
        self.__dict__[key] = value
    def __getattr__(self, key):
        # 实现通过user.name 取值
        if key in self.fields_list:
            return self.__dict__["fields"][key]
        return self.__dict__[key]
        
user = User()
user.name = "zhangsan"
print(user.__dict__['fields']['name']) # >> zhangsan

在实现了getattr方法的类的内部,对对象属性进行赋值时需要特别注意,很容易引发getattr的无限循环。在init中,对象的所有自定义属性都没有初始化,此时如果对属性赋值,会触发调用setattr方法,访问属性则触发getattr。所以在类内部,最好通过self.__dict__对对象属性进行赋值和取值,避免通过点(.)运算符(self.attrself.attr = xxx)引发无限循环。

在getattr方法中,如果要获取的属性不存在,应该抛出什么错误?
答案是AttributeError,因为在系统内置函数hasattrgetattr中(这两个函数不带双下划线),只有捕获到AttributeError,hasattr才会返回False,getattr(obj, attr, default)才能返回默认值(default)值。

__getatrribute__

getatrribute是一个属性访问拦截器,会拦截所有对对象属性访问请求,不管属性是否存在,且具有最优先的访问查询顺序。
对象的属性查找顺序如下:
实例的getattribute → 实例对象字典 → 实例所在类字典 → 实例所在类的父类(MRO顺序)字典 → 实例所在类的getattr → 报错

Called unconditionally to implement attribute accesses for instances of the class. If the class also defines __getattr__(), the latter will not be called unless __getattribute__() either calls it explicitly or raises an AttributeError. This method should return the (computed) attribute value or raise an AttributeError exception. In order to avoid infinite recursion in this method, its implementation should always call the base class method with the same name to access any attributes it needs, for example, object.__getattribute__(self, name).

既然getattribute会拦截所有属性的访问,那如果我们对于不存在的属性,想要调用getattr来处理,该怎么办呢?
答案是raise AttributeError,只要抛出此错误,就会触发getattr方法的调用。

getattr示例中,我们通过直接访问self.__dict__来规避触发getattr方法,但是getattribute在访问对象的已有属性时都会触发,即使是self.__dict__也会引发无限循环,所以getattribute方法中不能使用self.__dict__,解决办法是调用父类的getattribute方法,即在getattribute中通过super().__getattribute__(item)来读取对象的属性。

__getattribute__方法看着很强大,但是由于其敏感性,和容易引起无限循环,所以,正常情况下,并不建议覆写此方法。只有当自己明明白白知道需要通过此方法完成什么效果时,才使用。

__enter____exit__

enter和exit可以让对象通过with关键字来进行使用,提供进入with块前的初始化工作和退出with块后的清理工作,常用于文件和数据库操作中。

class DbConnect:
    def connect(self):
        print("Init and connect to db.")
    def execute(self):
        print("Execute SQL statement.")
    def disconnect(self):
        print("Disconnect from db.")
    def __enter__(self):
        self.connect()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.disconnect()
        
with DbConnect() as conn:
    conn.execute()

# 输出
# >> Init and connect to db.
# >> Execute SQL statement.
# >> Disconnect from db.

参考材料


乘着风
107 声望12 粉丝

五岁时,妈妈告诉我,人生的关键在于快乐。上学后,人们问我长大了要做什么,我写下“快乐”。他们告诉我,我理解错了题目,我告诉他们,他们理解错了人生。——约翰·列侬