Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Mở đầu

Nếu bạn đã từng sử dụng Objective C thì thấy rằng khi khai báo các property cho 1 class nào đấy chúng ta có 2 cách như sau:

1
2
3
@interface MyClass : NSObject {
    NSString *myString;
}

hoặc có thể dùng @property (attributes) type name để khai báo như sau:

1
2
3
@interface MyClass : NSObject {
}
@property (strong, nonatomic) NSString *myString;

Với cách thứ 2 thì compiler sẽ tự động sinh ra các setter/getter cho property ấy. Thế nhưng việc sinh ra setter/getter như thế nào là phụ thuộc vào tập attributes mà bạn đã set ở trên. Khi mới bắt đầu code iOS mình thấy việc set thuộc tính này hơi bị loạn với khá nhiều thuộc tính (retain, strong, weak, unsafe_unretained, nonatomic…). Rồi khi phiên bản thay đổi, kiểu project có dùng ARC hay không cũng dẫn đến việc sử dụng các thuộc tính này cũng khác nhau. Ngoài ra trong một số trường hợp nếu bạn không sử dụng đúng thuộc tính có thể làm app của bạn chạy bị lỗi. Trong bài viết này mình sẽ tóm tắt lại các thuộc tính của property, cũng như nói về khi nào sẽ dùng thuộc tính nào, tại sao, và thuộc tính nào là mặc định.

Các thuộc tính của property

Nếu chia nhóm thì có lẽ bao gồm 3 nhóm thuộc tính như sau:

Writability

Nhóm này có 2 thuộc tính là readwritereadonly. Nhóm thuộc tính này thì khá là dễ hiểu. Với thuộc tính readwrite thì compiler sẽ generate ra cả setter và getter, còn readonly thì compiler chỉ generate ra getter. Mặc định là readwrite (không liên quan đến project dùng ARC hay không).

Setter Semantics

Nhóm này gồm các thuộc tính để chỉ ra cách thức quản lý bộ nhớ, bao gồm các thuộc tính như sau: assign, strong, weak, unsafe_unretained, retain, copy. Khi chúng ta set một trong các thuộc tính này cho property thì setter (getter không liên quan) được tạo ra thay đổi tương ứng với thuộc tính đó. Trước hết chúng ta sẽ nói qua về cách quản lý bộ nhớ trước iOS5 khi mà ARC chưa xuất hiện.

1
2
3
Car *car1 = [[Car alloc] init];
//...
[car1 release]

Trước khi ARC xuất hiện thì các lập trình viên iOS đều phải tự quản lý bộ nhớ. Khi chúng ta tạo object với vùng nhớ của nó, đồng nghĩa với việc chúng ta nắm giữ ownership của object đó. Khi không cần dùng nữa thì phải huỷ bỏ ownership đấy đi bằng cách gửi message release. Một object có thể có nhiều ownership và mỗi object sẽ có 1 property tên là retainCount để lưu số lượng owner của nó. Mỗi khi chúng ta tạo object, hay retain thì retainCount lại được tăng lên 1. Khi chúng ta gửi message release tới object đấy thì retainCount lại bị giảm đi 1. Một khi retainCount bằng 0 thì vùng nhớ của nó sẽ bị giải phóng. Chúng ta có thể gửi message retain để tạo thêm ownership như ví dụ dưới đây. Khi đó car1car2 cùng trỏ đến 1 vùng nhớ và retainCount bây giờ bằng 2.

1
2
// retain
Car *car2 = [car1 retain];  // retainCount = 2

Ngoài ra để copy sang vùng nhớ mới chúng ta có thể gửi message copy như ví dụ dưới đây. Khi đó retainCount ở vùng nhớ mới có giá trị khởi tạo là 1.

1
2
// copy
Car *car3 = [car1 copy];    // retainCount = 1

Quay trở lại với thuộc tính của property. Thuộc tính đầu tiên là retain. Như ví dụ dưới đây khi ta set thuộc tính retain cho property name thì compiler sẽ sinh ra setter setName như bên dưới.

1
2
3
4
5
@interface Car: NSObject

@property (nonatomic, retain) NSString *name;

@end;
1
2
3
4
5
- (void)setName:(NSString *)newName {
    [newName retain];
    [_name release];
    _name = newName;
}

