2012-05-25 13 views
12

Tôi có đoạn code python sau:Metaclasses Python: Tại sao __setattr__ không được gọi cho các thuộc tính được đặt trong khi định nghĩa lớp?

class FooMeta(type): 
    def __setattr__(self, name, value): 
     print name, value 
     return super(FooMeta, self).__setattr__(name, value) 

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 
    def a(self): 
     pass 

tôi dự kiến ​​sẽ có __setattr__ của lớp meta được gọi cho cả FOOa. Tuy nhiên, nó không được gọi là gì cả. Khi tôi chỉ định thứ gì đó cho Foo.whatever sau khi lớp học đã được xác định, phương thức được gọi.

Lý do cho hành vi này và có cách nào để chặn các bài tập xảy ra trong quá trình tạo lớp học không? Sử dụng attrs trong __new__ sẽ không hoạt động kể từ khi tôi muốn check if a method is being redefined.

+0

vấn đề chính xác này là lý do tại sao cú pháp metaclass đã thay đổi trong py3! – SingleNegationElimination

Trả lời

20

Một khối lớp là khoảng cú pháp đường để xây dựng một từ điển, và sau đó gọi một metaclass để xây dựng đối tượng lớp.

này:

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 
    def a(self): 
     pass 

Comes ra khá nhiều, nếu như bạn đã viết:

d = {} 
d['__metaclass__'] = FooMeta 
d['FOO'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = d.get('__metaclass__', type)('Foo', (object,), d) 

Chỉ nếu không có sự ô nhiễm không gian tên (và trong thực tế cũng có một tìm kiếm thông qua tất cả các căn cứ để xác định metaclass, hoặc cho dù có một cuộc xung đột metaclass, nhưng tôi bỏ qua điều đó ở đây).

Các metaclass' __setattr__ có thể kiểm soát những gì xảy ra khi bạn cố gắng thiết lập một thuộc tính trên một trong các trường hợp của nó (đối tượng lớp), nhưng bên trong khối lớp bạn không làm điều đó, bạn đang chèn vào một đối tượng từ điển, do đó, lớp dict kiểm soát những gì đang xảy ra, chứ không phải metaclass của bạn. Vì vậy, bạn đang ra khỏi may mắn.


Trừ khi bạn đang sử dụng Python 3.x! Trong Python 3.x bạn có thể định nghĩa một lớp học __prepare__ classmethod (hoặc staticmethod) trên metaclass, điều khiển đối tượng nào được sử dụng để tích lũy các thuộc tính được đặt trong một khối lớp trước khi chúng được truyền cho hàm tạo metaclass. Giá trị mặc định __prepare__ chỉ đơn giản là trả về một từ điển thông thường, nhưng bạn có thể xây dựng một lớp dict giống như tùy chỉnh mà không cho phép các phím được định nghĩa lại, và sử dụng để tích lũy thuộc tính của bạn:

from collections import MutableMapping 


class SingleAssignDict(MutableMapping): 
    def __init__(self, *args, **kwargs): 
     self._d = dict(*args, **kwargs) 

    def __getitem__(self, key): 
     return self._d[key] 

    def __setitem__(self, key, value): 
     if key in self._d: 
      raise ValueError(
       'Key {!r} already exists in SingleAssignDict'.format(key) 
      ) 
     else: 
      self._d[key] = value 

    def __delitem__(self, key): 
     del self._d[key] 

    def __iter__(self): 
     return iter(self._d) 

    def __len__(self): 
     return len(self._d) 

    def __contains__(self, key): 
     return key in self._d 

    def __repr__(self): 
     return '{}({!r})'.format(type(self).__name__, self._d) 


class RedefBlocker(type): 
    @classmethod 
    def __prepare__(metacls, name, bases, **kwargs): 
     return SingleAssignDict() 

    def __new__(metacls, name, bases, sad): 
     return super().__new__(metacls, name, bases, dict(sad)) 


class Okay(metaclass=RedefBlocker): 
    a = 1 
    b = 2 


class Boom(metaclass=RedefBlocker): 
    a = 1 
    b = 2 
    a = 3 

Chạy này mang lại cho tôi:

Traceback (most recent call last): 
    File "/tmp/redef.py", line 50, in <module> 
    class Boom(metaclass=RedefBlocker): 
    File "/tmp/redef.py", line 53, in Boom 
    a = 3 
    File "/tmp/redef.py", line 15, in __setitem__ 
    'Key {!r} already exists in SingleAssignDict'.format(key) 
ValueError: Key 'a' already exists in SingleAssignDict 

một số lưu ý:

  1. __prepare__ có phải là một classmethod hoặc staticmethod, bởi vì nó được gọi là trước ngày e metaclass 'instance (class của bạn) tồn tại.
  2. type vẫn cần tham số thứ ba của nó là một thực dict, vì vậy bạn cần phải có một phương pháp __new__ có thể chuyển đổi các SingleAssignDict để một bình thường một
  3. tôi có thể subclassed dict, mà có lẽ có thể tránh được (2), nhưng Tôi thực sự không thích làm điều đó bởi vì các phương pháp phi cơ bản như update không tôn trọng phần ghi đè của bạn về các phương thức cơ bản như __setitem__. Vì vậy, tôi thích phân lớp collections.MutableMapping và bọc từ điển.
  4. Đối tượng Okay.__dict__ thực tế là từ điển thông thường, vì nó được đặt bởi typetype là khó tính về loại từ điển mà nó muốn. Điều này có nghĩa là ghi đè lên các thuộc tính lớp sau khi tạo lớp không làm tăng ngoại lệ. Bạn có thể ghi đè thuộc tính __dict__ sau khi gọi superclass trong __new__ nếu bạn muốn duy trì không ghi đè do từ điển của đối tượng lớp.

Đáng buồn là kỹ thuật này không có sẵn bằng Python 2.x (tôi đã kiểm tra). Phương thức __prepare__ không được gọi, có ý nghĩa như trong Python 2.x metaclass được xác định bởi thuộc tính ma thuật __metaclass__ thay vì một từ khóa đặc biệt trong khoanh vùng; có nghĩa là đối tượng dict được sử dụng để tích lũy các thuộc tính cho khối lớp đã tồn tại vào thời điểm metaclass được biết.

Hãy so sánh Python 2:

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 
    def a(self): 
     pass 

Là tương đương với:

d = {} 
d['__metaclass__'] = FooMeta 
d['FOO'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = d.get('__metaclass__', type)('Foo', (object,), d) 

Trường hợp metaclass để gọi được xác định từ vào từ điển, so với Python 3:

class Foo(metaclass=FooMeta): 
    FOO = 123 
    def a(self): 
     pass 

Being tương đương với:

d = FooMeta.__prepare__('Foo',()) 
d['Foo'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = FooMeta('Foo',(), d) 

Từ điển sử dụng được xác định từ metaclass.

2

Thuộc tính lớp được chuyển đến metaclass dưới dạng một từ điển duy nhất và giả thuyết của tôi là điều này được sử dụng để cập nhật thuộc tính __dict__ của tất cả cùng một lúc, ví dụ: giống như cls.__dict__.update(dct) thay vì thực hiện setattr() trên mỗi mục. Hơn nữa, tất cả được xử lý trong C-land và đơn giản là không được viết để gọi một tùy chỉnh __setattr__(). Bạn có thể dễ dàng làm bất cứ điều gì bạn muốn với các thuộc tính của lớp trong phương thức __init__() của metaclass, vì bạn đã vượt qua không gian tên lớp như là dict, vì vậy hãy thực hiện điều đó.

7

Không có bài tập nào xảy ra trong quá trình tạo lớp học. Hoặc: chúng đang xảy ra, nhưng không phải trong bối cảnh bạn nghĩ là chúng. Tất cả các thuộc tính lớp được thu thập từ phạm vi lớp học cơ thể và truyền cho metaclass' __new__, như là đối số cuối cùng:

class FooMeta(type): 
    def __new__(self, name, bases, attrs): 
     print attrs 
     return type.__new__(self, name, bases, attrs) 

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 

Lý do: khi mã trong cơ thể lớp thực hiện, không có lớp được nêu ra. Điều này có nghĩa là không có cơ hội để metaclass chặn mọi thứ nhưng.

0

Trong khi tạo lớp, không gian tên của bạn được đánh giá thành một dict và được chuyển làm đối số cho metaclass, cùng với tên lớp và lớp cơ sở. Do đó, việc gán một thuộc tính lớp bên trong định nghĩa lớp sẽ không hoạt động theo cách bạn mong đợi. Nó không tạo ra một lớp trống và gán mọi thứ. Bạn cũng không thể có các khóa trùng lặp trong một dict, vì vậy trong các thuộc tính tạo lớp đã được loại bỏ trùng lặp. Chỉ bằng cách đặt thuộc tính sau định nghĩa lớp, bạn có thể kích hoạt __setattr__ tùy chỉnh của mình.

Vì không gian tên là dict, không có cách nào để bạn kiểm tra các phương thức trùng lặp, như được đề xuất bởi câu hỏi khác của bạn. Cách thực tế duy nhất để làm điều đó là phân tích cú pháp mã nguồn.