Skip to content

Instantly share code, notes, and snippets.

@domodomodomo
Last active November 4, 2019 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save domodomodomo/0763dfdf960bd866d3d34efb2f017121 to your computer and use it in GitHub Desktop.
Save domodomodomo/0763dfdf960bd866d3d34efb2f017121 to your computer and use it in GitHub Desktop.
Make an object immutable without namedtuple.
"""
# 概要
immutable なオブジェクトを生成するメタクラス Immutable
属性参照は速いけど
namedtuple: 0.07781907100070384
自作したクラス: 0.04243788100029633
インスタンス化は遅い
namedtuple: 0.5539945139989868
自作したクラス: 0.9362985030002164
インスタンス化が圧倒的に namedtuple よりも遅かったので、
光の速さでお蔵入りになりました。
1 回生成したオブジェクトにつき
20 回以上属性参照をやるようなケースでは
メタクラスで作った Immutable の方が速くはなります。
しかし、1つのオブジェクトに対して 20 回以上も
属性参照するようなことってないと思うので。
# 実装方針
属性参照の速くなる __slots__ と メタクラスを組み合わせて、
属性参照の速い immutable なクラスを作ると言うことしています。
1) まず mutable なクラスからインスタンス化して、
2) 次に immutable なクラスにキャストする
ということをしています。
# 以下のコード
大きく以下の3つのパートからなっています。
1. 定義: メタクラス Immutable
2. 動作確認: 簡単に動くかどうか
3. 計測計測: 属性参照とインスタンス化
スクリプトは、そのまま実行できます。
計測した時間を出力します。
$ python immutable_metaclass.py
### 時間計測
# 属性参照
普通のクラス 0.07876141
__slots__ を使ったクラス 0.07551351699999999
namedtuple を使ったクラス 0.10180483000000001
Immutable メタクラスを使ったクラス 0.08366162300000002
# インスタンス化
普通のクラス 0.48623005900000005
__slots__ を使ったクラス 0.41285596299999994
namedtuple を使ったクラス 0.5147371169999999
Immutable メタクラスを使ったクラス 0.9039751979999999
$
"""
import collections
import os
import timeit
#
# 1. 定義
#
class Immutable(type):
"""Meta class to make object immutable."""
def __init__(self, name, bases, name_space):
# 1. Instantiate an object as mutable,
# then cast the object to immutable.
if '__new__' not in name_space:
self.__new__ = type(self).new
# 2. Make class immutable.
self.__setattr__ = self.setattr
# 3. Define mutable class for each immutable class,
# because of the __slots__'s limitation.
# > __class__ assignment works
# > only if both classes have the same __slots__.
# > [3.3.2.4.1. Notes on using __slots__](http://bit.ly/2txVQ7i)
exec(self.mutable_class_template())
self.MutableClass = eval('MutableClass')
#
# 1度文字列にした方が2倍近く速くなる。
#
# # 3-1) faster 1.0176027979996434
# exec(cls.mutable_class_template())
# cls.MutableClass = eval('MutableClass')
#
# # 3-2) slower 1.7439573310002743
# class MutableClass(object):
# __slots__ = cls.__slots__
# def __init__(self, *args):
# for slot, arg in zip(cls.__slots__, args):
# setattr(self, slot, arg)
# cls.MutableClass = MutableClass
def new(self, *args):
# 1. Instantiate the object as mutable object.
instance = self.MutableClass(*args)
# 2. Cast the object's class to immutable.
instance.__class__ = self
return instance
def mutable_class_template(self):
return os.linesep.join((
'class MutableClass(object):',
' __slots__ = ' + repr(self.__slots__),
' def __init__(self, ' + ', '.join(self.__slots__) + '):',
'',
)) + os.linesep.join(
' self.' + slot + ' = ' + slot for slot in self.__slots__
)
@staticmethod
def setattr(instance, key, value):
raise TypeError
#
# 2. 動作確認
# 矩形を表現する Region を作成て
# とりあえず動くか確認する。
#
# 1. Assing Immutable to metaclass.
class Region(metaclass=Immutable):
# 2. Set attribute names in __slots__.
__slots__ = (
'x1', 'y1', 'x2', 'y2',
'is_rectangle', 'is_line', 'is_dot')
# 3. If you override __new__
def __new__(cls, x1, y1, x2, y2):
width_0 = (x1 - x2 == 0)
height_0 = (y1 - y2 == 0)
is_rectangle = (not width_0 and not height_0) # 0 0
is_line = (width_0 != height_0) # 0 1 or 1 0; xor
is_dot = (width_0 and height_0) # 1 1
args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
# 4. call "cls.new(*args) method"
# to instantiate immutable object and return it.
self = cls.new(*args)
return self
def __eq__(self, other):
return all((self.x1 == other.x1,
self.y1 == other.y1,
self.x2 == other.x2,
self.y2 == other.y2))
def __repr__(self):
x1, y1, x2, y2 = self.x1, self.y2, self.x2, self.y2
return type(self).__name__ + f'({x1}, {y1}, {x2}, {y2})'
# 1. We can use __new__ inststead of __init__.
# we cannot use __init__ beacause an instance of Region is immutable.
region = Region(0, 0, 1, 1)
# 2. An assignment raises an error.
try:
region.x1 = 100
except TypeError:
pass
else:
raise
# 3. We can use methods properly.
region1 = Region(0, 0, 1, 1)
assert str(region1) == 'Region(0, 1, 1, 1)'
assert region1.is_rectangle
assert not region1.is_line
assert not region1.is_dot
region2 = Region(0, 0, 0, 0)
assert str(region2) == 'Region(0, 0, 0, 0)'
assert not region2.is_rectangle
assert not region2.is_line
assert region2.is_dot
assert region1 != region2
# 4. instantiation
# type -> Immutable -> Region -> region
assert type(region) is Region
assert type(Region) is Immutable
assert type(Immutable) is type
assert type(type) is type
# 5. inheritance
assert Region.__bases__ == (object,)
assert Region.__bases__ != (Immutable,)
#
# 3. 時間計測
#
# 1. 普通のクラス
class Point1(object):
def __init__(self, x, y):
self.x, self.y = x, y
point1 = Point1(1, 1)
# 2. __slots__ を使ったクラス
class Point2(object):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x, self.y = x, y
point2 = Point2(2, 2)
# 3. collections.namedtuple から生成したクラス
Point3 = collections.namedtuple('Point1', ('x', 'y'))
point3 = Point3(3, 3)
# 4. Immutable メタクラスから生成したクラス
class Point4(metaclass=Immutable):
__slots__ = ('x', 'y')
point4 = Point4(4, 4)
print('### 時間計測')
def print_result(label, stmt):
print(ljust(label, 40), str(measure(stmt)))
def measure(stmt):
return timeit.timeit(stmt, number=1_000_000, globals=globals())
def ljust(str_, len_):
count = 0
for char in str_:
if ord(char) <= 255:
count += 1
else:
count += 2
return str_ + (len_ - count) * ' '
print('# 属性参照')
print_result('普通のクラス', 'point1.x')
print_result('__slots__ を使ったクラス', 'point2.x')
print_result('namedtuple を使ったクラス', 'point3.x')
print_result('Immutable メタクラスを使ったクラス', 'point4.x')
print('# インスタンス化')
print_result('普通のクラス', 'Point1(0, 0)')
print_result('__slots__ を使ったクラス', 'Point2(0, 0)')
print_result('namedtuple を使ったクラス', 'Point3(0, 0)')
print_result('Immutable メタクラスを使ったクラス', 'Point4(0, 0)')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment