Last active
November 4, 2019 08:02
-
-
Save domodomodomo/0763dfdf960bd866d3d34efb2f017121 to your computer and use it in GitHub Desktop.
Make an object immutable without namedtuple.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
# 概要 | |
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