Sử dụng siêu dữ liệu
Tôi muốn giới thiệu Phương pháp # 2 , nhưng tốt hơn hết là bạn nên sử dụng siêu dữ liệu hơn là lớp cơ sở. Đây là một triển khai mẫu:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(object):
__metaclass__ = Singleton
Hoặc trong Python3
class Logger(metaclass=Singleton):
pass
Nếu bạn muốn chạy __init__
mỗi khi lớp được gọi, hãy thêm
else:
cls._instances[cls].__init__(*args, **kwargs)
để if
tuyên bố trong Singleton.__call__
.
Một vài lời về metaclass. Một siêu dữ liệu là lớp của một lớp ; đó là, một lớp là một thể hiện của siêu dữ liệu của nó . Bạn tìm siêu dữ liệu của một đối tượng trong Python với type(obj)
. Các lớp phong cách mới bình thường là loại type
. Logger
trong đoạn mã trên sẽ là kiểu class 'your_module.Singleton'
, giống như thể hiện (chỉ) của kiểu Logger
sẽ là kiểu class 'your_module.Logger'
. Khi bạn gọi logger với Logger()
, trước tiên Python hỏi siêu dữ liệu về Logger
, Singleton
phải làm gì, cho phép tạo cá thể được làm trống trước. Quá trình này giống như Python hỏi một lớp phải làm gì bằng cách gọi __getattr__
khi bạn tham chiếu một trong các thuộc tính của nó bằng cách thực hiện myclass.attribute
.
Một siêu dữ liệu về cơ bản quyết định định nghĩa của một lớp có nghĩa là gì và làm thế nào để thực hiện định nghĩa đó. Xem ví dụ http://code.activestate.com/recipes/498149/ , về cơ bản tái tạo các kiểu chữ C struct
trong Python bằng siêu dữ liệu. Chủ đề Một số trường hợp sử dụng (cụ thể) cho metaclass là gì? cũng cung cấp một số ví dụ, nhìn chung chúng dường như có liên quan đến lập trình khai báo, đặc biệt là được sử dụng trong ORM.
Trong tình huống này, nếu bạn sử dụng Phương thức # 2 của mình và một lớp con xác định một __new__
phương thức, nó sẽ được thực thi mỗi khi bạn gọi SubClassOfSingleton()
- bởi vì nó chịu trách nhiệm gọi phương thức trả về thể hiện được lưu trữ. Với một siêu dữ liệu, nó sẽ chỉ được gọi một lần , khi thể hiện duy nhất được tạo. Bạn muốn tùy chỉnh ý nghĩa của việc gọi lớp , được quyết định bởi loại của nó.
Nói chung, nó có ý nghĩa để sử dụng một siêu dữ liệu để thực hiện một singleton. Một singleton là đặc biệt bởi vì chỉ được tạo một lần và siêu dữ liệu là cách bạn tùy chỉnh việc tạo một lớp . Sử dụng siêu dữ liệu cho phép bạn kiểm soát nhiều hơn trong trường hợp bạn cần tùy chỉnh các định nghĩa lớp singleton theo các cách khác.
Các singletons của bạn sẽ không cần nhiều kế thừa (vì siêu dữ liệu không phải là lớp cơ sở), nhưng đối với các lớp con của lớp được tạo sử dụng nhiều kế thừa, bạn cần đảm bảo rằng lớp singleton là lớp đầu tiên / ngoài cùng với siêu lớp xác định lại __call__
Điều này rất khó có thể là một vấn đề. Dict thể hiện không nằm trong không gian tên của cá thể nên nó sẽ không vô tình ghi đè lên nó.
Bạn cũng sẽ nghe rằng mô hình đơn lẻ vi phạm "Nguyên tắc trách nhiệm duy nhất" - mỗi lớp chỉ nên làm một việc . Bằng cách đó, bạn không phải lo lắng về việc làm xáo trộn một điều mà mã làm nếu bạn cần thay đổi một thứ khác, bởi vì chúng riêng biệt và được gói gọn. Việc thực hiện siêu dữ liệu vượt qua bài kiểm tra này . Siêu dữ liệu chịu trách nhiệm thực thi mẫu và lớp được tạo và các lớp con không cần phải biết rằng chúng là singletons . Phương thức # 1 thất bại trong bài kiểm tra này, như bạn đã lưu ý với "Bản thân MyClass là một hàm aa, không phải là một lớp, vì vậy bạn không thể gọi các phương thức lớp từ nó."
Phiên bản tương thích Python 2 và 3
Viết một cái gì đó hoạt động trong cả Python2 và 3 yêu cầu sử dụng sơ đồ phức tạp hơn một chút. Vì siêu dữ liệu thường là các lớp con của loại type
, nên có thể sử dụng một lớp để tự động tạo một lớp cơ sở trung gian trong thời gian chạy với nó như là siêu dữ liệu của nó và sau đó sử dụng nó làm Singleton
lớp cơ sở của lớp cơ sở công cộng . Thật khó để giải thích hơn là làm, như minh họa tiếp theo:
# works in Python 2 & 3
class _Singleton(type):
""" A metaclass that creates a Singleton base class when called. """
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Singleton(_Singleton('SingletonMeta', (object,), {})): pass
class Logger(Singleton):
pass
Một khía cạnh mỉa mai của phương pháp này là nó sử dụng phân lớp để thực hiện siêu dữ liệu. Một lợi thế có thể là, không giống như với một siêu dữ liệu thuần túy, isinstance(inst, Singleton)
sẽ trở lại True
.
Đính chính
Ở một chủ đề khác, có lẽ bạn đã nhận thấy điều này, nhưng việc triển khai lớp cơ sở trong bài viết gốc của bạn là sai. _instances
cần được tham chiếu trên lớp , bạn cần sử dụng super()
hoặc bạn đang đệ quy và __new__
thực sự là một phương thức tĩnh mà bạn phải truyền lớp đó , không phải là một phương thức lớp, vì lớp thực tế chưa được tạo khi nó được gọi là. Tất cả những điều này cũng sẽ đúng cho việc triển khai siêu dữ liệu.
class Singleton(object):
_instances = {}
def __new__(class_, *args, **kwargs):
if class_ not in class_._instances:
class_._instances[class_] = super(Singleton, class_).__new__(class_, *args, **kwargs)
return class_._instances[class_]
class MyClass(Singleton):
pass
c = MyClass()
Trang trí trở lại một lớp học
Ban đầu tôi đã viết một bình luận nhưng nó quá dài, vì vậy tôi sẽ thêm nó vào đây. Phương pháp số 4 tốt hơn so với phiên bản trang trí khác, nhưng nó có nhiều mã hơn mức cần thiết cho một người độc thân và không rõ ràng về những gì nó làm.
Các vấn đề chính xuất phát từ lớp là lớp cơ sở của riêng nó. Đầu tiên, không có gì lạ khi có một lớp là một lớp con của một lớp gần giống với cùng tên chỉ tồn tại trong __class__
thuộc tính của nó ? Điều này cũng có nghĩa là bạn không thể xác định bất kỳ phương pháp mà gọi phương thức cùng tên trên lớp cơ sở của họ với super()
bởi vì họ sẽ recurse. Điều này có nghĩa là lớp của bạn không thể tùy chỉnh __new__
và không thể xuất phát từ bất kỳ lớp nào cần __init__
gọi chúng.
Khi nào nên sử dụng mẫu singleton
Trường hợp sử dụng của bạn là một trong những ví dụ tốt hơn về việc muốn sử dụng một singleton. Bạn nói trong một trong những bình luận "Đối với tôi, việc đăng nhập dường như luôn là một ứng cử viên tự nhiên cho Singletons." Bạn hoàn toàn đúng .
Khi mọi người nói singletons là xấu, lý do phổ biến nhất là họ ở trạng thái chia sẻ ngầm . Trong khi với các biến toàn cục và nhập mô-đun cấp cao nhất là trạng thái chia sẻ rõ ràng , các đối tượng khác được truyền xung quanh thường được khởi tạo. Đây là một điểm tốt, với hai ngoại lệ .
Điều đầu tiên, và một điều được nhắc đến ở nhiều nơi khác nhau, là khi các singletons không đổi . Việc sử dụng các hằng số toàn cầu, đặc biệt là enums, được chấp nhận rộng rãi và được coi là lành mạnh bởi vì dù thế nào, không ai trong số người dùng có thể gây rối cho bất kỳ người dùng nào khác . Điều này cũng đúng cho một singleton liên tục.
Ngoại lệ thứ hai, được nhắc đến ít hơn, thì ngược lại - khi singleton chỉ là phần chìm dữ liệu , không phải là nguồn dữ liệu (trực tiếp hoặc gián tiếp). Đây là lý do tại sao logger cảm thấy như một cách sử dụng "tự nhiên" cho singletons. Vì những người dùng khác nhau không thay đổi logger theo cách mà những người dùng khác sẽ quan tâm, nên không có trạng thái chia sẻ thực sự . Điều này phủ nhận đối số chính chống lại mẫu đơn và làm cho chúng trở thành một lựa chọn hợp lý vì dễ sử dụng cho nhiệm vụ.
Đây là một trích dẫn từ http://googletesting.blogspot.com/2008/08/root-cause-of-singletons.html :
Bây giờ, có một loại Singleton là OK. Đó là một singleton nơi tất cả các đối tượng có thể tiếp cận là bất biến. Nếu tất cả các đối tượng là bất biến hơn Singleton không có trạng thái toàn cầu, vì mọi thứ đều không đổi. Nhưng thật dễ dàng để biến loại đơn này thành một loại đột biến, nó rất trơn trượt. Do đó, tôi cũng chống lại những Singletons này, không phải vì họ xấu, mà vì họ rất dễ xấu. (Như một lưu ý phụ, bảng liệt kê Java chỉ là những loại singletons. Miễn là bạn không đặt trạng thái vào bảng liệt kê của mình, bạn vẫn ổn, vì vậy xin đừng.)
Một loại Singletons khác, có thể chấp nhận được là những loại không ảnh hưởng đến việc thực thi mã của bạn, Chúng không có "tác dụng phụ". Ghi nhật ký là ví dụ hoàn hảo. Nó được tải với Singletons và nhà nước toàn cầu. Có thể chấp nhận được (vì nó sẽ không làm tổn thương bạn) vì ứng dụng của bạn không hoạt động khác nhau cho dù một trình ghi nhật ký đã cho phép hay chưa. Thông tin ở đây chảy theo một cách: Từ ứng dụng của bạn vào bộ ghi chép. Ngay cả logger nghĩ là trạng thái toàn cầu vì không có thông tin nào chuyển từ logger vào ứng dụng của bạn, logger có thể được chấp nhận. Bạn vẫn nên tiêm logger nếu bạn muốn kiểm tra của mình để xác nhận rằng một cái gì đó đang được ghi lại, nhưng nói chung Loggers không có hại mặc dù có đầy đủ trạng thái.
foo.x
hoặc nếu bạn nhấn mạnhFoo.x
thay vìFoo().x
); sử dụng các thuộc tính lớp và các phương thức tĩnh / lớp (Foo.x
).