Nhìn vào setter ta thấy đầu tiên là tạo ownership (hay tăng retainCount thêm 1) của newName bằng cách gọi [newNmane retain]. Tiếp theo là việc gửi message release tới _name ban đầu để xoá ownership ban đầu đi. Sau đó mới gán contrỏ trỏ đến object mới. Vậy nên thuộc tính retain giúp tạo ra setter trong đó tạo ownership mới và trỏ đến vùng nhớ mới. Chú ý rằng thuộc tính retain chỉ dùng cho những project không dùng ARC.

Và từ iOS5 trở đi Apple giới thiệu ARC giúp cho việc quản lý bộ nhớ đơn giản hơn. ARC không hoạt động như các Garbage Collection khác mà thực ra chỉ là phần front-end của compiler nhằm mục đich tự động chèn thêm các đoạn code gọi message như retain hay release. Từ đấy lập trình viên không phải gọi các message này nữa. Ví dụ như 1 object được tạo trong 1 method thì sẽ chèn thêm đoạn gửi message release tới object đó ở gần cuối method. Hay trong trường hợp là property của 1 class Car ở trên thì tự động chèn [_name release] trong method dealloc của class Car chẳng hạn.

1
2
3
4
5
6
- (void)dealloc
{
  //...
  [_name release];
  //...
}

Khi project của bạn dùng ARC thì chúng ta sẽ dùng thuộc tính strong thay cho thuộc tính retain. strong cũng tương tự như retain sẽ giúp tạo ra setter, mà trong setter đó tạo ra ownership mới (tăng retainCount thêm 1). Và ngoài ra ARC sẽ thêm các đoạn gửi message release tới các property này trong method dealloc của class.

Thế nhưng xuất hiện vấn đề có tên là Strong Reference Cycles. Mình sẽ lấy 1 ví dụ để thấy rõ hơn về vấn đề này. Một object A nào đấy có ownership của 1 object B. Object B lại có ownership của 1 object C. Object C lại có ownership của object B.

Một khi object A ko cần thiết nữa thì trong method dealloc của A sẽ gửi message release tới object B. retainCount của object B giảm đi 1 nhưng vẫn còn 1 ( do object C retain ) thế nên method dealloc của object B không bao giờ được gọi, kéo theo message release cũng không bao giờ được gửi tới object C. Từ đó dẫn đến vùng nhớ của object B và object C không được giải phóng => xuất hiện hiện tượng Leak Memory. Vì vậy để tránh hiện tượng này ta sẽ dùng thuộc tính weak thay vì dùng thuộc tính strong trong class của object C. Với thuộc tính weak thì trong setter được sinh ra sẽ không retain (không tăng retainCount thêm 1) mà chỉ đơn thuần gán con trỏ trỏ đến vùng nhớ mới. Thuộc tính weak cũng chỉ dùng trong trường hợp bạn đang dùng ARC. Và một cái hay của weak nữa là khi vùng nhớ bị giải phóng thì con trỏ được set bằng nil. Mà trong Objective C thì gửi message đến nil sẽ không vấn đề gì, app của bạn không bị crash. Điển hình nhất của việc dùng thuộc tính weak đó là cho các delegate, datasource.

Tuy nhiên vẫn còn một vài class như NSTextView, NSFont, NSColorSpace chưa hỗ trợ khai báo thuộc tính weak nên với những class này bạn có thể dùng thuộc tính unsafe_unretained thay cho weak. Thế nhưng chú ý 1 điều rằng sau khi vùng nhớ nó trỏ tới bị xoá thì con trỏ không được set la nil.

Tiếp theo là thuộc tính copy. Với việc thiết lập thuộc tính này compiller sẽ tạo ra setter như sau:

1
2
3
4
5
@interface Car: NSObject

@property (nonatomic, copy) NSString *name;

@end;
1
2
3
4
- (void)setName:(NSString *)newName {
    [_name release];
    _name = [newName copy];     // retainCount = 1
}

Như ở trên ta thấy 1 vùng nhớ mới được copy ra và _name giờ chiếm giữ 1 ownership của vùng nhớ đó. Tại sao chúng ta không dùng strong ở đây mà lại dùng copy. Giả sử ở trên chúng ta dùng thuộc tính strong và xem qua 2 ví dụ dưới đây.

1
2
3
NSString *name1 = @"Toyota";
car1.name = name1;
name1 = @"Honda";

Trong trường hợp này car1.name vẫn có giá trị là “Toyota” và name1 giờ chuyển thành “Honda”. Hoàn toàn không có vấn đề gì. Thế nhưng trong ví dụ thứ 2 dưới đây thay vì dùng NSString mà dùng subclass của nó là NSMutableString.

