Python Cleanコードはディスクの立盤実戦を発表します

44426 ワード

コードをディスクフライスとして抽象化することが重要です.
重複コードを抽象化することで、クライアントコードを革新的に削減できます.
本コードの目的
属性を持つ一般クラスでは、属性の値が変化した場合に追跡を試みます.
具体的には
クラスは旅行者を表し、現在の都市の属性を有する.
ユーザーがアクセスする都市をプログラムで追跡します.
実施1号
属性のsetterメソッドで値の変更を確認すると、リストなどの内部変数に値を保存します.
import time


class Traveller:
    def __init__(self, name, current_city):
        self.name = name
        self._current_city = current_city
        self._cities_visited = [current_city]

    @property
    def current_city(self):
        return self._current_city

    @current_city.setter
    def current_city(self, new_city):
        if new_city != self._current_city:
            self._cities_visited.append(new_city)
        self._current_city = new_city

    @property
    def cities_visited(self):
        return self._cities_visited
 
    """
    >>> alice = Traveller("Alice", "Barcelona")
    >>> alice.current_city = "Paris"
    >>> alice.current_city = "Brussels"
    >>> alice.current_city = "Amsterdam"
    >>> alice.cities_visited
    ['Barcelona', 'Paris', 'Brussels', 'Amsterdam']
    >>> alice.current_city
    'Amsterdam'
    >>> alice.current_city = "Amsterdam"
    >>> alice.cities_visited
    ['Barcelona', 'Paris', 'Brussels', 'Amsterdam']
    >>> bob = Traveller("Bob", "Rotterdam")
    >>> bob.current_city = "Amsterdam"
    >>> bob.current_city
    'Amsterdam'
    >>> bob.cities_visited
    ['Rotterdam', 'Amsterdam']
    """
同じ論理を多くの場所で使うと、繰り返し使うので面倒です.
レコーダまたはPropertyコンストラクタは記述できますが、Propertyコンストラクタはディスク立棒のより複雑な特殊なバージョンであり、本書では言及していません.
(疑問:Python property builderにはほとんど現れません;;設計モードのみでコンストラクタを生成します)
https://refactoring.guru/design-patterns/builder/python/example )
ディスク・レバーを使用した理想的な方法

class HistoryTracedAttribute:
    """Trace the values of this attribute into another one given by the name at
    ``trace_attribute_name``.
    """

    def __init__(self, trace_attribute_name: str) -> None:
        self.trace_attribute_name = trace_attribute_name # [1]
        self._name = None

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        self._track_change_in_value_for_instance(instance, value)
        instance.__dict__[self._name] = value

    def _track_change_in_value_for_instance(self, instance, value):
        self._set_default(instance) # [2]
        if self._needs_to_track_change(instance, value):
            instance.__dict__[self.trace_attribute_name].append(value)

    def _needs_to_track_change(self, instance, value) -> bool:
        """Determine if the value change needs to be traced or not.
        Rules for adding a value to the trace:
            * If the value is not previously set (it's the first one).
            * If the new value is != than the current one.
        """
        try:
            current_value = instance.__dict__[self._name]
        except KeyError: # [3]
            return True
        return value != current_value # [4]

    def _set_default(self, instance):
        instance.__dict__.setdefault(self.trace_attribute_name, []) # [6]


