Python:自定义类或模块时的注意事项

 

 

1.好的命名

定义自己的类,就好比在代码库中添加了一位新成员。因此应该给类起个好名字。虽然类名的唯一限制是合法Python变量的规则(例如,不能以数字开头),但是有一些好用的方法来命名类。

  • 使用易于发音的名词。在参与团队项目时,这一点尤其重要。在小组演讲中,你恐怕不愿意这样讲:“在这种情况下,我们创建Zgnehst类的实例。”
    另外,易于发音也意味着名称不应太长,使用三个以上的单词来定义类名简直无法想象。一个字是最佳,两个字其次,三个字不能再多啦!

  • 反映其存储的数据和预期功能。就像在现实生活中一样——当看到男性化的名字时,我们就会默认这个孩子是男孩。同样的方式也适用于类名(或通常的任何其他变量),命名规则很简单——不要让人感觉奇怪。如果要处理学生的信息,那么该课程应该命名为Student,KiddosAtCampus并不是一个常规的好名字。

  • 遵循命名约定。应该对类名使用骆驼拼写法,例如GoodName。以下是非常规类名称的不完整列表:goodName,Good_Name,good_name以及GOodnAme。遵循命名约定是为了使意图表现明确。在别人阅读你的代码时,可以毫无疑问地假定命名为GoodName的对象是一个类。

也有适用于属性和功能的命名规则和约定,以下各节将在使用情况下简要提及,但是总体原理是相同的。

2.显式实例属性

在大多数情况下,我们都想定义自己的实例初始化方法(即__init__)。在此种方法中,设置了新创建的类实例的初始状态。但是,Python并没有限制可以在何处使用自定义类定义实例属性。换句话说,你可以在创建实例之后的后续操作中定义其他实例属性。