1
2
3
NSMutableString *name1 = @"Toyota";
car1.name = name1;
[name1 appendString:"2"];

Trong trường hợp này giá trị của car1.name là “Toyota2” mặc dù ban đầu chúng ta set là “Toyota”. Vì vậy mặc dù property name trong class Car với kiểu NSString nhưng nếu dùng strong giá trị của name vẫn có thể bị append như trên. Để tránh những trường hợp như thế ta dùng copy để mỗi lần gán sẽ copy 1 vùng nhớ mới tránh được những trường hợp như trên. Đối với những class có subclass là Mutable... thì chúng ta nên chú ý dùng thuộc tính copy. Ngoài ra block cũng phải dùng copy.

Thuộc tính cuối cùng trong nhóm này là assign thì dùng cho các property kiểu không phải là object. Tức là các kiểu dữ liệu như int, NSInteger, float,…

Với nhóm thuộc tính này thì strong là thuộc tính mặc định trong trường hợp dùng ARC, còn retain là thuộc tính mặc định trong trường hợp không dùng ARC.

Atomicity

Nhóm thuộc tính này bao gồm 2 thuộc tính là atomicnonatomic. Thuộc tính mặc định là atomic. Nhóm thuộc tính này liên quan đến vấn đề multithread. Chưa bàn đến atomic hay nonatomic, mà chúng ta cùng xem ví dụ sau:

1
2
3
4
5
6
@interface MyView {
}

@property CGPoint center;

@end

khi đấy chúng ta có setter/getter như sau:

1
2
3
4
5
6
7
- (CGPoint) center {
  return _center;
}

- (void)setCenter:(CGPoint)newCenter {
  _center = newCenter;
}

và bởi vì struct CGPoint có 2 thành phần CGFloat x, CGFloat y nên thực ra setter sẽ thực hiện các bước như sau:

1
2
3
4
- (void)setCenter:(CGPoint)newCenter {
  _center.x = newCenter.x;
  _center.y = newCenter.y;
}

Trong trường hợp chúng ta chạy multithread thì có thể xảy ra khả năng như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// giả sủ ban đầu center của myView là (-5.f, -8.f)

// thread 1 gọi setter
[myView setCenter:CGPointMake(1.f, 2.f)];

// tiep theo bên trong setCenter sẽ chạy
_center.x = newCenter.x; // _center.x giờ có giá trị là 1.f và _center.y vẫn giữ giá trị là -8.f

// chưa kịp chạy lệnh tiếp theo để set _center.y thì ở thread 2 gọi getter
CGPoint point = [myView center];
// và getter chạy trả về (1.f, -8.f)

// thread 1 tiếp tục giá trị cho y
_center.y = newCenter.y // _center.y giờ là  2.f

Như trường hợp ở trên ta thấy giá trị center là (1.f, 2.f) nhưng tại thread 2 giá trị lấy được lại là (1.f, -8.f) dẫn đến kết quả không được như mong muốn. Vì vậy trong trường hợp multithread để tránh những tình huống như trên ta set thuộc tính atomic cho property. Khi đấy compiler sẽ sinh ra các setter/getter như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (CGPoint) center {
  CGPoint curCenter;
  @synchronized(self) {
    curCenter = _center;
  }
  return curCenter;
}

- (void)setCenter:(CGPoint)newCenter {
  @synchronized(self) {
    _center = newCenter;
  }
}

Bên trong setter/getter sử dụng lock để tránh việc nhiều thread truy cập đồng thời. Thế nhưng việc dùng lock sẽ mất chi phí cũng như cản trở tốc độ của chương trình. Vì vậy nên trong trường hợp bạn không dùng multithread hoặc không thể xảy ra những vấn đề như trên thì bạn nên dùng thuộc tính nonatomic để tăng tốc độ cho chương trình.

Tổng kết

Bài viết này mình đã trình bày về các thuộc tính cho property, giải thích qua về các thuộc tính cũng như khi nào nên dùng thuộc tính nào. Mặc dù mình vẫn thấy còn những lập trình viên không dùng ARC nhưng có lẽ đa số mọi người đã chuyển qua dùng ARC. Thế nên thuộc tính retain có thể không cần dùng nữa. Để tìm hiểu kĩ hơn các bạn có thể đọc tại Programming With Objective C

Comments

