相关阅读
Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm=1001.2014.3001.5482
Python是一门面向对象的编程语言,其核心概念之一是类。类是对象的蓝图或模板,定义了对象的属性和方法。理解类的属性和方法对于掌握Python编程至关重要。本文将深入探讨Python中的类的属性和方法,帮助读者全面掌握这一重要主题。
1、类和实例
在Python中,类是使用class关键字定义的。类定义了对象的属性和方法,对象也可以称为实例是类的具体实现。例1是一个简单的类定义和其实例化:
# 例1
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person1 = Person("Alice", 30) # 创建一个实例,它拥有两个属性
当Python解释器遇到Person("Alice", 30)时,它会调用Person类的__new__()魔术方法(例1中看不到该方法是因为它继承自object基类),创建一个实例并在返回前调用__init__()魔术方法,将该实例作为第一个参数(self)传入进行初始化。
2、类的属性
类的属性可以分为两类:实例属性和类属性。
实例属性
实例属性是绑定到实例上的属性,而每个实例都有独立的实例属性,通常在__init__()魔术方法中定义,可以通过"实例名.实例属性名"或者内建函数getattr()访问实例属性(仅能作为右值)。例2展示了一些关于实例属性的操作。
# 例2
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
car1 = Car("Toyota", "Camry") # 创建一个实例,它拥有两个属性
car2 = Car("Honda", "Civic") # 创建一个实例,它拥有两个属性
print(car1.make) # 使用"实例名.实例属性名"访问实例的属性,输出:Toyota
print(getattr(car1, "make")) # 使用内建函数getattr()访问实例的属性,输出:Toyota
print(car2.make) # 使用"实例名.实例属性名"访问实例的属性,输出:Honda
print(getattr(car2, "make")) # 使用内建函数getattr()访问实例的属性,输出:Honda
事实上,其实在任何能访问到实例的地方都能访问它的属性,如例3所示,但不建议这样做。
# 例3
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
car1 = Car("Toyota", "Camry") # 创建一个实例,它拥有两个属性
car1.year = 1998 # 对于car1实例又创建了一个属性
car1.make = "Benz" # 对于car1实例,将其make属性改为字符串"Benz"(实际上是创建了字符串对象并将属性make换绑到它)
类属性
类属性是绑定到类本身的属性,所有实例共享一个类属性,类属性通常在类内方法外定义,可以通过"实例名.类属性名"、"类名.类属性名"或者内建函数getattr()访问类属性(仅能作为右值)。例4展示了一些关于类属性的操作。
# 例4
class Dog:
species = "Canis"
old = [1, 2, 3]
def __init__(self, name):
self.name = name
dog1 = Dog("Buddy")
print(dog1.species) # 使用"实例名.类属性名"访问类属性,输出:Canis
print(Dog.species) # 使用"类名.类属性名"访问类属性,输出:Canis
print(getattr(Dog, "species")) # 使用"内建函数getattr()访问类属性,输出:Canis
print(getattr(dog1, "species")) # 使用内建函数getattr()访问类属性,输出:Canis
Dog.species = "Hello" # 通过"类名.类属性名"改变类属性,这会影响所有实例的类属性
dog2 = Dog("Max")
print(dog1.species) # 输出:Hello
print(dog2.species) # 输出:Hello
dog1.species = "World" # 如果将"实例名.类属性名"作为左值(且不是针对可变对象的索引),会创建一个实例属性(实际上是创建了字符串对象并将属性species换绑到它),这不会影响类属性
print(dog1.species) # 输出:World
print(dog2.species) # 输出:Hello
dog1.old[0] = 4 # 如果将"实例名.类属性名"作为左值(且是针对可变对象的索引),不会创建一个实例属性,而是改变类属性
print(dog1.old) # 输出:[4, 2, 3]
print(dog1.old) # 输出:[4, 2, 3]
3. 类的方法
类的方法是绑定到类或实例的函数。根据绑定方式的不同,类的方法可以分为实例方法、类方法和静态方法。
实例方法
实例方法是绑定到实例上的方法,一般通过"实例名.实例方法名"调用,调用时会自动将实例作为第一个参数传入实例方法,因此其第一个参数通常命名为self(不建议更改)表示实例本身,例5展示了实例方法的调用过程:
# 例5
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print(f"{self.name} says meow.")
cat1 = Cat("Whiskers")
cat1.meow() # 输出:Whiskers says meow.
print(cat1.meow) # 输出:<bound method Cat.meow of <__main__.Cat object at 0x000001586AE68160>> 这个地址指的是实例(cat1)的地址
print(id(cat1.meow)) # 输出:1995396036288
print(id(cat1.meow)) # 输出:1995396036288
print(id(cat1.meow)) # 输出:1995396036288
a = cat1.meow
b = cat1.meow
c = cat1.meow
print(id(a)) # 输出:1995396036288
print(id(b)) # 输出:1995396036352
print(id(c)) # 输出:1995396036416
实例方法在每次调用时都会重新创建方法对象,直接使用内建函数id()检测无法观察到这一点,因为每次调用结束后,实例方法会被销毁,相应id号都会被回收,因此下次调用可能分配到相同的id号。将实例方法赋给标识符,保证其至少有一个引用,这样就可以观察到不同调用时,实例方法是重新创建的。
类方法
类方法使用@classmethod装饰器修饰,绑定到类而不是实例(但使用实例也能访问它,这在之后的特殊情况会进行说明),一般通过"类名.类方法名"调用,调用时会自动将类作为第一个参数传入类方法,第一个参数通常命名为cls(建议不更改)表示类本身,例6展示了类方法的调用过程:
# 例6
class Bird:
species = "Aves"
def __init__(self, name):
self.name = name
@classmethod
def set_species(cls, species):
cls.species = species # 注意,此处是访问类属性
bird1 = Bird("Sparrow")
Bird.set_species("NewSpecies")
print(bird1.species) # 输出:NewSpecies
print(Bird.set_species) # 输出:<bound method Bird.set_species of <class '__main__.Bird'>>
a = Bird.set_species
b = Bird.set_species
c = Bird.set_species
print(id(a)) # 输出:2228195894336
print(id(b)) # 输出:2228195894400
print(id(c)) # 输出:2228195894272
类方法在每次调用时都会重新创建方法对象,直接使用内建函数id()检测无法观察到这一点,因为每次调用结束后,类方法会被销毁,相应id号都会被回收,因此下次调用可能分配到相同的id号。将类方法赋给标识符,保证其至少有一个引用,这样就可以观察到不同调用时,类方法是重新创建的。
静态方法
静态方法使用@staticmethod装饰器修饰,它不绑定到类或实例,但一般通过"类名.静态方法名"调用,静态方法是一种逻辑上属于类但不需要访问实例属性或类属性的方法,例7展示了静态方法的调用过程:
# 例7
class Math:
@staticmethod
def add(a, b):
return a + b
result = Math.add(5, 3)
print(result) # 输出:8
print(Math.add) # 输出:<function Math.add at 0x000001B90619BAF0> 这个地址指的是静态方法的地址(它不会改变)
a = Math.add
b = Math.add
c = Math.add
print(hex(id(a))) # 输出:0x1b90619baf0
print(hex(id(b))) # 输出:0x1b90619baf0
print(hex(id(c))) # 输出:0x1b90619baf0
静态方法不会在每次调用时都会重新创建方法对象,将类方法赋给标识符,保证其至少有一个引用,这样就可以观察到不同调用时,静态方法不会重新创建。
4. 特殊情况
实例方法的非绑定调用
上面所说的实例方法的调用是绑定式调用,即对于实例方法,使用了相应的实例调用,但这并不是Python强制要求的,例8展示了实例方法的非绑定调用,使用“类名.实例方法名”就可以做到。
# 例8
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print(f"{self.name} says meow.")
cat1 = Cat("Whiskers")
# 利用“实例名.实例方法名”的形式绑定调用,无需手动传递实例作为参数,会自动将实例作为实例方法的第一个参数传入
cat1.meow() # 输出:Whiskers says meow.
# 利用“类名.实例方法名”的形式非绑定调用,需要手动传递实例作为参数
Cat.meow(cat1) # 输出:Whiskers says meow.
Cat.meow(self=cat1) # 输出:Whiskers says meow.
print(Cat.meow) # 输出:<function Cat.meow at 0x0000024D1973BB80> 这个地址指的是非绑定的实例方法的地址
print(cat1.meow) # 输出:<bound method Cat.meow of <__main__.Cat object at 0x0000021E51FF3FD0>> 这个地址指的是实例(cat1)的地址
a = Cat.meow
b = Cat.meow
c = Cat.meow
print(hex(id(a))) # 输出:0x24d1973bb80
print(hex(id(b))) # 输出:0x24d1973bb80
print(hex(id(c))) # 输出:0x24d1973bb80
可以看到,print("类名.实例方法名")的输出结果就像是静态方法那样,没有绑定到类或实例,事实上,非绑定的实例方法就像是静态方法那样不会在每次调用时都会重新创建方法对象。在进行非绑定调用时,需要手动传递实例对象,而对于绑定式调用,实例对象会自动传入并作为实例方法的第一个参数。
实际上,在进行非绑定调用时,传入的甚至可以不是该类的实例,如例9所示。
# 例9
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print(f"{self.name} says meow.")
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says bark.")
def What(self):
print(f"This is just like an {self}")
cat1 = Cat("Whiskers")
dog1 = Dog("Fido")
Cat.meow(dog1) # 输出:Fido says meow.
Dog.bark(cat1) # 输出:Whiskers says bark.
Dog.What("function") # 输出:This is just like an function
对于例9,只要传入bark方法的实例拥有name属性,传入meow方法的实例拥有name属性,就不会报错(这是因为方法中访问了传入对象的name属性)。对于What方法,传入的参数甚至没要求。
警告:这是个不建议的行为,因为它消除了实例方法和实例之间的绑定,甚至在某种意义上,该方法都不能称为实例方法了(因为可以像调用任何普通的函数一样调用它)。
一个好的建议是:对于所有的实例方法,都使用实例绑定式调用。
类方法的非绑定调用
上面所说的类方法的调用是绑定式调用,即对于类方法,使用了相应的类调用。这并不是Python强制要求的,例10展示了类方法的非绑定调用,使用“实例名.类方法名”就可以做到。
# 例10
class Bird:
species = "Aves"
def __init__(self, name):
self.name = name
@classmethod
def show_species(cls):
print(cls)
bird1 = Bird("Sparrow")
# 利用“类名.类方法名”的形式绑定调用,会自动将类作为类方法的第一个参数传入
Bird.show_species() # 输出:<class '__main__.Bird'>
# 利用“实例名.类方法名”的形式非绑定调用,会自动将实例所属的类作为类方法的第一个参数传入
bird1.show_species() # 输出:<class '__main__.Bird'>
print(Bird.show_species) # 输出:<bound method Bird.show_species of <class '__main__.Bird'>>
print(bird1.show_species) # 输出:<bound method Bird.show_species of <class '__main__.Bird'>>
a = bird1.show_species
b = bird1.show_species
c = bird1.show_species
print(id(a)) # 输出:2268603949888
print(id(b)) # 输出:2268603949952
print(id(c)) # 输出:2268603950016
可以看到,print("实例名.类方法名")的输出结果还是绑定到类,且在每次调用时都会重新创建方法对象,类方法的非绑定调用要求调用类方法的必须是该类的实例。
一个好的建议是:对于所有的类方法,都使用相应的类进行调用。
静态方法的实例调用和访问实例属性
其实静态方法只代表着,不论是使用实例还是类,对其进行调用时,不会自动传递实例或类作为参数,在实例方法的非绑定调用中,手动传入了实例,这也代表着静态方法其实也可以访问类或实例属性(尽管这不推荐)。使用实例名也可以调用静态方法,即“实例名.静态方法名”。如例11所示。
# 例11
class Math:
def __init__(self, x, y):
self.x = x
self.y = y
@staticmethod
def add(a, b):
return a + b
@staticmethod
def mul(self):
return self.x * self.y
math1= Math(2, 3)
# 利用“类名.静态方法名”的形式调用
result1 = Math.add(5, 3)
print(result1) # 输出:8
# 利用“实例名.静态方法名”的形式调用,此时相当于使用该实例所属的类调用
result2 = math1.add(5, 3)
print(result2) # 输出:6
print(Math.add) # 输出:<function Math.add at 0x000001DAE0E9A9D0> 这个地址指的是静态方法的地址(它不会改变)
print(math1.add) # 输出:<function Math.add at 0x000001DAE0E9A9D0> 这个地址指的是静态方法的地址(它不会改变)
a = math1.add
b = math1.add
c = math1.add
print(hex(id(a))) # 输出:0x1dae0e9a9d0
print(hex(id(b))) # 输出:0x1dae0e9a9d0
print(hex(id(c))) # 输出:0x1dae0e9a9d0
result3 = Math.mul(math1)
print(result3) # 输出:6
可以看到,print("实例名.静态方法名")的输出结果和print(Math.add)一样,都是静态方法的地址,且在每次调用时不会重新创建方法对象,静态方法的实例调用要求调用静态方法的实例必须是该类的实例。
一个好的建议是:对于所有的静态方法,使用相应的类进行调用,并且不要访问任何属性和方法。
总结
本文总结了Python中类的属性(实例属性、类属性)和方法(实例方法、类方法、静态方法)相关的知识点,并在最后给出了一些特殊情况,这并不是鼓励去使用,只是为了更加严谨,避免”类方法不能访问实例属性“或”静态方法不能访问属性“之类的绝对说辞。