class Traveller:
"""
A person visiting several cities.
We wish to track the path of the traveller, as he or she is visiting each
new city.
"""

    current_city = HistoryTracedAttribute("cities_visited") # [1]

    def __init__(self, name, current_city):
        self.name = name
        self.current_city = current_city # [5]

  • プロパティの名前は、ディスク・バッチに割り当てられた変数の1つで、current cityです.
    次に、変数の名前をディスクライタに渡して、変数の追跡を保存します.この例ではcities visuateプロパティ
    トレースcurrent cityのすべての値を示します.

  • ディスク・ディスクリートが最初に呼び出されると、トレース値は存在しないため、後で追加するために空の配列に初期化されます.

  • Travelerを最初に呼び出すときにアクセスポイントがないため、インスタンス辞書にもcurrent cityキーは存在しません.
    このような状況でも新たな旅行先が生まれ、追跡の対象となる.これは、前にディレクトリを初期化した理由と似ています.

  • 変更は、現在設定されている値と新しい値が異なる場合にのみ追跡されます.

  • Travelerのinitメソッドでは、ディスクエンドミルが作成されています.[割り当て](Assign)コマンドは、[空のリストを作成](Create Empty List)を実行してステップ2の値を追跡し、[空のリストを作成](Create Empty List)コマンドは、ステップ3を実行してリストに値を追加し、後で検索するキーを設定します.

  • 辞書のsetdefaultメソッドは、KeyErrorを回避するために使用されます.setdefaultは、最初のパラメータの値を返し、ない場合は2番目のパラメータを返します.
  • >>> alice = Traveller("Alice", "Barcelona")
    >>> alice.current_city = "Paris"
    >>> alice.current_city = "Brussels"
    >>> alice.current_city = "Amsterdam"
    >>> alice.cities_visited
    ['Barcelona', 'Paris', 'Brussels', 'Amsterdam']
    >>> alice.current_city
    'Amsterdam'
    >>> alice.current_city = "Amsterdam"
    >>> alice.cities_visited
    ['Barcelona', 'Paris', 'Brussels', 'Amsterdam']
    >>> bob = Traveller("Bob", "Rotterdam")
    >>> bob.current_city = "Amsterdam"
    >>> bob.current_city
    'Amsterdam'
    >>> bob.cities_visited
    ['Rotterdam', 'Amsterdam']
    
    ディスク・スクリプト・コードは複雑ですが、クライアント・クラス・コードはかなり簡単になります.
    ディスク・バッチはクライアント・クラスとは完全に独立しているため(ビジネス・ロジックはなく、通常は関数名も作成されます).
    まったく異なるクラスに適用すると、同じ効果が得られます.
    ディスク・スクリプトは、ビジネス・ロジックを実装するよりも、ライブラリ、フレームワーク、または内部APIを定義するのに適しています.
    その他のタイプのディスクエンドミル
    グローバル状態共有ホットスポット
    ディスク・バッチはクラス・プロパティに設定されているため、いくつかの点を考慮する必要があります.
  • クラスのプロパティであるため、データをディスク立棒オブジェクトに保存すると、すべてのオブジェクトが同じ値にアクセスできます.
  • class SharedDataDescriptor:
        def __init__(self, initial_value):
            self.value = initial_value
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return self.value 
    
        def __set__(self, instance, value):
            self.value = value # slef 즉 ClientClass.descriptor에 받음 
    
    
    class ClientClass:
        descriptor = SharedDataDescriptor("first value")
        
    >>> client1 = ClientClass()
    >>> client1.descriptor
    'first value'
    >>> client2 = ClientClass()
    >>> client2.descriptor
    'first value'
    >>> client2.descriptor = "value for client 2"
    >>> client2.descriptor
    'value for client 2'
    >>> client1.descriptor
    'value for client 2'
    
    1つのオブジェクトの値が変更され、すべてのオブジェクトの値が変更されました.ClientClass.記述子が唯一だからです.すべてのインスタンスのプロパティは同じです.
    クラス内のすべてのオブジェクトの共有ステータスのBorgモードを実際に使用できますが、通常はオブジェクトを区別します(9章「一般設計モード」で詳しく説明します).
    (疑問)
    次のように変更すると、値は共有されません.でも不確実なら.次はいったいどのように機能しているのでしょうか.
    instance.__dict[self.name]=valueの違いは?
    上書きすると思いますが、delがないから削除するわけにはいきません...
    set nameに変えてみようか?!?
    class SharedDataDescriptor:
        def __init__(self, initial_value):
            self.value = initial_value
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return self.value
    
        def __set__(self, instance, value):
            instance.value = value
    
    
    class ClientClass:
        descriptor = SharedDataDescriptor("first value")
        
    解決策オブジェクトへの事前アクセス
  • ディスク・バッチは、各インスタンスの値を保存して返す必要があります.これが、インスタンスのdict辞書で値を設定して検索する理由です.gettar()またはsettar()は無限ループのため使用できないため、dictプロパティを変更することが最後に使用可能な選択です.
  • 解決策弱い参照の使用
  • weakrefモジュールを使用する弱いキー辞書
  • dictを使用したくない場合は、ディスク立棒オブジェクトを内部に直接マッピングすることで、各インスタンスの値を保持および戻すことができます.
    ただし、これらの内部マッピングを行う場合、辞書は使用できません.クライアントはディスク・スクリプトの参照を使用できますが、ディスク・スクリプトはディスク・スクリプトを使用するオブジェクトの参照(インスタンス)に参照を持つため、ループ依存関係として不正に収集されないという問題があります.そのためweakrefを使用します.
    weakref英語説明
    Weakref韓国語説明
    弱参照について
    from weakref import WeakKeyDictionary
    
    
    class DescriptorClass:
        def __init__(self, initial_value):
            self.value = initial_value
            self.mapping = WeakKeyDictionary()
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return self.mapping.get(instance, self.value)
    
        def __set__(self, instance, value):
            self.mapping[instance] = value
    
    
    class ClientClass:
        descriptor = DescriptorClass("default value")
    
    
    >>> client1 = ClientClass()
    >>> client2 = ClientClass()
    >>> client1.descriptor = "new value"
    client1 must have the new value, whilst client2 has to still be with the
    default one:
    >>> client1.descriptor
    'new value'
    >>> client2.descriptor
    'default value'
    Changing the value for client2 doesn't affect client1
    >>> client2.descriptor = "value for client2"
    >>> client2.descriptor
    'value for client2'
    >>> client2.descriptor != client1.descriptor
    True
    弱参照で考慮すべき事項

  • ディスクライタにはプロパティがあります.
    インスタンス・オブジェクトには属性がないため、議論があります.概念の観点から見れば正しくないかもしれない.これらの詳細を忘れた場合は、オブジェクト辞書の内容(ex:vars(client)オブジェクトに属性がないため、完全なデータは返されません.

  • オブジェクトはhash法を実施することによってハッシュを実現しなければならない.ハッシュが不可能であれば、WeakKeyDictionaryにマッピングできません.一部のアプリケーションでは、これは厳しい要件である可能性があります.
  • したがって、通常はインスタンスのdict辞書を使用することが望ましい.
    ディスクライタのその他の注意事項.
    ディスク・レバーを使用して実装する場所はどこですか?
    ディスク立棒で実施するメリットとデメリットは?
    コード再利用
    保証する
    Propertyはディスク・バッチの特殊な状況です.
    構成が必要な構造の重複が発生した場合、ディスク立棒を使用して抽象化すると、コードの重複を減らすことができます.
    @prorety Decoratorはget、set、deleteを定義します.
    これは、すべてのディスク・レバー・プロトコルを実現したディスク・レバーです.
    つまり、ディスク・レバーは、プログラムよりも複雑なタスクに使用できます.
    レコーダとの比較
    レコーダのように3のルールを使うことができます.
    ディスク・バッチには、通常、ビジネス・ロジックは含まれず、実装コードのみが含まれます.
    つまり、内部APIでのみディスクレバーを使用することを推奨します.これは、使い捨てソリューションではなく、ライブラリやフレームワークの設計に機能を拡張するのに便利だからです.
    クラスdecorateと実装の比較
    from datetime import datetime
    from functools import partial
    from typing import Any, Callable
    
    
    class BaseFieldTransformation:
        """Base class to define descriptors that convert values."""
    
        def __init__(self, transformation: Callable[[Any, str], str]) -> None:
            self._name = None
            self.transformation = transformation
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            raw_value = instance.__dict__[self._name]
            return self.transformation(raw_value)
    
        def __set_name__(self, owner, name):
            self._name = name
    
        def __set__(self, instance, value):
            instance.__dict__[self._name] = value
    
    
    ShowOriginal = partial(BaseFieldTransformation, transformation=lambda x: x)
    HideField = partial(
        BaseFieldTransformation, transformation=lambda x: "**redacted**"
    )
    FormatTime = partial(
        BaseFieldTransformation,
        transformation=lambda ft: ft.strftime("%Y-%m-%d %H:%M"),
    )
    
    functtools.partialを使用して、他のサブクラスを作成します.呼び出し可能な関数をクラス戻り関数に直接渡し、関数の新しいバージョンを作成します.
    
    class LoginEvent:
        username = ShowOriginal()
        password = HideField()
        ip = ShowOriginal()
        timestamp = FormatTime()
    
        def __init__(self, username, password, ip, timestamp):
            self.username = username
            self.password = password
            self.ip = ip
            self.timestamp = timestamp
    
        def serialize(self):
            return {
                "username": self.username,
                "password": self.password,
                "ip": self.ip,
                "timestamp": self.timestamp,
            }
            
       >>> le = LoginEvent(
       ...     "usr", "secret password", "127.0.0.1", datetime(2016, 7, 20, 15, 45)
       ... )
       >>> vars(le)
       {'username': 'usr', 'password': 'secret password', 'ip': '127.0.0.1', 
       'timestamp': datetime.datetime(2016, 7, 20, 15, 45)}
       >>> le.serialize()
       {'username': 'usr', 'password': '**redacted**', 'ip': '127.0.0.1', 
       'timestamp': '2016-07-20 15:45'}
       >>> le.password
       '**redacted**'
    serialize()メソッドを追加し、結果が表示される前に非表示にします.ただし、元の値を取得することもできます.値を設定すると、変換された値を保存し、インポート時にインポートできます.
    より簡単にLogineEventを実装
    
    class BaseEvent:
        """Abstract the serialization and the __init__"""
    
        def __init__(self, **kwargs):
            self.__dict__.update(kwargs)
    
        def serialize(self):
            return {
                attr: getattr(self, attr) for attr in self._fields_to_serialize()
            }
    
        def _fields_to_serialize(self):
            for attr_name, value in vars(self.__class__).items():
                if isinstance(value, BaseFieldTransformation):
                    yield attr_name
    
    
    class NewLoginEvent(BaseEvent):
        username = ShowOriginal()
        password = HideField()
        ip = ShowOriginal()
        timestamp = FormatTime()
    コードはより簡潔です.デフォルトクラスは共通メソッドのみを抽象化し、各イベントクラスはより小さく、より簡単です.
    クラスレコーダを使用する元の方法も良いですが、ディスクディスクディスクの立盤を使用する方法が良いです.