Là một web developer nói chung và ruby on rails developer nói riêng, bạn chắc hẳn sẽ gặp nhiều khó khăn khi muốn thêm chức năng mới vào hệ thống hiện tại. Khó khăn đáng nói đến nhất bao gồm: bạn phải add sao cho chức năng mới được add vào sẽ dễ extend, dễ tháo bỏ khi không cần thiết, và việc add chức năng mới vào sẽ có ảnh hưởng tối thiểu nhất đến các chức năng đã có. Trong bài viết này, mình sẽ giới thiệu về chanko, một (framework/engine) trên ruby on rails, mà sẽ giúp cho việc tạo chức năng mới trên một app đã có cực kì clean và dễ dàng.

1. Cài đặt

Để cài đặt chanko thì chúng ta chỉ cần add chanko vào Gemfile:
1
gem "chanko"

2. Sử dụng

Đầu tiên chúng ta sẽ nói về ý tưởng của chanko. Chanko tách chức năng mới với app hiện tại thông qua việc tạo ra một folder trong /app/unit. Trong đó sẽ chứa các chức năng được tạo mới thông qua chanko. Việc này có thể được nhìn thấy dễ dàng khi chúng ta sử dụng chanko generator.

1
2
3
4
5
6
7
8
9
10
$ rails generate chanko:unit example_unit
      create  app/units/example_unit
      create  app/units/example_unit/example_unit.rb
      create  app/units/example_unit/views/.gitkeep
      create  app/units/example_unit/images/.gitkeep
      create  app/units/example_unit/javascripts/.gitkeep
      create  app/units/example_unit/stylesheets/.gitkeep
      create  app/assets/images/units/example_unit
      create  app/assets/javascripts/units/example_unit
      create  app/assets/stylesheets/units/example_unit

Chúng ta có thể thấy chanko generator gần tương tự như scaffold generator của rails. unit ở đây là một đơn vị chức năng.

Ví dụ khi bạn cần add chức năng search button thì bạn sẽ generate chanko:unit add_search_button, khi đó chanko sẽ tự động tạo folder add_search_button ở trong app/units, và tạo sẵn file add_search_button_unit.rb và thư mục view để chứa view của chức năng mới này. File add_search_button.rb này sẽ chứa logic của cả model/controller của chức năng add_search_button mà chúng ta cần thêm vào.

Các bạn có thể thấy rõ ý tưởng của chanko là tách logic và cả asset của chức năng mới cần thêm vào càng tách biệt với các chức năng cũ càng tốt. Việc này có tác dụng là chúng ta có thể thêm, bớt chức năng vào hệ thống cũ bằng một flow rất clean , và độ ảnh hưởng với hệ thống cũ cực kì thấp. Vậy nếu tách unit mới ra dưới dạng gần như một thư viện riêng như vậy, chúng ta sẽ intergrate unit này vào rails ra sao?

Việc intergrate unit được tạo ra bởi chanko vào rails được thể hiện qua các chức năng dưới đây:

2.1 Invoke

1
2
3
4
5
6
7
8
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    invoke(:add_search_button_unit, :index) do
      @users = User.all
    end
  end
end
1
2
3
4
5
6
module AddSearchButtonUnit
  include Chanko::Unit
  function (:index) do
    @users = Users.unit.active
  end
end

Hàm invoke này sẽ đưa logic của hàm index được định nghĩa trong add_search_button_unit.rb vào trong logic của hàm hiện tại. Block được pass vào hàm invoke sẽ là fallback function, được execute khi có lỗi hày có vấn đề gì xảy ra với hàm invoke. Chúng ta có thể hình dung đơn giản chức năng invoke dùng để extend logic của một hàm của một unit (chức năng mới) và logic của controller (chức năng cũ)

2.2 Unit module

Unit module chính là module của chức năng mới được thêm vào, ở đây chính là module AddSearchButton mà chúng ta đã nói đến ở trên. Trong module này sẽ định nghĩa logic cho controller, model và cả view helper cho chức năng mới. Tất cả MVC logic đều được nhét vào 1 file có thể hơi khó nhìn khi chức năng của chúng ta có nhiều logic phức tạp , tuy nhiên khi dừng lại ở mức prototyping thì việc này có thể chấp nhận được.

Logic của controller được add vào thông qua hàm scope(:controller)
1
2
3
4
5
6
7
8
9
scope(:controller) do
  function(:show) do
    @user = User.find(params[:id])
  end

  function(:index) do
    @users = User.active
  end
end

