欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

理解 Python 类的变量,方法与属性

程序员文章站 2022-06-30 17:35:59
熟悉了传统的 C++/Java 类定义的风格,来感受一下 Python 是如何定义类的。本篇是阅读 《The Quick Python Book》第二版关于类定义的笔记,由原书内容进一步引申,不过是依照本人的思考顺序来组织的。在理解 Python 类定义的同时头脑中应该闪现出 JavaScript/ ......

熟悉了传统的 c++/java 类定义的风格,来感受一下 python 是如何定义类的。本篇是阅读 《the quick python book》第二版关于类定义的笔记,由原书内容进一步引申,不过是依照本人的思考顺序来组织的。在理解 python 类定义的同时头脑中应该闪现出 javascript/java 如何定义类的情景。

最简单的类定义

理解 Python 类的变量,方法与属性

 class myclass:      pass 

由于 class myclass 后面要有个冒号,而冒号后总得有点东西才能表示该类定义结束了,于是放个 pass 当占位符。python 也像 java 一样,有一个根类,叫做 object,例如上面的定义

>>> myclass.__bases__
(<class 'object'>,)
>>> import inspect
>>> inspect.getmro(myclass)
(<class '__main__.myclass'>, <class 'object'>)

我们能看到它隐式的基类是 object , 而不用显式的声明为 class myclass(object) 。看到 __bases__ 属性是一个 tuple, 意识到  python 是支持多重继承的。

实例的属性

python 实例的属性不需要像 java 那样放在类中方法外来定义,我们可以随时随地它实例新增属性,或在类定义外删除某个属性,俗话是 on the fly。这个特性有点像 javascript 的类。

用下面的代码来解释说明

class circle:
    pass
 
my_circle = circle()      #1
my_circle.radius = 5      #2
print(my_circle.radius)   #3
my_circle.hello = lambda name: print(name)
my_circle.hello('world')  #4  输出 'world'
del my_circle.radius      #5 hello 也是个属性,所以也能 del my_circle.hello
print(my_circle.radius)   #6

我们首先创建一个最粗糙的类 circle

  1. #1, 创建 circle 实例的方式是类名后加上括号当方法用,没有 new 关键字,有点类似 scala 的 case class 的实例创建。后面我们会知道 circle() 会映射到对 __init__  方法的调用
  2. #2,新创建的 my_circle 没有任何自定的属性,想要加新属性直接用点号添加就行,该属性不存在就会创建
  3. #3, 输出新加的属性,输出 5
  4. 用 lambda 随时增加一个方法也不是事,但是这个 lambda 却不知如何访问当前实例 my_circle 中的成员了
  5. #4, del  关键字还能删除实例的属性
  6. #5,属性已删除,因此会报出  attributeerror: 'circle' object has no attribute 'radius'  的错误

看看下面的实例方法也可以操作实例的属性

class circle:
    def __init__(self):
        self.radius = 1
 
    def foo(self):  #实践中不建议下面的操作,实例属性应该在构建函数 __init__ 中声明
        del self.radius
        self.color = 'red'
 
 
my_circle = circle()
print(my_circle.radius) # 1
# print(my_circle.color) # attributeerror: 'circle' object has no attribute 'color'
my_circle.foo()         # 调用该方法才创建 my_circle 的 color 属性
print(my_circle.color)  # 'red'
print(my_circle.radius) # attributeerror: 'circle' object has no attribute 'radius'

简单的来讲,python 的实例属性就是绑定在 self 上的属性。

上方代码只是演示了 python 提供了那些特性,实际编码中应该在 __init__ 中引入属性,而不应该恶意使用 python 的这一便利,上帝打开一扇窗不一定允许你翻窗进来。

关于实例方法

在其他面向对象语言中,一提到实例方法我们都会说,调用时会传递一个隐式参数表示调用者实例本身,一般用 this 表示,如 java/c++/c#  等,javascript 就把 this 搞得更复杂无比。比如我们在 java 中用反射来调用一个方法时 method.invoke(object, parms...) 不得不显式的传入当前实例。

而 python 中的实例方法就不再对调用者参数遮遮掩掩,明确的声明为第一个参数,通常命名为self , 你想改成别的名称也无妨,比如 me ,当然最好不要给别人造成太大的冲击。实例方法的第一个参数写在那里,但调用的时候却不用显式传入,而是实参与方法的形参依序后推。

