当前位置: 首页 > 科技观察

Python进阶版:定义类时应用的9个最佳实践

时间:2023-03-17 11:30:33 科技观察

作为一种OOP语言,Python通过支持以对象为中心的各种函数来处理数据和函数。例如,数据结构都是对象,包括整数、字符串等原始类型,在其他一些语言中不认为是对象。再举一个例子,函数是所有简单地定义其他对象(例如类或模块)的属性的对象。尽管可以在不创建任何自定义类的情况下使用内置数据类型并编写一组函数,但是随着项目范围的扩大,代码会变得越来越难以维护。这些单独的代码部分的主题并不相同,尽管很多信息是相关的,但管理它们之间的联系并不简单。在这些情况下,定义您自己的类是值得的,这样您就可以对相关信息进行分组并改进项目的结构。由于您将要处理较少碎片化的代码,因此代码库的长期可维护性将会提高。但是请注意,只有在类声明以正确的方式完成时才能进行操作,并且定义自定义类的好处超过管理它们的费用。1.好的命名定义自己的类就像在代码库中添加新成员一样。所以你应该给班级起个好名字。虽然对类名的唯一限制是合法Python变量的规则(例如,不能以数字开头),但有一些方便的方法来命名类。使用易于发音的名词。这在处理团队项目时尤为重要。在小组演示中,您可能不想说这样的话:“在这种情况下,我们创建了Zgnehst类的一个实例。”还有,读起来方便也意味着名字不能太长,用三个以上的词来定义类名简直难以想象。一个字最好,两个字次之,三个字不能再多了!它反映了存储的数据和预期的功能。就像在现实生活中一样——当我们看到一个男性名字时,我们会假设这个孩子是男孩。这同样适用于类名(或一般的任何其他变量),命名约定很简单-不要觉得奇怪。如果你处理的是学生信息,那么班级就应该命名为Student,KiddosAtCampus不是一个常规的好名字。遵循命名约定。类名应该使用驼峰式大小写,例如GoodName。以下是不常见类名的不完整列表:goodName、Good_Name、good_name和GOodnAme。遵循命名约定以明确意图。当有人阅读你的代码时,毫无疑问,名为GoodName的对象是一个类。也有适用于属性和函数的命名规则和约定,以下部分将简要提及它们的使用位置,但一般原则是相同的。2.显式实例属性在大多数情况下,我们希望定义自己的实例初始化方法(即__init__)。在这个方法中,设置了新创建的类实例的初始状态。但是,Python不限制可以使用自定义类定义实例属性的位置。也就是说,您可以在创建实例后的后续操作中定义其他实例属性。classStudent:def__init__(self,first_name,last_name):self.first_name=first_nameself.last_name=last_namedefverify_registration_status(self):status=self.get_status()self.status_verified=status=="registered"defget_guardian_name(self):self.guardian="Goodman"defget_status(self):#gettheregistrationstatusfromadatabasestatus=query_database(self.first_name,self.last_name)returnstatus(1)初始化方法如上所示,可以通过指定学生的名字和姓氏。稍后,当调用实例方法(即verify_registration_status)时,设置“学生实例”的状态属性。但这并不是一个理想的模式,因为如果各种实例属性分散在整个类中,那么类就无法明确实例对象有哪些数据。因此,最好的做法是将实例的属性放在__init__方法中,这样代码阅读者就可以通过一个地方了解你的类的数据结构,就像这样:classStudent:def__init__(self,first_name,last_name):self.first_name=first_nameself.last_name=last_nameself.status_verified=Noneself.guardian=None(2)更好的初始化方法对于那些实例属性不能初始设置的问题,可以设置一个占位符值(比如None)。虽然无需担心,但此更改还有助于防止在忘记调用某些实例方法来设置适用的实例属性时可能出现的错误,从而导致AttributeError('Student'objecthasnoattribute'status_verified')。在命名约定方面,属性应以小写字母命名,并遵循蛇形命名法——如果使用多个单词,则在它们之间使用下划线。此外,所有名称都应该对它们存储的数据有一个有意义的指示(例如first_name比fn更好)。3.使用属性——但要简短来源:unsplash有些人在学习Python编码的同时具有其他OOP语言(例如Java)的背景,并且习惯于为实例的属性创建getter和setter。可以使用属性装饰器在Python中模仿这种模式。下面的代码显示了使用属性装饰器实现getter和setter的基本形式:"{self.first_name}{self.last_name}"@name.setterdefname(self,name):print("Setterforthename")self.first_name,self.last_name=name.split()(3)属性修饰创建该属性后,即使它是通过内在函数实现的,我们仍然可以使用点符号将它用作常规属性。>>>student=Student("John","Smith")...print("StudentName:",student.name)...student.name="JohnnySmith"...print("Aftersetting:",student.name)...GetterforthenameStudentName:JohnSmithSetterforthenameGetterforthename(4)使用属性使用属性实现的优点包括验证正确的值设置(检查字符串,而不是整数)和只读访问(通过不实现setter方法)。但是应该同时使用属性,如果自定义类看起来像这样,它会非常分散注意力——属性太多了!classStudent:def__init__(self,first_name,last_name):self._first_name=first_nameself._last_name=last_name@propertydefirst_name(self):returnself._first_name@propertydeflast_name(self):returnself._last_name@propertydefname(self):returnf“{self._first_name}{self._last_name}"(5)滥用属性大多数情况下,这些属性可以使用实例属性来代替,因此我们可以直接访问和设置它们。除非特别需要使用上述属性的好处(例如:值验证),否则使用属性优先于在Python中创建属性。4.定义有意义的字符串表示在Python中,函数名前后带有双下划线的函数称为特殊方法或魔术方法,也有人将其称为dunder方法。这些方法对于解释器的基本操作有特殊用途,包括我们之前介绍的__init__方法。__repr__和__str__这两个特殊方法对于创建自定义类的正确字符串表示至关重要,这将为代码读者提供有关该类的更直观的信息。它们之间的主要区别在于__repr__方法定义了一个字符串,您可以使用该字符串通过调用eval(repr("therepr"))重新创建对象,而__str__方法定义了一个更具描述性的字符串并允许进行更多自定义.换句话说,您可以认为在__repr__方法中定义的字符串是供开发人员查看的,而在__str__方法中使用的字符串是供普通用户查看的。考虑以下示例:)"implementationofdef__str__(self):returnf"Student:{self.first_name}{self.last_name}"stringnotation:注意在__repr__方法的实现中,f-strings使用!r来显示带引号的这些字符串,因为它是构造具有格式良好的字符串的实例所必需的。如果没有!r格式,字符串将是Student(John,Smith),这不是构造“Student”实例的正确方法。让我们看看这些实现如何为我们显示字符串:在交互式解释器中访问对象时调用__repr__方法,而在打印对象时默认调用__str__方法。>>>student=Student("David","Johnson")>>>studentStudent('David','Johnson')>>>print(student)Student:DavidJohnson字符串表示5.实例方法、类方法和静态方法在一个类中,我们可以定义三种方法:实例方法、类方法和静态方法。我们需要考虑为我们关心的功能使用哪种方法,这里有一些通用指南。图源:unsplash例如,如果方法与单个实例对象相关,则需要访问或更新该实例的特定属性。在这种情况下,应该使用实例方法。这些方法具有以下签名:defdo_something(self):,其中self参数指的是调用该方法的实例对象。如果方法与单个实例对象无关,则应考虑使用类方法或静态方法。这两种方法都可以使用适用的修饰符轻松定义:classmethod和staticmethod。两者之间的区别在于类方法允许您访问或更新与类关联的属性,而静态方法独立于任何实例或类本身。类方法的一个常见示例是提供方便的实例化方法,而静态方法可以只是一个实用函数。请看下面的代码示例:classmethoddeffrom_dict(cls,name_info):first_name=name_info['first_name']last_name=name_info['last_name']returncls(first_name,last_name)@staticmethoddefshow_duties():return"Study,Play,Sleep"也可以使用不同的方法类似的方式创建类属性。与前面讨论的实例属性不同,类属性是所有实例对象共享的,它们应该反映一些独立于每个实例对象的特性。6.使用私有属性进行封装当为你的项目编写自定义类时,你需要考虑封装,尤其是当你希望其他人也使用你的类时。当类的功能增长时,一些函数或属性只与类内的数据处理有关。换句话说,这些函数都不会在类之外被调用,除了你以外的类的其他用户甚至不会关心这些函数的实现细节。在这些情况下,应考虑封装。按照惯例,应用封装的一种重要方式是给属性和函数添加一两个下划线。两者之间有一个微妙的区别:带有下划线的被认为是受保护的,而带有两个下划线的被认为是私有的,这涉及到创建后的名称操作。本质上,这样命名属性和函数是在告诉IDE(即集成开发环境,如PyCharm),虽然Python中没有真正的私有属性,但它们不会在类外被访问。classStudent:def__init__(self,first_name,last_name):self.first_name=first_nameself.last_name=last_namedefbegin_study(self):print(f"{self.first_name}{self.last_name}beginsstudying.")@classmethoddefrom_dict(cls,name_info):first_name=name_info['first_name']last_name=name_info['last_name']returncls(first_name,last_name)@staticmethoddefshow_duties():return"Study,Play,Sleep"封装上面的代码展示了一个简单的封装例子。如果想知道学生的评估GPA,那么我们可以使用get_mean_gpa方法来获取GPA。用户不需要知道平均GPA是如何计算的,我们可以通过在函数名前加下划线来保护相关方法。此最佳实践的主要收获是仅公开与使用您的代码的用户相关的最少数量的公共API。对于那些只在内部使用的代码,使其成为受保护或私有的方法。资料来源:unsplash7。关注点分离和解耦随着项目的增长,您会发现自己要处理更多的数据,如果您坚持只使用一个类,这会变得很麻烦。继续“学生”类的例子,假设学生在学校吃午饭,每个人都有一个用餐账户可以用来支付餐费。理论上,我们可以在Student类中处理与帐户相关的数据和功能,如下所示:=get_account_number(self.student_id)balance=get_balance(account_number)returnbalancedefload_money(self,amount):account_number=get_account_number(self.student_id)balance=get_balance(account_number)balance+=amountupdate_balance(account_number)混合函数上面的代码显示了一些伪代码用于检查帐户余额和向帐户中添加资金,这两者都在Student类中实现。与此帐户相关的操作还有很多,例如冻结丢失的卡、合并帐户——执行所有这些操作会使“学生”类越来越大,使其越来越难以维护。您应该分离这些职责,让学生类不负责这些与帐户相关的功能,这种设计模式称为解耦。classStudent:def__init__(self,first_name,last_name,student_id):self.first_name=first_nameself.last_name=last_nameself.student_id=student_idself.account=Account(self.student_id)defcheck_account_balance(self):returnsself.account.get_balance_money(),金额):self.account.load_money(amount)classAccount:def__init__(self,student_id):self.student_id=student_id#getadditionalinformationfromthedatabaseself.balance=400defget_balance(self):#Theoretically,student.account.balancesqueuewillwork,butjustincase#weneedstepouchasdatabase#againtomakesurethedataisuptodatereturnsself.balancedefload_money(self,amount):#getthebalancefromthedatabaseself.balance+=amountself.save_to_database()关注点分离上面的代码展示了我们如何使用额外的Account类设计数据结构。如您所见,我们将所有与帐户相关的操作都移到了帐户类中。为了实现检索学生帐户信息的功能,Student类将通过从Account类检索信息来处理该功能。如果想实现更多与该类相关的功能,只需要简单更新Account类即可。设计模式的要点是您希望各个类具有不同的关注点。通过分离这些职责,您的类将更小,处理更小的代码组件将使将来的更改更容易。8.考虑使用__slots__进行优化如果你的类主要是作为存储数据的数据容器,可以考虑使用__slots__来优化类的性能。既可以提高属性访问速度,又可以节省内存。如果您需要创建数千个或更多的实例对象,这就是它发挥重要作用的地方。原因是,对于常规类,实例属性是通过内部管理的字典存储的。相比之下,通过使用__slots__,实例属性使用在幕后用C语言实现的数组相关数据结构进行存储,并优化了它们的性能以提高效率。classStudentRegular:def__init__(self,first_name,last_name):self.first_name=first_nameself.last_name=last_nameclassStudentSlot:__slots__=['first_name','last_name']def__init__(self,first_name,last_name):self.first_name=first_nameself.last使用类定义中的__slots__上面的代码显示了如何在类中实现__slots__的简单示例。具体来说,将所有属性列为一个序列,这会在数据存储中创建一对一匹配,以加快访问速度并减少内存消耗。如前所述,常规类使用字典进行属性访问,而不是实现__slots__的字典。以下代码证实了这一点:>>>student_r=StudentRegular('John','Smith')>>>student_r.__dict__{'first_name':'John','last_name':'Smith'}>>>student_s=StudentSlot('John','Smith')>>>student_s.__dict__Traceback(mostrecentcalllast):File"",line1,inAttributeError:'StudentSlot'对象在类中没有属性'__dict__'with__slots____dict__A关于使用__slots__的详细讨论可以在StackOverflow上找到,您还可以从官方文档(https://docs.python.org/3/reference/datamodel.html)中找到更多信息。请注意,使用__slots__有一个副作用——它会阻止您动态创建其他属性。有些人建议将其作为一种机制来控制类具有哪些属性,但这并不是它设计的目的。9.文档最后我们必须讨论类文档。我们需要明白,写文档并不能代替任何代码,写大量的文档并不能提高代码的性能,也不一定能使代码更具可读性。如果您必须依赖文档字符串来阐明您的代码,那么您的代码很可能有问题。下面的代码将向您展示程序员可能犯的一个错误——使用不必要的注释来补偿错误的代码(即,在这种情况下,是无意义的变量名)。相比之下,一些好名字的好代码甚至不需要注释。#howmanybillablehoursa=6#thehourlyrateb=100#totalchargec=a*b#Theabovevs.thebelowwithnocommentsbillable_hours=6hourly_rate=100total_charge=billable_hours*hourly_rate失败解释案例我并不是说反对写评论和文档字符串,这真的取决于你自己的实例。如果您的代码被多人使用或多次使用(例如,您是唯一一个多次访问同一代码的人),那么请考虑写一些好的评论。这些注释可以帮助您或队友阅读您的代码,但他们都不能假设您的代码完全按照注释所说的执行。换句话说,编写好的代码始终是要牢记的第一件事。如果最终用户要使用代码的特定部分,则需要编写文档字符串,因为这些人不熟悉相关的代码库。他们只是想知道如何使用相关的API,文档字符串将构成帮助菜单的基础。因此,作为程序员,您有责任确保就如何使用您的程序提供清晰的说明。本文回顾了定义您自己的类时要考虑的重要因素。您编写的代码越多,您就越能意识到在定义类之前牢记这些原则的重要性。在定义类时坚持遵循这些准则,一个好的设计将在以后节省大量的开发时间。