Gần tương tự, Logic của của model sẽ được thực hiện thông qua hàm model. Một điểm hơi khác là trong block pass vào thì chúng ta phải extend model mà chúng ta muốn thêm chức năng vào. Một điều đặc biệt ở đây là các hàm được extend cho một model X sẽ không được add trực tiếp vào model X thông qua monkey patch, mà sẽ add gián tiếp thông qua một proxy tên là unit. Do đó giả sử chúng ta một thêm hàm method vào model X thì chúng ta sẽ gọi nó thông qua X.unit.method. Như ở ví dụ dưới đây thì hàm active? sẽ được gọi thông qua user.unit.active?

1
2
3
4
5
6
7
8
9
models do
  expand(:User) do
    scope :active, lambda { where(:deleted_at => nil) }

    def active?
      deleted_at.nil?
    end
  end
end

Logic của view được thực hiện thêm vào thông qua hàm scope(:view) và qua file view được add vào thư mục /units/unit_name/views (file view này sẽ có extension là slim)

1
2
3
4
5
scope(:view) do
  function(:active) do
    render '/active' if user.unit.active?
  end
end

Hàm view này sẽ render view active.html.slim nằm trong app/units/unit_name/views

Ngoài việc add logic của unit vào controller/model/view thông qua các hàm scope và model như đã giới thiệu ở trên. Chanko::Unit cung cấp cho chúng ta một hàm rất hữu ích là active_if. Hàm này giống như một dạng functionality toggle, giúp chúng ta có thể on/off một chức năng mới cực kì dễ dàng. Block được pass vào active_if sẽ quyêt định chức unit có được enable không, nếu không được enable thì tất cả các logic của unit sẽ không được execute.

1
2
3
active_if do |context, options|
  true
end

3. Kết luận

Qua bài viết này chúng ta đã biết cách sử dụng gem chanko để có thể prototype chức năng mới một cách dễ dàng hơn, và ít ảnh hưởng đến hệ thống cũ nhất. Một cách đơn giản thì chanko đưa logic của cả model/view/controller vào tập trung trong 1 file, và cung cấp các helper function để giúp logic của hệ thống cũ có thể invoke các chức năng của unit mới một cách đơn giản nhất.

Các bạn có thể tham khảo chi tiết thông qua homepage của chanko

Comments

Trong các bài viết trước, chúng tôi đã giới thiệu về các kiến thức cơ bản trong python, như object trong python, decorators.

Bài viết này sẽ giới thiệu một kỹ nâng cao trong Python, đó là descriptor

1. Ví dụ về descriptor

Xét ví dụ khi chúng ta muốn xây dựng mô hình cho bài toán về các lập trình viên

programmer.py
1
2
3
4
5
6
class Programmer(object):
    def __init__(self, name, age, salary, rating):
        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating

Giờ nếu bạn muốn thêm một điều kiện là tuổi của lập trình viên phải luôn lớn hơn 0, bạn có thể cài đặt như sau

programmer.py
1
2
3
4
5
6
7
8
9
10
class Programmer(object):
    def __init__(self, name, age, salary, rating):
        self.name = name
        self.salary = salary
        self.rating = rating

        if age > 0:
            self.age = age
        else:
            raise ValueError("Negative value not allowed: %s" % age)

Tuy nhiên với cách làm này, bạn vẫn có thể làm cho age < 0, nếu gán giá trị của age trực tiếp từ instance của Programmer

1
2
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> kiennt.age = -10

May mắn thay, ta có thể sử dụng property để giải quyết vấn đề này

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Programmer(object):
    def __init__(self, name, age, salary, rating):
        self._age = None # tạo một thuộc tính private cho age

        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if age > 0:
            self._age = value
        else:
            raise ValueError("Negative value not allowed: %s" % age)
python
1
2
3
4
5
6
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> try:
        kiennt.age = -10
    except ValueError:
        print "Cannot set negative value"
Cannot set negative value

Cách chúng ta làm ở đây đó là tạo ra một biến private _age để chứa giá trị thật của age. Và sử dụng @getter@setter để bind thuộc tính age với 2 method. Trong 2 method này, chúng ta sẽ cài đặt logic cho việc gán trị của age. Khi chúng ta gọi kiennt.age = value, python sẽ tự động gọi đến setter của age, còn nếu chỉ gọi kiennt.age (không có gán giá trị), thì getter sẽ được gọi.

2. Vấn đề của getter và setter