class myclass:
    def foo(self, a, b, c):
        self.a = a
        print(b, c)

调用实例方法

mc = myclass()
mc.foo(5, 6, 7)  #5, 6, 7 分别对应到上面的  a, b, c

定义 foo 方法式,把  self 放在第一个参数方便我们访问当前实例的成员。python 的实例方法用了 self 之后在访问成员变量与局部变量不在模棱两可。例如在 java 中

public void foo() {
    //string name = "world";
    system.out.println(name);
    this.name = name;
}

方法中的 name 可能是在引用一个局部变量(如果 name 在方法内部声明),也可能是引用一个实例变量(方法内未声明),只有明确用 this.name 才是对实例变量 name 的引用。然而在 python 中没有这种情况,使用实例变量必须是 self.name , 不带 self 的话,直接 name 也一定是在使用局部变量。即使方法中要使用类变量也必须明确前缀:

class myclass:
    name = 'hola'
 
    def foo(self, a):
        print(self.name, myclass.name, self.__class__.name)
        print(name) #1

以上 #1 处会得到错误: nameerror: name 'name' is not defined

私有属性与方法

python 没有像 private  那样的关键字来表明私有属性或方法,同样是用命名约定来说告诉编译器是否是私有的。python 约定双下划线 __ 开头,但不以 __ 结尾命名的就是私有的

class myclass:
 
    def __init__(self):
        self.__x = 12    # __x private
 
    def __bar(self):     # __bar private
        pass
    
    def __baz__(self):  # __baz__ public
        pass
 
 
mc = myclass()
mc.__baz__()       # ✔︎
print(mc.__x)      # ✘
mc.__bar()         # ✘

构造方法 __init__

python 的构造方法可以当作是特殊名称的实例方法来看待,额外的两个特性:1)它在用类名当成方法名使用时被调用,2)隐式返回该类的一个实例,即 self 。它的第一个参数也是 self , 其他参数顺推,我们认为一旦进入 __init__ 方法后, self 便创建就绪。然后可以基于 self 初始化实例成员。除此之外构造方法没有什么特别的,和其他实例方法完全一样,支持默认参数,变参等,甚至 __init__ 也能作为普通方法来调用。

class myclass:
    def __init__(self, name):
        self.name = name
        print(name)
 
mc = myclass('hola')  #1
x = mc.__init__('x')  #2
print(x)              # 输出 none
  1. #1, 类名当方法名来用 myclass(..), 会调用相应的  __init__  方法,返回 myclass 的实例
  2. #2, 把 __init__  当成普通方法来调用,所以它返回的是 none

和普通实例方法一样,构造方法也不支持重载,后声明的同名方法会把前面的方法定义覆盖掉。但 python 可以借助于方法的默认参数来达到与 java 等其他语言方法重载相当的效果。

类变量

既然有实例变量,python 也有类变量,类比于其他面向对象语言,类变量就是不依赖于实例而存在的变量,并且为所有实例共享。python 在访问类变量也是既能通过类名,也能通过实例来引用,推荐用类来引用类变量,这一点 c# 比较好,语法上杜绝用实例来引用类变量。

什么是类变量,写在类当中但游离于方法之外的变量就是类变量。例如:

class myclass:
    pi = 3.14159
 
    def foo(self):
        print(self.pi, myclass.pi, self.__class__.pi)  #1
        self.pi = 3.14
        print(self.pi, myclass.pi, self.__class__.pi)
        # print(pi)  # nameerror: name 'pi' is not defined
 
 
mc = myclass()
mc.foo()

以上代码输出

 3.14159 3.14159 3.14159  3.14 3.14159 3.14159 

这里演示了类变量可以通过实例或类来访问,上面 #1 表示的三种形式。不建议通过实例来访问类变量,因为通过实例不能明确是在访问实例变量还是类变量。由上可知 self.pi 优先访问实例变量 pi , 找不到实例变量 pi 才试图访问类变量 pi ,再涉及到类的继承关系就更复杂些。因此最好是使用哪个类的类变量就明确的写出特定的类名,像这里的 myclass.pi 。

对于不同引用类变量的方式,再来看几个例子:

