Python Static 变量和方法

Python中并不像Java或者C++一样有原生支持的static关键字,那么我们要如何定义一个class的static variable或是static method呢?

我在Google上搜了一下,结合自己的一些理解,这里给出一些实现static关键字的方式,并且给出了一定的解释。下面的所有代码均在Python3.6中调试,2.7中的实现会略有不同。

0. Python的类变量

Python类变量的行为类似于Java中的static变量,但是还是有一些小的区别。

1
2
3
4
5
class Foo(object):
count = 1
def bar(self, x):
count += x

在上面的代码中,我们的Foo类含有一个类变量count和一个实例变量self.count。类变量是可以直接通过类名.变量名进行访问的,所有该类的实例对象共享这一个变量。

1
2
3
4
5
6
func1, func2 = Foo(), Foo()
print(Foo.count) # 1
Foo.count += 1
print(func1.count) # 2
Foo.count += 1
print(func2.count) # 3

但是,如果对实例进行类变量的操作,那么会生成一个新的同名实例变量,覆盖掉该对象之前的类变量,导致与预期的static行为不符。

1
2
3
4
5
6
7
8
9
# 接着上面的代码
func1.count = 55 # 修改了实例对象,此时实质上在func1对象内部生成了self.count = 55
Foo.count += 1
print(func2.count) # 4
print(func1.count) # 55
Foo.count += 1
print(Foo.count) # 5
print(func2.count) # 5
print(func1.count) # 55

因此,使用Python类变量时要小心,如果想要保持static的行为,实例变量的明明应当与类变量相区分。

在这顺带也扯一些类内访问控制的东西。众所周知,如果想要一个实例变量或是类变量无法在类的外部被访问,我们可以在变量名前添加两条下划线,例如varName就可以改成__varName
但是实际上,我们仍然可以从类的外部进行访问。。。

1
2
3
4
5
6
7
8
class Foo(object):
def __init__(self, x):
self.__count = x
obj = Foo(5)
print(obj.__count) # AttributeError: type object 'Foo' has no attribute '__count'
print(obj._Foo__count) # 5... success!

可以使用形如实例名._类名__变量名的方式来访问受控制的类内变量。

(最好别这么做,这样做的行为是不可预知的,况且别人已经明确了不想给你看…)

1. 使用global关键字

Python中的global关键字表示声明的变量是当前module的全局变量。

注意:global变量并不是当前Python进程(process)中的全局变量!

@property装饰器可以将类中的方法包装成为类的属性,在类定义的对象中外部调用方法时,可以直接采用类似于获取对象的语句: obj.attr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_count = 0
class Foo(object):
def bar(self, x):
global _count
for _ in range(x):
_count += 1
@property
def count(self):
global _count
return _count
######
# Test
c1, c2, c3 = Foo(), Foo(), Foo()
print(c1._count) # 0
c1.bar(2)
print(c2._count) # 2
c2.bar(3)
print(c3._count) # 5

上面的测试输出中可以看到,对于不同的实例对象,他们的count属性(通过使用@property装饰器包装count()方法模拟)指向的是同一个类变量。

如果我们像像Java一样对类名直接调用方法,我们可以使用如下的方式:

1
2
3
4
5
6
7
Foo.bar(Foo, 2)
print(Foo().count) # 2
Foo.bar(Foo, 2)
print(Foo().count) # 4
Foo.bar(Foo, 2)
print(Foo.count) # <property object at some_addr>

2. 使用@classmethod装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Foo(object):
_count = 0
@classmethod
def bar(cls, x):
for _ in range(x):
cls. _count += 1
######
# Test
c1, c2, c3 = Foo(), Foo(), Foo()
print(c1._count) # 0
c1.bar(2)
print(c2._count) # 2
c2.bar(3)
print(c3._count) # 5
Foo.bar(1) # 6
Foo.bar(2) # 8
Foo.bar(3) # 11

这种方法相比于方法1要更为简洁,而且相当于@classmethod装饰器已经为我们完成了大部分的封装。我们可以直接通过类名来调用对应的方法。但是_count变量在这里是可以被外部直接修改的,使用如Foo._count = 12这样的语句即可。

在这里也可以使用@staticmethod装饰器,它跟@classmethod的区别可以参考这篇文章

这种实现存在的问题是,_count本质上仍然是类的成员变量,如果直接手动修改每个对象的_count变量,那么_count的行为与预期的static是不符的。在Java中,static变量是与类绑定的,修改任意一个实例的static变量,其变化都会反映在所有该类的对象上。

1
2
3
4
c1, c2= Foo(), Foo()
c1._count = 12
print(c1._count) # 12
print(c2._count) # 0 <----- bad behavior!

3. 设计一个callable的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo(object):
def __init__(self):
self._count = 0
def __call__(self, x):
for _ in range(x):
self. _count += 1
print(self._count)
func1 = Foo()
func1(1) # 1
func1(2) # 3
func1(3) # 6
func2 = Foo()
func2(1) # 1
func2(2) # 3
func2(3) # 6

这种方法实质上是将类包装成了一个可调用的方法。也就是说,当我们调用默认的constructor(__init(self)时,生成的其实是一个可调用的函数对象。而这个函数对象的行为则在__call(self,x)__中定义了。每次我们调用这个constructor,返回的都是不同的函数对象。

因此,从原理上来看这种方法有着与前面方法2一样的问题。不同实例对象的类变量依然是独立的。

因此,这种方式仅适合实现static类方法。

4. 使用metaclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Callable(type):
def __call__(self, *args, **kw):
return self._call(*args, **kw)
class yourfunc(object):
__metaclass__ = Callable
_numer_of_times = 0
@classmethod
def _call(cls, x, y):
for i in range(x):
for j in range(y):
cls._number_of_time += 1

这种方法是在网上看到的,大部分开发过程中并不会用到metaclass这样的特性,因此看看就好了。。。

5. 使用默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Foo(object):
def bar(cls, x, _static_var=[0]):
for i in range(x):
_static_var[0] += 1
print(_static_var[0])
# Python 3.6
Foo.bar(Foo, 1) # 1
Foo.bar(Foo, 2) # 3
Foo.bar(Foo, 3) # 6
# Python 2.7
# It takes 'cls' the same as 'self', so it requires an instance.
# Foo.bar(Foo(), 1) # 1
# Foo.bar(Foo(), 2) # 3
# Foo.bar(Foo(), 3) # 6
a, b = Foo(), Foo()
a.bar(4) # 10
b.bar(5) # 15

这种方法是最tricky的。这种做法的原理在于:函数的默认参数只在module被载入(load)时初始化一次, 所以在一个module中,同一个函数的同一个默认参数都指向同一个内存中的对象。我们在实例代码中令默认参数_static_var指向了一个list对象。在程序的生命周期中,该引用的指向没有变化,因此每次更新都会反映在整个类及所有该类的对象中。

EoF

其实看到这个问题,我的第一想法是用闭包(closure)来实现。但是在Google上看了一下似乎没人这么做。Python的设计哲学是能用一种方法解决的事,就不要反复造不同花式的轮子来做。但是static这个轮子,不知为什么语言的设计者没有考虑,我个人的猜想可能是与Python的解释运行模式有关?

References:

  1. https://www.cnblogs.com/2gua/archive/2012/09/03/2668125.html
  2. https://stackoverflow.com/questions/26630821/static-variable-in-python

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.