Nếu giờ, chúng ta cũng muốn kiểm tra giá trị của hai thuộc tính salaryrating. Chúng ta có thể làm tương tự như sau

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Programmer(object):
    def __init__(self, name, age, salary, rating):
        self._age = None # tạo một thuộc tính private cho age
        self._salary = None # tạo một thuộc tính private cho salary
        self._rating = None # tạo một thuộc tính private cho rating

        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if age > 0:
            self._age = value
        else:
            raise ValueError("Negative value not allowed: %s" % age)

    @property
    def salary(self):
        return self._salary

    @age.setter
    def salary(self, value):
        if salary > 0:
            self._salary = value
        else:
            raise ValueError("Negative value not allowed: %s" % age)

    @property
    def rating(self):
        return self._rating

    @age.setter
    def rating(self, value):
        if rating > 0:
            self._rating = value
        else:
            raise ValueError("Negative value not allowed: %s" % age)

Tuy nhiên cách làm này làm cho code của chúng ta có qúa nhiều đoạn code lặp về logic. Đây chính là lúc descriptor có thể sử dụng.

3. Descriptor

Descriptor cho phép chúng ta bind cách xử lý truy cập của một thuộc tính trong class A với một class B khác. Nói cách khác, nó cho phép đưa việc truy cập thuộc tính ra ngoài class. Sau đây là cách cài đặt đối với bài toán của chúng ta

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NonNegativeDescriptor(object):
    def __init__(self, label):
        self.label = label

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.label)

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.label] = value
        else:
            raise ValueError("Negative value not allowed: %s" % age)


class Programmer(object):
    age = NonNegativeDescriptor('age')
    salary = NonNegativeDescriptor('salary')
    rating = NonNegativeDescriptor('rating')

    def __init__(self, name, age, salary, rating):
        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating
python
1
2
3
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> print kiennt.age
>>> kiennt.age = 20

NonNegativeDescriptor là một descriptor vì class này cài đặt 2 phương thức __get____set__. Python nhận ra một class là descriptor nếu như class đó implement một trong 3 phương thức.

  • __get__: Nhận 2 tham số instanceowner. instance là instance của class mà Descriptor được bind tới. owner là class của instance. Trong trường hợp, không có instance nào được gọi, owner sẽ là None.
  • __set__: Nhận 2 tham số instancevalue. instance có ý nghĩa như trong __get__, value là giá trị muốn set cho thuộc tính của instance
  • __delete__: Nhận 1 tham số instance

Trong class Programmer, chúng ta tạo ra 3 Descriptor ở mức class là age, salaryrating. Khi gọi print kiennt.age, python sẽ nhận ra age là một descriptor, nên nó sẽ gọi đến hàm __get__ của descriptor NonNegativeDescriptor.__get__(kiennt, Programmer). Tương tự khi gán giá trị cho kiennt.age = 20, hàm __set__ của descriptor cũng được gọi NonNegativeDescriptor.__set__(kiennt, 20).

Nếu chúng ta gọi Programmer.age, thì hàm __get__ sẽ được gọi với owner = None.

4. Descriptor và Metaclass

Một điểm cần lưu ý đó là trong descriptor, có sử dụng biến label để bind giữa descriptor và thuộc tính của class. Ta có thể sử dụng Metaclass để giải quyết vấn đề này

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class NonNegativeDescriptor(object):
    def __init__(self, label=None):
        self.label = label

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.label)

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

class DescriptorMeta(type):
    def __new__(cls, name, bases, attrs):
        for k, v in attrs.items():
            if isinstance(v, NonNegativeDescriptor):
                v.label = k
        return super(DescriptorMeta, cls).__new__(cls, name, bases, attrs)

class Programmer(object):
    age = NonNegativeDescriptor()
    salary = NonNegativeDescriptor()
    rating = NonNegativeDescriptor()

    def __init__(self, name, age, salary, rating):
        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating

Metaclass hoạt động như thế nào, sẽ được giới thiệu trong bài viết tiếp theo.

Kết luận

Bài viết này giới thiệu với các bạn về descriptor trong Python. Với descriptor, chúng ta có thể chuyển việc can thiệp vào từng thuộc tính của một instance trong class tới việc can thiệp vào thuộc tính ở mức class. Cùng với metaclass, descriptor được sử dụng như một ma thuật đen (black magic) trong metaprogramming. Descriptor được sử dụng rất nhiều khi xây dựng các bộ thư viện về ORM (django ORM, peewee, redisco)

Tham khảo

Copyright © 2015 kỹ thuật máy tính