Python中的魔法方法是一种特殊的方法,可以让你为类添加“魔法”。它们通常被命名为由两个下划线包围的Python魔术方法,也称为dunder(双下划线)方法。大多数时候,我们将它们用于简单的事情,例如构造函数(__init__)、字符串表示(__str__、__repr__)或算术运算符(__add__/__mul__)。事实上,有很多方法你可能没听说过,但非常有用。在本文中,我们将整理这些神奇的方法!我们都知道迭代器__len__方法的大小,可以在容器类中实现上面的len()函数。但是如果你想获得一个实现迭代器的类对象的长度怎么办?it=iter(range(100))print(it.__length_hint__())#100next(it)print(it.__length_hint__())#99a=[1,2,3,4,5]it=iter(a)print(it.__length_hint__())#5next(it)print(it.__length_hint__())#4a.append(6)print(it.__length_hint__())#5你需要做的就是实现__length_hint__方法,这是迭代器(不是生成器)上的内置方法,如上所示,还支持动态长度变化。然而,顾名思义,这只是一个提示(hint),并不能保证完全准确:对于列表迭代器,可以获得准确的结果,但对于其他迭代器,则不确定。但即使它不准确,它也可以帮助我们获得我们需要的信息,正如PEP424length_hint中所解释的那样,length_hint必须返回一个整数(否则会引发TypeError)或NotImplemented,并且不需要准确。它可能会返回一个大于或小于容器实际大小的值。NotImplemented的返回值表示没有有限长度估计。它可能不会返回负值(否则会引发ValueError)。元编程大多很少你看到的魔术方法都与元编程有关,虽然元编程可能不是我们每天都需要使用的东西,但有一些方便的技巧可以使用它。一个这样的技巧是使用__init_subclass__作为扩展基类功能的快捷方式,而不必处理元类:.default_breed=default_breedclassDog(Pet,default_name="GermanShepherd"):通过上面的代码我们在基类中添加了一个关键字参数,可以在定义子类时设置。当您想要处理提供的参数而不是仅仅分配给属性时,实际用例可能会使用此方法。它看起来很晦涩,很少使用,但你可能已经遇到过很多次,因为它通常在构建API时使用,例如在SQLAlchemy或FlaskViews中。另一个元类魔术方法是__call__。此方法允许自定义调用类实例时发生的情况:classCallableClass:def__call__(self,*args,**kwargs):print("Iwascalled!")instance=CallableClass()instance()#Iwascalled!可用于创建无法调用的类:classNoInstances(type):def__call__(cls,*args,**kwargs):raiseTypeError("Can'tcreateinstanceofthisclass")classSomeClass(metaclass=NoInstances):@staticmethoddeffunc(x):print('Astaticmethod')instance=SomeClass()#TypeError:Can'tcreateinstanceofthisclass对于只有静态方法的类,不需要创建一个类的实例使用了这个方法。另一个类似的场景是单例模式——一个类最多可以有一个实例:**kwargs)def__call__(cls,*args,**kwargs):如果cls.__instance为None:cls.__instance=super().__call__(*args,**kwargs)returncls.__instanceelse:returncls.__instanceclassLogger(metaclass=Singleton):def__init__(self):print("CreatingglobalLoggerinstance")Singleton类有一个私有的__instance-如果没有,如果它已经存在,它将被创建和分配,它只会被回。假设有一个类,你想在不调用__init__的情况下创建它的实例。__new__方法可以帮助解决这个问题:classDocument:def__init__(self,text):self.text=textbare_document=Document.__new__(Document)print(bare_document.text)#AttributeError:'Document'objecthasnoattribute'text'setattr(bare_document,"text","Textofthedocument")在某些情况下我们可能需要绕过创建实例的通常过程,上面的代码演示了如何做到这一点。我们调用Document.__new__(Document)而不是调用Document(...),它创建一个裸实例而不调用__init__。因此,实例的属性(这里是text)并没有被初始化,所以我们需要额外使用setattr函数来赋值(也是一个神奇的方法__setattr__)。你为什么要那样做。因为我们可能要替换构造函数,比如:classDocument:def__init__(self,text):self.text=text@classmethoddeffrom_file(cls,file):#替代构造函数d=cls.__new__(cls)#Dostuff...returnd这里定义了from_file方法,它充当构造函数,首先使用__new__创建实例,然后在不调用__init__的情况下对其进行配置。下一个与元编程相关的神奇方法是__getattr__。当正常的属性访问失败时调用此方法。这可用于将对缺失方法的访问/调用委托给另一个类:returngetattr(self._value,name)s=String("sometext")s.custom_operation()#CallsString.custom_operation()print(s.split())#CallsString.__getattr__("split")和委托tostr.split#['some','text']print("sometext"+"moretext")#...worksprint(s+"moretext")#TypeError:不支持的操作数类型+:'String'和'str'我们想在类中添加一些额外的函数(如上面的custom_operation)来定义字符串的自定义实现。但是我们不想像split、join、capitalize等每一个字符串方法都重新实现。这里我们可以使用__getattr__来调用这些已有的字符串方法。虽然这适用于普通方法,但请注意,在上面的示例中,魔术方法__add__(它提供连接等操作)未被委托。因此,如果我们希望它们也能正常工作,我们必须重新实现它们。内省(introspection)最后一个与元编程相关的方法是__getattribute__。看起来和前面的__getattr__很像,但是它们有一个细微的区别,__getattr__只在属性查找失败时调用,而__getattribute__在尝试属性查找之前调用。因此__getattribute__可用于控制对属性的访问,或者您可以创建一个装饰器来记录每次访问实例属性的尝试:deflogger(cls):original_getattribute=cls.__getattribute__defgetattribute(self,name):print(f“获取:‘{name}’”)returnoriginal_getattribute(self,name)cls.__getattribute__=getattributereturncls@loggerclassSomeClass:def__init__(self,attr):self.attr=attrdeffunc(self):...instance=SomeClass("value")instance.attr#Getting:'attr'instance.func()#Getting:'func'装饰器函数记录器首先记录原始的__getattribute__方法它装饰的类。然后将其替换为自定义方法,该方法记录在调用原始__getattribute__方法之前访问的属性的名称。魔法属性到目前为止我们只讨论了魔法方法,但在Python中也有相当多的魔法变量/属性。其中之一是__all__:#some_module/__init__.py__all__=["func","some_var"]some_var="data"some_other_var="moredata"deffunc():return"hello"#-----------fromsome_moduleimport*print(some_var)#"data"print(func())#"hello"print(some_other_var)#Exception,"some_other_var"没有被模块导出这个属性可以用来定义从属模块导出了哪些变量和函数。我们创建一个Python模块.../some_module/单独的文件(__init__.py)。此文件中定义了2个变量和一个函数,其中仅导出了2个(func和some_var)。如果我们尝试在其他Python程序中导入some_module的内容,我们只会得到2个内容。但是请注意,__all__变量仅影响上面显示的*import,我们仍然可以使用显式名称导入函数和变量,例如importsome_other_varfromsome_module。另一个常见的双下划线变量(模块属性)是__file__。此变量标识访问它的文件的路径:frompathlibimportPathprint(__file__)print(Path(__file__).resolve())#/home/.../directory/examples.py#或旧方法:importosprint(os.path.dirname(os.path.abspath(__file__)))#/home/.../directory/所以我们可以组合__all__和__file__将所有模块加载到一个文件夹中:#目录结构:#.#|____some_dir#|____module_three.py#|____module_two.py#|____module_one.pyfrompathlibimportPath,PurePathmodules=list(Path(__file__).parent.glob("*.py"))打印([PurePath(f).stemforfinmodulesiff.is_file()andnotf.name=="__init__.py"])#['module_one','module_two','module_three']lastone我的重要属性是__debug__。它可以用于调试,但更具体地说,它可以用于更好地控制断言:#example.pydeffunc():if__debug__:print("debugginglogs")#Dostuff...func()如果我们使用pythonexample.py正常运行此代码,我们将看到打印出“调试日志”,但如果我们使用python-Oexample.py,优化标志(-O)会将__debug__设置为false并删除调试消息。因此,如果您在生产中使用-O运行代码,则不必担心在调试期间忘记打印调用,因为它们都不会出现。创建自己的魔术方法?我们可以创建自己的方法和属性吗?是的,你可以,但你不应该。双下划线名称保留用于Python语言的未来扩展,不应在您自己的代码中使用。如果您决定在您的代码中使用这样的名称,那么如果将来将它们添加到Python解释器中,它将与您的代码不兼容。所以对于这些方法,我们只需要记住并使用即可。https://avoid.overfit.cn/post/6a5057b4833b4f188d8c850385cfcbca作者:MartinHeinz
