一个新手的自学笔记,通过案例来帮助自己加深理解,通篇采用
大白话编写,本篇用到的模块代码存放在码云上。
模块的使用
from PyStyleClass import GeoCoordinates, getHemisphere
print(GeoCoordinates.doc()) # 查看模块介绍
G = GeoCoordinates(160.07, 180.02)
print(G) # 打印对象
print(repr(G)) # 打印对象
G1 = GeoCoordinates(160.07, 180.02)
print(G == G1) # 判断两个对象是否相等
octets = bytes(G) # 将对象转为bytes存储
print(octets)
G2 = GeoCoordinates.formBytes(octets) # 通过bytes重新生成对象
print(G2)
print(getHemisphere(G))
用来保存经纬度的类
东经160.07度, 北纬180.02度
(160.07, 180.02)
True
b'd\n\xd7\xa3p=\x02d@q=\n\xd7\xa3\x80f@'
东经160.07度, 北纬180.02度
东北半球
目录
对象的表示形式
在Python中,提供了两种关于对象表示形式的方法,分别是`__str__`和`__repr__`,接下来将详细介绍关于这两个表达形式的相同点和不同点。 假设我们有这样一个对象,用来创造一个人类。class people:
def __init__(self, age, name):
self.name = name
self.age = age
当我们尝试创建找个对象并直接打印找个对象的时候,会发现打印的内容对我们而言其实并没有太大的作用。
p = people(18, "张三")
p
<__main__.people at 0x24efd446320>
我们自己设计的类,如果没有添加对象表现形式的两种方法时,默认的打印就是这样,可以看到,这样的打印其实并不符合我们的直观感受,因为无法确认究竟是怎样的一个类, 也不知道类中都有哪些数据。假设我们在编写日志的时候,要保存这个数据,会发现没有任何意义。
import logging
logging.warning(p)
WARNING:root:<__main__.people object at 0x0000024EFD446320>
为了方便我们查看和日志的存储,我们可以给我们的类添加一个__str__方法,通过这个方法我们可以打印出我们所需要内容
class people:
def __init__(self, age, name):
self.name = name
self.age = age
def __str__(self) -> str:
"""返回值必须是字符串"""
return f"people({self.name}, {self.age})"
p = people(18, "张三")
print(p)
people(张三, 18)
logging.warning(p)
WARNING:root:people(张三, 18)
此时我们可以看到,我们创建的实例,在打印和日志保存方面更符合我们的直观感受,且打印的内容更有意义,相对于<__main__.people object at 0x0000024EFD446320>。但是此时其实还存在一个问题,假如我们并不是通过print打印,而是在调试过程中,在控制台打印呢?
p
<__main__.people at 0x24efd446dd0>
此时,打印在控制台上的内容实际上没有任何的变化,这也就说明__str__是无法实现控制台的打印输出的,那么__repr__可不可以呢?
class people:
def __init__(self, age, name):
self.name = name
self.age = age
def __repr__(self) -> str:
"""返回值也必须是字符串对象"""
return f"P({self.name}, {self.age})"
p = people(18, "张三")
p
P(张三, 18)
可以看到,当我们实现了__repr__方法后,可以在控制台打印出我们想要的内容了,那么缺少了__str__,我们的print是否还能正常输出呢?
print(p)
P(张三, 18)
可以看到,即便没有__str__方法,我们的对象仍然可以正常的输出,这个原因就在于Python对特殊方法的调用是隐式的,当我们打印某个对象的时候,会先找这个对象的__str__方法,如果找不到,会重新查找__repr__方法,但是如果找不到__repr__方法,是不会去找__str__方法的,不然就容易形成循环套用的情况,所有,如果我们只想实现一种打印方式的话,推荐实现__repr__
classmethod与staticmethod
这个是Python在定义类时常用到的两个装饰器,通过这两个装饰,我们可以直接通过类调用`相关的方法`,而不是必须进行实例化后才能调用。**classmethod**我们通常称为`构造方法`。我们这里还是以`people`类为例。class people:
def __init__(self, name, age):
self.name = name
self.age = age
def AgeAddOne(self):
"""对年龄加一岁"""
self.age += 1
return True
people.AgeAddOne()
当我们没有实例化而直接使用内部函数时,会出现上述的异常,虽然大多数时候,我们也基本上不会说直接通过类去调用里面的方法,而是通过构造相关的对象后再去使用。但是在某些情况下我们确实可能只想用到类中的某些方法,如果没有这种方案,在某些情况也确实会让人感到难受。
classmethod
我们一般称为构造方法,一般会通过找个方法通过类直接定义出新的对象,第一个参数默认使用
cls,代表类本身。
class people:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromStr(cls, String:str):
name, age = String.split(',')
return cls(name, age)
def __repr__(self):
return f"P({self.name}, {self.age})"
s = "李四,20"
P = people.fromStr(s)
P
P(李四, 20)
可以看到,通过classmethod装饰器可以帮助我们实现一些特殊的构造方法,进一步加强了类的茁壮性,了解了这一点,我们接下来将详细说明cls和self的区别,cls指代的是类本身,self指代的是实例本身,二者都是类方法中的第一个参数(也可以用其他参数名代替这两个,但是约定俗称用这两个)。
class people:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromStr(cls, String:str):
print("cls", cls)
name, age = String.split(',')
return cls(name, age)
def PrintSelf(self):
print("self", self)
s = "李四,20"
print("people", people)
P = people.fromStr(s)
P.PrintSelf()
people <class '__main__.people'>
cls <class '__main__.people'>
self <__main__.people object at 0x0000024EFD61F580>
<class '__main__.people'>说明cls指代的就是类本身,由于是类本身,所以cls无法调用cls.name和cls.age,因为二者在类的实例构造方法__init__中实现的绑定,但是可以调用类方法,<__main__.people object at 0x0000024EFE77AEC0>说明self指代的是people类生成的对象,我们可以通过self调用类属性和实例属性(类方法和实例方法)。
class people:
doc = "我是人类" # 类属性
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
@classmethod
def fromStr(cls, String:str): # 类方法
print("cls", cls.doc)
try:
print("cls", cls.name)
except Exception as e:
print(e)
finally:
name, age = String.split(',')
return cls(name, age)
def PrintSelf(self): # 实例方法
print("self", self.doc)
print("self", self.name)
s = "李四,20"
print("people", people)
P = people.fromStr(s)
P.PrintSelf()
people <class '__main__.people'>
cls 我是人类
type object 'people' has no attribute 'name'
self 我是人类
self 李四
staticmethod
staticmethod也可以将实例方法变为类方法(可以直接通过类调用),但是这个方法,默认是不能调用类属性和实例属性的。
class people:
doc = "我是人类" # 类属性
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
@staticmethod
def run(Strint):
print(Strint)
return True
@staticmethod
def AddAge(Obj:people):
Obj.age += 1
def __repr__(self):
return f"P({self.name}, {self.age})"
people.run("hello")
P = people("王五", 13)
print(P)
P.AddAge(P)
print(P)
hello
P(王五, 13)
P(王五, 14)
虽然staticmethod可以通过类直接调用,但是实际功能上还是显得比较鸡肋的,因为我们完全可以通过在外部定义函数来实现相同的效果
class people:
doc = "我是人类" # 类属性
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
def __repr__(self):
return f"P({self.name}, {self.age})"
def AddAge(Obj:people):
Obj.age += 1
P = people("王五", 13)
print(P)
AddAge(P)
print(P)
P(王五, 13)
P(王五, 14)
classmethod与staticmethod
主要区别在于classmethod比staticmethod默认多提供一个类本身的参数。
class people:
doc = "我是人类" # 类属性
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
def __repr__(self):
return f"P({self.name}, {self.age})"
@classmethod
def Fire(*args):
print("classmethod", args)
@staticmethod
def Sec(*args):
print("staticmethod", args)
people.Fire()
people.Fire("hello")
people.Sec()
people.Sec("hello")
classmethod (<class '__main__.people'>,)
classmethod (<class '__main__.people'>, 'hello')
staticmethod ()
staticmethod ('hello',)
对象可哈希
可哈希是指可以对象有一个`固定且唯一的值`,一般来说,我们可以通过`__eq__`和`__hash__`来实现。class people:
def __init__(self, name, age):
self.name = name
self.age = age
p = people("张三", 18)
hash(p)
158643747141
s = set()
s.add(p)
s
{<__main__.people at 0x24efe779450>}
可以看到,对象默认是可哈希的,哈希值用的是对象的内存id,我们可以保存多个不同的实例对象,但是这样也会出现下面这样的问题
P1 = people("李四", 18)
P2 = people("李四", 18)
print(hash(P1) == hash(P2))
S1 = set()
S1.add(P1)
S1.add(P2)
print(S1)
False
{<__main__.people object at 0x0000024EFEAC8BE0>, <__main__.people object at 0x0000024EFEACBC70>}
明明P1和P2保存的是同一份内容,但是如果通过集合去重,却仍旧会保留这两个内容一样,哈希值不同(每次实例化都会重新分配一个内存地址,所以每次创建的新的对象都是不同的)
class people:
def __init__(self, name, age):
self.name = name
self.age = age
def __hash__(self):
return hash((self.name, self.age))
P = people("张三", 18)
P1 = people("张三", 18)
print(hash(P))
print(hash(P1))
print(hash(P) == hash(P1))
-1588918259230332048
-1588918259230332048
True
通过对象的__hash__的实现,我们发现目前可以保障哈希值是根据对象的内容进行生成,而不是用默认的内存id,但是只有__hash__就够了吗?答案是NO
S = set()
S.add(P)
S.add(P1)
print(S)
{<__main__.people object at 0x0000024EFEB3A5F0>, <__main__.people object at 0x0000024EFEB3B490>}
还是有两个元素,也就是说我们还是没办法通过集合进行去重,这是因为两个对象只是哈希值相等,但是两个对象直接判断是否相等呢?
P == P1
False
看起来好像并不相等呢,那么我们该如何实现当两个对象内容一样值,则判断两个对象为同一个对象呢,答案是__eq__,在对对象进行相等性判断的时候,会调用__eq__方法。
class people:
def __init__(self, name, age):
self.name = name
self.age = age
def __hash__(self):
return hash((self.name, self.age))
def __eq__(self, other):
if isinstance(other, people):
return self.name == other.name and self.age == other.age
else:
return False
P = people("张三", 18)
P1 = people("张三", 18)
S = set()
S.add(P)
S.add(P1)
print(S)
print(P == P1)
print(P == [1, 2, 3])
{<__main__.people object at 0x0000024EFECB6E00>}
True
False
目前我们已经可以通过集合进行去重了,同时在__eq__中我们添加了isinstance(other, people)这样的一个判断,这样也是为了预防在判定是将pople对象和其他对象进行判定(千万分之一可能成功)的误差。但是这样就一定安全了吗?答案是No。
P.name = "李四"
S.add(P)
print(S)
{<__main__.people object at 0x0000024EFECB6E00>, <__main__.people object at 0x0000024EFECB6E00>}
之所以出现这样的问题,是因为我们的对象目前仍然是可变的,我们可以通过对象.属性的方式去改变对象,那么,如何让我们的对象不可变呢?请看下一节。
私有属性?
为什么要加一个`?`,因为Python所谓的私有属性,完全就是`防君子不防小人`,class people:
def __init__(self, name, age):
self._name = name
self._age = age
def __repr__(self):
return f"P({self._name}, {self._age})"
p = people("张三", 18)
print(p._name)
print(p)
p._name = "李四"
print(p)
张三
P(张三, 18)
P(李四, 18)
可以看到如果我们只用一个_是没办法私有化变量的,当然,常见的也并不是使用单个_而是使用两个_来实现的变量私有化。
class people:
def __init__(self, name, age):
self.__name = name
self.__age = age
def __repr__(self):
return f"P({self.__name}, {self.__age})"
p = people("张三", 18)
print(p.__name)
print(p)
p.__name = "李四"
print(p)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[1], line 8
6 return f"P({self.__name}, {self.__age})"
7 p = people("张三", 18)
----> 8 print(p.__name)
9 print(p)
10 p.__name = "李四"
AttributeError: 'people' object has no attribute '__name'
看起来,加了双_的变量确实是不能直接访问和修改的,但是这是足够安全的吗?
print(p.__dict__)
print(p._people__name)
p._people__name = "李四"
print(p)
{'_people__name': '张三', '_people__age': 18}
张三
P(李四, 18)
可以看到,当我们在类中加入两个_修饰变量的情况下,python会将这个变量自动添加类的表示,变成_类__变量的形式,知道这一点的我们其实是完全可以修改所谓的私有变量的,所以如前面所说,Python的私有变量是防君子不防小人,正规的程序员如果在程序中看到两个_修饰的变量,应该知道这是不希望在外部被直接更改的变量,但是有些时候我们希望能够在外部对变量做一些操作,有不像做小人,该怎么办呢?
使用property来访问私有变量
class people:
def __init__(self, name, age):
self.__name = name
self.__age = age
@property
def name(self):
return self.__name
@property
def count(self):
return self.__age
def __repr__(self):
return f"P({self.__name}, {self.__age})"
p = people("张三", 18)
print(p.name)
print(p.count)
张三
18
使用setter修改私有变量
class people:
def __init__(self, name, age):
self.__name = name
self.__age = age
@property
def name(self):
return self.__name
@property
def count(self):
return self.__age
@name.setter
def name(self, value):
self.__name = value
@count.setter
def count(self, value):
self.__age = value
def __repr__(self):
return f"P({self.__name}, {self.__age})"
p = people("张三", 18)
print(p.name)
p.name = "李四"
print(p.name)
张三
李四
想要添加setter,需要先设置property(get)方法
使用__slots__节省内存
当我们创建一个对象是,Python会将对象中的变量和相关的值存放到`__dict__`这个默认字典里面,但是众所周知,字典时一种极其消耗内存的数据结构。当我们一个类里面对对象中的属性操作较少,且要存储很多的对象实例,那么我们完全可以通过`__slots__`来降低我们的内存消耗`(但是现在用的很少,因为服务器的内存多比较多,存储这些数据还是绰绰有余的)`,首先,先来看一下正常情况下,我们创建的类实例中变量是怎样存放的。class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
V = Vector(5, 6)
print(V.__dict__)
try:
print(V.__slots__)
except Exception as e:
print(e)
V.__dict__["z"] = 100 # __dict__支持运行时修改
print(V.__dict__)
{'x': 5, 'y': 6}
'Vector' object has no attribute '__slots__'
{'x': 5, 'y': 6, 'z': 100}
我们的类对象中,默然将属性和相关的值存放在__dict__这个字典中,且默认是没有__slots__这个属性的。
class Vector:
__slots__ = ("x", "y") # 需要设置为类属性,且不支持运行时在修改
def __init__(self, x, y):
self.x = x
self.y = y
V = Vector(5, 6)
try:
print(V.__dict__)
except Exception as e:
print(e)
print(V.__slots__)
V.__slots__ = ("x", "y", "z")
print(V.__slots__)
'Vector' object has no attribute '__dict__'
('x', 'y')
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[15], line 12
10 print(e)
11 print(V.__slots__)
---> 12 V.__slots__ = ("x", "y", "z")
13 print(V.__slots__)
AttributeError: 'Vector' object attribute '__slots__' is read-only
可以看到,当我们在对象中设置了__slots__属性后,对象将不存在__dict__这个内容,接下来就该到了验证__slots__节省内存的环节了
使用__slots__节省内存案例
import time
import tracemalloc
def check(Func, *args, num = 1000):
"""检查对象生成n个消耗的时间和内存"""
tracemalloc.start()
start_time = time.time()
l = [Func(*args) for i in range(num)]
end_time = time.time()
current, peak = tracemalloc.get_traced_memory()
time_taken = end_time - start_time
current_memory = current / (1024 * 1024) # Convert to MB
peak_memory = peak / (1024 * 1024) # Convert to MB
print(f"耗时:{time_taken} 秒")
print(f"当前内存使用:{current_memory} MB")
print(f"峰值内存使用:{peak_memory} MB")
class V1:
def __init__(self, x, y):
self.x = x
self.y = y
check(V1, 100, 100, num=1000000)
耗时:1.8717374801635742 秒
当前内存使用:153.36623096466064 MB
峰值内存使用:153.3664026260376 MB
class V2:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
check(V2, 100, 100, num=1000000)
耗时:1.162348747253418 秒
当前内存使用:54.17479705810547 MB
峰值内存使用:54.17496871948242 MB
关于__slots__的子类问题
class A:
__slots__ = ("X", "Y")
def __init__(self, X, Y):
self.X = X
self.Y = Y
class B(A):
def __init__(self, X, Y, Z):
self.X = X
self.Y = Y
self.Z = Z
b = B(4, 5, 6)
print(b.__dict__)
print(b.__slots__)
{'Z': 6}
('X', 'Y')
可以看到子类会继承父类的__slots__属性,但是由于子类没有设置__slots__,导致__dict__仍旧存在(与Z变量无关)
class A:
__slots__ = ("X", "Y")
def __init__(self, X, Y):
self.X = X
self.Y = Y
class B(A):
def __init__(self, X, Y):
self.X = X
self.Y = Y
b = B(4, 5)
print(b.__dict__)
print(b.__slots__)
{}
('X', 'Y')
所以,为了实现节省内存这件事,我们应该在相关子类中也通过__slots__中添加多出来的变量(因为会从父类中继承一部分)
为__slots__的对象添加弱引用?
import weakref
class P:
__slots__ = ("X", "Y")
def __init__(self, X, Y):
self.X = X
self.Y = Y
p = P(5, 6)
result = weakref.ref(p)
print(result)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[30], line 8
6 self.Y = Y
7 p = P(5, 6)
----> 8 result = weakref.ref(p)
9 print(result)
TypeError: cannot create weak reference to 'P' object
可以看到,使用__slots__修饰的对象实例无法添加弱引用,解决方法也很简单,就是在__slots__的元组中添加__weakref__属性就可以
import weakref
class P:
__slots__ = ("X", "Y", "__weakref__")
def __init__(self, X, Y):
self.X = X
self.Y = Y
p = P(5, 6)
result = weakref.ref(p)
print(result())
<__main__.P object at 0x00000161B61F4CC0>