class Student:

	def __init__(self, first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name
	
	def verify_registration_status(self):
		status = self.get_status()
		self.status_verified = status =="registered"
	
	def get_guardian_name(self):
		self.guardian ="Goodman"
	
	def get_status(self):
		# get the registration status from a database
		status =query_database(self.first_name, self.last_name)
	
	return status
 

初始化方法

如上所示,可以通过指定学生的名字和姓氏来创建“学生”类的实例。稍后,在调用实例方法(即verify_registration_status)时,将设置“学生实例”的status属性。

但这不是理想的模式,因为如果在整个类中散布了各种实例属性,那么该类就无法明确实例对象拥有哪些数据。因此,最佳做法是将实例的属性放在__init__方法中,这样代码阅读器就可以通过单一位置来了解你的类的数据结构,如下所示:

class Student:

	def __init__(self, first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name
		self.status_verified =None
		self.guardian =None
 

更好的初始化方法

对于最初无法设置的那些实例属性的问题,可以使用占位符值(例如None)进行设置。尽管没什么好担心的,但是当忘记调用某些实例方法来设置适用的实例属性时,此更改还有助于防止可能的错误,从而导致AttributeError(‘Student’ object has noattribute ‘status_verified’)。

在命名规则方面,应使用小写字母命名属性,并遵循蛇形命名法——如果使用多个单词,请在它们之间使用下划线连接。此外,所有名称都应对其存储的数据有具有意义的指示(例如first_name比fn更好)。

3.使用属性——但要精简

有些人在具备其他OOP语言(例如Java)背景的情况下学习Python编码,并且习惯于为实例的属性创建getter和setter。可以通过在Python中使用属性装饰器来模仿这一模式。以下代码展示了使用属性装饰器实现getter和setter的基本形式:

class Student:

	def __init__(self, first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name
	
	@property
	def name(self):
		print("Getter for the name")
		returnf"{self.first_name}{self.last_name}"
	
	@name.setter
	def name(self, name):
		print("Setter for the name")
		self.first_name, self.last_name = name.split()
 

属性装饰

创建此属性后,尽管它是通过内部函数实现的,我们仍然可以使用点符号将其用作常规属性。

student =Student("John", "Smith")
print("StudentName:", student.name)
student.name ="JohnnySmith"
print("Aftersetting:", student.name)
 
Getterfor the name
StudentName: JohnSmith
Setterfor the name
Getterfor the name
After setting: JohnnySmith
 

使用属性

使用属性实现的优点包括验证正确的值设置(检查是否使用字符串,而不是使用整数)和只读访问权限(通过不实现setter方法)。但应该同时使用属性,如果自定义类如下所示,可能会很让人分心——属性太多了!

class Student:

	def __init__(self, first_name, last_name):
		self._first_name = first_name
		self._last_name = last_name
	
	@property
	def first_name(self):
		return self._first_name
	
	@property
	def last_name(self):
		return self._last_name
	
	@property
	def name(self):
		returnf"{self._first_name}{self._last_name}"
 

滥用属性

在大多数情况下,这些属性可以用实例属性代替,因此我们可以访问它们并直接设置它们。除非对使用上述属性的好处有特定的需求(例如:值验证),否则使用属性优先于在Python中创建属性。

4.定义有意义的字符串表示法

在Python中,名称前后带有双下划线的函数称为特殊方法或魔术方法,有些人将其称为dunder方法。这些方法对解释器的基本操作有特殊的用法,包括我们先前介绍的__init__方法。__repr__和__str__这两种特殊方法对于创建自定义类的正确字符串表示法至关重要,这将为代码阅读器提供有关类的更直观信息。

它们之间的主要区别在于__repr__方法定义了字符串,你可以使用该字符串通过调用eval(repr(“therepr”))重新创建对象,而__str__方法定义的字符串则更具描述性,并允许更多定制。换句话说,你可以认为__repr__方法中定义的字符串由开发人员查看,而__str__方法中使用的字符串由常规用户查看。请看以下示例:

class Student:

	def __init__(self, first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name
	
	def __repr__(self):
		returnf"Student({self.first_name!r}, {self.last_name!r})"
	
	def __str__(self):
		returnf"Student: {self.first_name}{self.last_name}"
 

字符串表示法的实现

请注意,在__repr__方法的实现中,f字符串使用!r来显示带引号的这些字符串,因为使用格式正确的字符串构造实例很有必要。如果不使用!r格式,则字符串将为Student(John, Smith),这不是构造“学生”实例的正确方法。

来看看这些实现如何为我们显示字符串:在交互式解释器中访问对象时会调用__repr__方法,而在打印对象时默认会调用__str__方法。

>>> student =Student("David", "Johnson")

>>> student

Student('David', 'Johnson')

>>>print(student)

Student: DavidJohnson
 

字符串表示法

5.实例方法,类方法和静态方法

在一个类中,我们可以定义三种方法:实例方法、类方法和静态方法。我们需要考虑针对所关注的功能应使用哪些方法,以下是一些常规准则。

  • 例如,如果方法与单个实例对象有关,那么需要访问或更新实例的特定属性。在这种情况下,应使用实例方法。这些方法具有如下签名:def do_something(self):,其中self自变量引用调用该方法的实例对象。

  • 如果方法与单个实例对象无关,则应考虑使用类方法或静态方法。可以使用适用的修饰符轻松定义这两种方法:类方法(classmethod)和静态方法(staticmethod)。

  • 两者之间的区别在于,类方法允许你访问或更新与类相关的属性,而静态方法则独立于任何实例或类本身。类方法的一个常见示例是提供一种方便的实例化方法,而静态方法可以只是一个实用函数。请看以下代码示例:

class Student:

	def __init__(self,first_name, last_name):
	
		self.first_name = first_name
		self.last_name = last_name

	def begin_study(self):
	
		print(f"{self.first_name}{self.last_name}beginsstudying.")
	
	@classmethod
	def from_dict(cls,name_info):
		first_name = name_info['first_name']
		last_name = name_info['last_name']
		returncls(first_name,last_name)
	
	@staticmethod
	def show_duties():
		return"Study,Play, Sleep"
 

不同的方法

也可以用类似的方式创建类属性。与前面讨论的实例属性不同,类属性由所有实例对象共享,并且它们应当反映一些独立于各个实例对象的特征。

6.使用私有属性进行封装

在为项目编写自定义类时,需要考虑封装问题,尤其期望其他人也使用你的类的话就更应如此。当类的功能增长时,某些功能或属性仅与类内数据处理相关。换句话说,除了类之外,这些函数都将不会被调用,并且除你之外其他使用类的用户甚至不会在意这些函数的实现细节。在这些情况下,应该考虑封装。

按照惯例,应用封装的一种重要方法是为属性和函数加上下划线或两个下划线。二者之间有着细微的区别:带有下划线的被认为是受保护的,而带有两个下划线的被认为是私有的,这涉及在创建后进行名称处理。

从本质上来说,像这样命名属性和功能,是在告诉IDE(即集成开发环境,例如PyCharm),尽管在Python中不存在真正的私有属性,但它们不会在类之外被访问。

class Student:

	def get_mean_gpa(self):
		grades = self._get_grades()
		gpa_list =Student._converted_gpa_from_grades(grades)
		returnsum(gpa_list) /len(gpa_list)
	
	def _get_grades(self):
		# fetch grades from a database
		grades = [99, 100, 94, 88]
		return grades
	
	@staticmethod
	def _converted_gpa_from_grades(grades):
		# convert the grades to GPA
		gpa_list = [4.0, 4.0, 3.7, 3.4]
		return gpa_list
 

封装

上面的代码展示了一个简单的封装示例。如果想了解学生的评价GPA,那么我们可以使用get_mean_gpa方法获得GPA。用户不需要知道平均GPA的计算方式,我们可以通过在函数名称前添加下划线来保护相关方法。

这一最佳做法的主要收获是,与用户使用你的代码相关的公共API,仅公开最少的数量。对于仅在内部使用的那些代码,请将其设置为受保护的方法或私有方法。

7.分离关注点和解耦

随着项目的发展,你会发现自己正在处理更多数据,如果你只坚持使用一个类会变得很麻烦。继续以“学生”类为例,假设学生在学校吃午餐,并且每个人都有一个餐饮帐户,可以用来支付餐费。从理论上讲,我们可以处理学生类中与帐户相关的数据和功能,如下所示:

class Student:

	def __init__(self, first_name, last_name, student_id):
		self.first_name = first_name
		self.last_name = last_name
		self.student_id = student_id
	
	def check_account_balance(self):
		account_number =get_account_number(self.student_id)
		balance =get_balance(account_number)
		return balance
	
	def load_money(self, amount):
		account_number =get_account_number(self.student_id)
		balance =get_balance(account_number)
		balance += amount
		update_balance(account_number, balance)
 

混合功能

上面的代码向展示了一些有关检查账户余额和向账户充值的伪代码,这两种伪代码都在Student类中实现。还有更多与该帐户相关的操作,例如冻结丢失的卡、合并帐户——实施所有这些操作会使“学生”类越来越大,从而使维护变得越来越困难。你应该分离这些职责并使学生类不负责这些与帐户相关的功能,即一种称为解耦的设计模式。

class Student:

	def __init__(self, first_name, last_name, student_id):
		self.first_name = first_name
		self.last_name = last_name
		self.student_id = student_id
		self.account =Account(self.student_id)
	
	def check_account_balance(self):
		return self.account.get_balance()
		
	def load_money(self, amount):
		self.account.load_money(amount)
	
	
class Account:

	def __init__(self, student_id):
		self.student_id = student_id
		# get additional information from the database
		self.balance =400
	
	def get_balance(self):
		# Theoretically, student.account.balance will work, but just in case
		# we need to have additional steps to check, such as query the database
		# again to make sure the data is up to date
		return self.balance
	
	def load_money(self, amount):
		# get the balance from the database
		self.balance += amount
		self.save_to_database()
 

分离关注点

上面的代码展示了我们如何使用附加的Account类来设计数据结构。如你所见,我们将所有与帐户相关的操作移至Account类。要实现检索学生的帐户信息的功能,学生类将通过从Account类中检索信息来处理。如果想实现更多与该类相关的功能,只需简单地更新Account类即可。

设计模式的主要要点是,希望各个类具有单独的关注点。通过将这些职责分开,你的类将变小,处理较小的代码组件会使将来的更改变得更容易。

8.考虑使用__slots__进行优化

如果你的类主要用于存储数据的数据容器,那么可以考虑使用__slots__来优化类的性能。它不仅可以提高属性访问的速度,还可以节省内存,如果需要创建数千个或更多实例对象,就是它发挥大作用之处啦。

原因是,对于常规类,实例属性是通过内部托管的字典存储的。相比之下,通过使用__slots__,实例属性将使用在幕后使用C语言实现的与数组相关的数据结构存储,并且以更高的效率优化了它们的性能。

class StudentRegular:

	def __init__(self,first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name

class StudentSlot:

	__slots__ = ['first_name', 'last_name']
	
	def __init__(self,first_name, last_name):
		self.first_name = first_name
		self.last_name = last_name
 

在类的定义中使用__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 (most recentcall last):

File"<input>", line 1, in <module>

AttributeError: 'StudentSlot' object has noattribute '__dict__'
 

具有__slots__的类中没有__dict__

有关使用__slots__的详细讨论可以在Stack Overflow找到答案,你也可以从官方文档中找到更多信息(https://docs.python.org/3/reference/datamodel.html)。

需要注意,使用__slots__会有一个副作用——它会阻止你动态创建其他属性。有人建议将其作为一种控制类拥有的属性的机制,但这并不是它的设计初衷。

9.文件

最后我们必须讨论一下类的文档。我们需要明白编写文档并不能替代任何代码,编写大量文档并不能提高代码的性能,也不一定会使代码更具可读性。如果必须依靠文档字符串来澄清代码,那么你的代码很可能有问题。

以下代码将向大家展示一个程序员可能犯的错误——使用不必要的注释来补偿错误的代码(即,在这种情况下,无意义的变量名)。相比之下,一些有好名字的好代码甚至不需要注释。

# how many billable hours
a =6
# the hourly rate
b =100
# total charge
c = a * b
# The above vs.the below with no comments
billable_hours =6
hourly_rate =100
total_charge = billable_hours * hourly_rate
 

失败解释案例

我并不是说反对写评论和文档字符串,这实际上取决于自己的实例。如果你的代码被多个人使用或多次使用(例如,你是唯一一个多次访问同一代码的人),那么就应考虑编写一些好的注释。
这些注释可以帮助你自己或者团队伙伴阅读你的代码,但是他们都不可以假定你的代码完全按照注释中的说明进行。换句话说,编写好的代码始终是需要牢记的头等大事。

如果最终用户要使用代码的特定部分,那么需要编写文档字符串,因为这些人对相关的代码库并不熟悉。他们只想知道如何使用相关的API,而文档字符串将构成帮助菜单的基础。因此,作为程序员,你有责任确保提供有关如何使用程序的明确说明。

本文中回顾了定义自己的类时需要考虑的重要因素。编写的代码越多,你就越会发现在定义类之前牢记这些原则的重要性。定义类时,请不断练习这些准则,好的设计会在以后节省很多开发时间。

 

2024-05-22 11:44:58【出处】:https://blog.csdn.net/weixin_43755719/article/details/122826551

=======================================================================================

Python自定义模块的使用和注意事项

自定义模块

除了使用系统提供的内置模块以外,我们还能自己写一个模块供自己的程序使用。一个.py文件就是一个模块,所以,自定义模块很简单,基本上相当于创建一个.py文件。但是,需要注意的是,如果一个.py文件要作为一个模块被别的代码使用,这个.py文件的名字一定要遵守标识符的命名规则

模块的查找路径

创建一个模块非常简单,安装标识符的命名规则创建一个.py文件就是一个模块。但是问题是,我们需要把创建好的这个.py文件放在哪个位置,在代码中使用 import 语句才能找到这个模块呢?

Python内置sys模块的path属性,列出了程序运行时查找模块的目录,只需要把我们创建好的模块放到这些任意的一个目录里即可。

import sys
print(sys.path)
[
 'C:\\Users\\chris\\Desktop\\Test',
 'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\python37.zip',
 'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\DLLs',
 'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\lib',
 'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37',
 'C:\\Users\\chris\\AppData\\Roaming\\Python\\Python37\\site-packages',
 'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\lib\\site-packages'
]
 

__all__的使用

使用from <模块名> import *导入一个模块里所有的内容时,本质上是去查找这个模块的__all__属性,将__all__属性里声明的所有内容导入。如果这个模块里没有设置__all__属性,此时才会导入这个模块里的所有内容。

模块里的私有成员

模块里以一个下划线_开始的变量和函数,是模块里的私有成员,当模块被导入时,以_开头的变量默认不会被导入。但是它不具有强制性,如果一个代码强行使用以_开头的变量,有时也可以。但是强烈不建议这样使用,因为有可能会出问题。

总结

test1.py:模块里没有__all__属性

a = 'hello'
def fn():
    print('我是test1模块里的fn函数')
 

test2.py:模块里有__all__属性

x = '你好'
y = 'good'
def foo():
    print('我是test2模块里的foo函数')
__all__ = ('x','foo')
 

test3.py:模块里有以_开头的属性

m = '早上好'
_n = '下午好'
def _bar():
    print('我是test3里的bar函数')
 

demo.py

from test1 import *
from test2 import *
from test3 import *
print(a)
fn()
print(x)
# print(y) 会报错,test2的__all__里没有变量 y
foo()
print(m)
# print(_n)  会报错,导入test3时, _n 不会被导入
import test3
print(test3._n)  # 也可以强行使用,但是强烈不建议
 

__name__的使用

在实际开中,当一个开发人员编写完一个模块后,为了让模块能够在项目中达到想要的效果,这个开发人员会自行在.py文件中添加一些测试信息,例如:

test1.py

def add(a,b):
    return a+b
# 这段代码应该只有直接运行这个文件进行测试时才要执行
# 如果别的代码导入本模块,这段代码不应该被执行
ret = add(12,22)
print('测试的结果是',ret)
 

demo.py

import test1.py   # 只要导入了tets1.py,就会立刻执行 test1.py 代码,打印测试内容
 

为了解决这个问题,python在执行一个文件时有个变量__name__。在Python中,当直接运行一个.py文件时,这个.py文件里的__name__值是__main__,据此可以判断一个一个.py文件是被直接执行还是以模块的形式被导入。

def add(a,b):
    return a+b
if __name__ == '__main__':  # 只有直接执行这个`.py`文件时, `__name__`的值才是 `__main__`
    # 以下代码只有直接运行这个文件才会执行,如果是文件被别的代码导入,下面的代码不会执行
    ret = add(12,22)
    print('测试的结果是',ret)
 

注意事项

在自定义模块时,需要注意一点,自定义模块名不要和系统的模块名重名,否则会出现问题!

 

出处:https://developer.aliyun.com/article/1317063

=======================================================================================

posted on 2024-05-22 11:52  jack_Meng  阅读(15)  评论(0编辑  收藏  举报

导航