class parent:
    pi = 3.14159
 
    def foo(self):
        return self.pi  #下面的 c.foo() 调用返回的是 3.14
        # 这里写成  self.__class__.pi 也是返回 3.14, 根据需求也许总是要 parent.pi
 
 
class child(parent):
    pi = 3.14
 
 
c = child()
print(c.pi)         #3.14
print(c.foo())      #3.14
print(parent.pi, child.pi)  # 3.14159 3.14

所以安全稳妥的方式还是 classname.variablename 。

另外,类变量也可以动态增加或删除

类方法和静态方法

什么?python 的类方法与静态方法还不一回事,在 java 里只要有 static 修饰的方法即是类方法也是静态方法。相比于实例方法,python 的类方法的第一个参数表示当前类

类方法

如果以下方式来定义一个类方法 hello

class myclass:
    def hello(cls, name):     # 其实就是 def hello(self, name)
        print(cls, name)

由于函数中的参数只是个名称,即使第一个参数名写成了 cls , 它于 def hello(self, name)定义是没有区别的,所以它实际上是一个实例方法。还必须加个装饰告诉它是一个类方法而非实例方法, cls 是当前类,而非当前实例。

class myclass:
 
    @classmethod               # 标识这是一个类方法,并且方法第一个参数为类本身,约定用 cls  表示 class
    def hello(cls, name):
        print(cls, name)
 
myclass.hello('hello')        # 类方法的调用,myclass 作为隐式参数对应于 cls

静态方法

如果只简单的定义一个无参数的方法

class myclass:
 
    def hello():          # 这里会提示错误:method must have a first parameter, usually called 'self' 
        print('hello')

无参数的方法或都方法的第一个参数既不想是 cls 也不想是 self , 那么就要用  @staticmethod 把它标识为一个静态方法

class myclass:
 
    @staticmethod
    def hello():
        print('hello')
 
    @staticmethod
    def greeting(name):
        print(name)
 
myclass.hello()
myclass.greeting('world')

方法的调用方式

python 定义方法时没有学其他面向对象语言那样把 this(self) 指向实际自身的参数隐去,而是声明时写在第一个位置上,但调用时可跳过。其实 python 调用方法时也能显式的通过第一个参数传入 self 或 cls, 这时候调用的主体就是类名。

class myclass:
 
    def instance_method(self, name):
        print('foo', name)
 
    @classmethod
    def class_method(cls, name):
        print('bar', name)
 
    @staticmethod
    def static_method(name):
        print('baz', name)
 
 
mc = myclass()
 
mc.instance_method('instance method 1')
myclass.instance_method(mc, 'instance method 2')
 
mc.class_method('class method 1')
myclass.class_method('class method 2')
 
mc.static_method('static method 1')
myclass.static_method('static method 2')

上面例子列出了三种类型方法的不同调用方式。

python 属性 @property

最后提一下 python 真正叫做属性的东西,@property,执行以下的代码

class temperature:
    def __init__(self):
        self._temp_fahr = 0
 
    @property
    def temp(self):
        print("@property")
        return (self._temp_fahr - 32) * 5 / 9
 
    @temp.setter
    def temp(self, new_temp):
        print("@temp.setter")
        self._temp_fahr = new_temp * 9/5 + 32
 
    @temp.getter
    def temp(self):
        print("@temp.getter")
        return (self._temp_fahr - 32) * 5 / 9
 
 
t = temperature()
print(t.temp)
t.temp = 23

输出为

 @temp.getter  -17.77777777777778  @temp.setter 

t.temp  调用了 @temp.getter 对应的取值方法,t.temp = 23 调用了 @temp.setter 对应的设值方法。你会发现这个类有两个 def temp(self) 方法定义,只是有不同的装饰, @property 对应的方法在这里没起到作用,相当于是

@property
def temp(self):
    pass

其实 temp.getter 才显得多余,所以通常让 @property 注解的方法承担 getter 的责任

class temperature:
    def __init__(self):
        self._temp_fahr = 0
 
    @property
    def temp(self):
        print("@property")
        return (self._temp_fahr - 32) * 5 / 9
 
    @temp.setter
    def temp(self, new_temp):
        print("@temp.setter")
        self._temp_fahr = new_temp * 9/5 + 32

@temp.getter 一般没什么用处