在 Python 中实现双向关联关系
Implementing a bidirectional association relationship in Python
在 Martin Fowler 的 UML Distilled 中,在“双向关联”部分,他说:
Implementing a bidirectional association in a programming language is often a little tricky because
you have to be sure that both properties are kept synchronized. Using C#, I use code along these
lines to implement a bidirectional association:
书中的代码
class Car...
public Person Owner {
get {return _owner;}
set {
if (_owner != null) _owner.friendCars().Remove(this);
_owner = value;
if (_owner != null) _owner.friendCars().Add(this);
}
}
private Person _owner;
...
class Person ...
public IList Cars {
get {return ArrayList.ReadOnly(_cars);}
}
public void AddCar(Car arg) {
arg.Owner = this;
}
private IList _cars = new ArrayList();
internal IList friendCars() {
//should only be used by Car.Owner
return _cars;
}
....
问题 1:
我尝试在 python 中实现这个(get_cars()
在 Person
&& get_owner_v2()
在 Car 中),我想知道我的代码是否可以用来描述一个'bidirectional association' 还是不行,如果不行,应该怎么修改才行?
注意:第一个版本(检查调用者的 class/object)工作正常,直到我开始独立创建汽车对象并通过两个步骤将它们分配给所有者(最后 4 个打印语句证明了这一点)。第二个版本使用许可证号 lno
来确定所有者。不确定我是否做对了,但根据我的理解,它确实起作用了。
我的实现:
#trying the inverse-bidirectional association
class Person:
cars_and_lnos = []
def __init__(self,name):
self.__cars = []
self.__cars.append("Dummy")
self.__name = name
def add_car(self, *args, obj = None):
# import inspect
if not obj:
car_object = Car(*args)
else:
car_object = obj
Person.cars_and_lnos.append((car_object,self.__name))
self.__cars.append(car_object)
def __repr__(self):
return f"{self.__name}"
def get_cars(self):
return self.__cars
class Car:
car_count = 0
def __init__(self, lno, price ,year, make, model):
import inspect
self.__lno = lno
self.__price = price
self.__year = year
self.__make = make
self.__model = model
Car.car_count += 1
self.__car_id = Car.car_count
if "self" in inspect.getargvalues(inspect.stack()[1][0]).args :
self.__owned_by = f"Car (ID:{self.__car_id}) is Owned By: {inspect.stack()[1][0].f_locals['self']}, which is an instance of Class: {inspect.stack()[1][0].f_locals['self'].__class__.__name__}"
else:
self.__owned_by = "This car is not owned by anyone."
def __repr__(self):
return f"Car ID: {self.__car_id}."
def get_specs(self):
print(f"+{'-'*30}+")
print(f"""
Liscense No.: {self.__lno}
Price: {self.__price}
Year: {self.__year}
Make: {self.__make}
Model: {self.__model}
""")
@property
def get_lno(self):
return self.__lno
def get_owner_v1(self):
# import inspect
return self.__owned_by
def get_owner_v2(self):
if Person.cars_and_lnos:
for tup in Person.cars_and_lnos:
if self.__lno == tup[0].get_lno:
return f"Car (ID: {self.__car_id}) is owned by: {tup[1]}, he is a: {Person.__name__} Class."
return "[0] This car is not owned by anyone."
else:
return "[1] This car is not owned by anyone."
owner1 = Person("William")
owner1.add_car("4567781",10000,2012,"Toyota","Corrolla")
owner1.add_car("2137813",8000,2010,"Porshe","GT3")
owner1.get_cars()[1].get_owner_v1()
print(f"{owner1} owns {len(owner1.get_cars())-1} Car(s).")
print(owner1.get_cars()[1].get_owner_v1())
print("=====================================================")
owner2 = Person("Defoe")
owner2.add_car("8729120",10000,2012,"Dodge","Challenger")
print(f"{owner2} owns {len(owner2.get_cars())-1} Car(s).")
print(owner2.get_cars()[1].get_owner_v1())
print("=====================================================")
car1 = Car("7839291",10000,2012,"Chevrolet","Camaro")
car2 = Car("6271531",10000,2012,"Ford","Mustang")
print(car2.get_owner_v1())
print("=====================================================")
owner3 = Person("Lyan")
owner3.add_car("656721",9000,2013,"Toyota", "Camry")
owner3.add_car("652901",9000,2013,"Nissan", "Sunny")
owner3.add_car("870251",9000,2013,"BMW", "6 Series")
print(owner3.get_cars()[1].get_owner_v2())
print(owner2.get_cars()[1].get_owner_v2())
print(owner1.get_cars()[1].get_owner_v2())
print("=====================================================")
car3 = Car("5424201",10000,2012,"Volks","Eos")
print(car3.get_owner_v1())
print(car3.get_owner_v2())
owner4 = Person("Daphne")
owner4.add_car(obj=car3)
print(car3.get_owner_v1())
print(car3.get_owner_v2())
问题二:
在本节中,他说这两个符号可能相同:
下面的符号(没有指定任何箭头)是否也可以认为是双向关系?
编辑:
我知道对 class 图的逻辑改进是 * 到 *(多对多),但我只关心我对 description/design 的实现有多正确,并且如何改进,以及无箭头关联线在图片中的位置。
Q2:是的。没有箭头表示未指定。这意味着可导航。聚丙烯。 UML 2.5 的 18:
Arrow notation is used to denote association end navigability. By definition, all class-owned association ends are navigable. By convention, all association-owned ends in the metamodel are not navigable.
An association with neither end marked by navigability arrows means that the association is navigable in both directions.
第2题是。因此,在关注问题 1 之前,我将仅提供一些有关适航性的补充信息。
什么是适航性?
导航性是在 UML 规范中以非常广泛的术语定义的:
Navigability means that instances participating in links at runtime (instances of an Association) can be accessed efficiently from instances at the other ends of the Association. The precise mechanism by which such efficient access is achieved is implementation specific. If an end is not navigable, access from the other ends may or may not be possible, and if it is, it might not be efficient.
导航性表达了对实现的一些承诺或约束。因此,在早期阶段通常不指定适航性。在后面的阶段,您将明确显示必须支持的导航路径。但它很少系统地完成,而且很少看到 non-navigability(X 而不是箭头)。
结论:当你没有看到箭头和 X 时,你无法得出任何结论,除非你的团队定义了一些更精确的约定。
它是如何实现的?
关联可以通过多种方式实现。一个典型的实现是在 class A 的对象中保留对 class B 的关联对象的一个或多个引用。根据定义,这确保了导航性 A ---> B
因为有一个有效的访问。但是回去的路呢?
如果你什么都不做,对象B就不能轻易找到它关联的对象A。唯一的方法是遍历所有 A 对象以找到它的引用(可能但效率不高)如果引用不是私有的(这将使搜索变得不可能)。所以你有一条不可通航的路返回A x--> B
要使其可双向导航,您可以在 B 对象中保留对关联的 A 对象的引用。这使得导航双向:A <--> B
.
双向导航面临哪些挑战?
这里的主要挑战是,您必须使两个对象中的引用保持同步。
- 如果
Person
A 保留了一个拥有 Car
的 C1、C2 和 C3 的列表,则每个 Car
C1、C2、C3 都会保留对其所有者 A 的引用。
- 如果你把车主C2的车主改成了B,你最好更新车主A的车主列表(去掉C2),和车主B的车主列表(添加C2)。
- 如果 B 将新车 C4 添加到其拥有的汽车列表中,则 C4 的所有者应相应更新。
设计问题:每个class应该很好地封装并使用另一个class的public访问。所以 Owner.AddCar(Car)
和 Car.SetOwner(Owner)
应该暴露在 public 接口中。但是,如果您在某处调用 B.AddCar(C4)
,则 AddCar()
方法会希望调用 C4.SetOwner(B)
以确保一致性。但是 SetOwner()
方法也可以从外部调用,因此我们也希望通过调用 B.AddCar(C4)
来保持同步然后......你将以两个方法的堆栈溢出结束永远互相调用保持同步。
Fowler 的代码 通过 friendCars()
授予 Car
访问 Person
的汽车列表的特权,从而解决了这个问题。这解决了这个问题,但需要 Car
了解 Person
的内部结构并创建不必要的耦合。
你的代码有问题
我不是 Python 的专家,但据我了解,您的代码是不同的:
- 您维护一个 class 变量
cars_and_lnos
,其中包含所有汽车及其所有者的元组。
- 该人在将汽车添加到拥有的汽车时维护此列表。
- 汽车的
__owned_by
仅在汽车制造时更新,之后不会更新。所以它可能在基于 *arg
的添加过程中构建汽车时有效,但在将现有汽车添加到所有者时失败。
- 因此,
get_owner_v1()
失败,因为它基于 __owned_by
。但是 get_owner_v2()
效果很好,因为它在 cars_and_lnos
上迭代(非常低效),即 up-to-date.
简而言之,v1 失败是因为您的代码生成了不一致的对象。
也就是说,您的方法比 Fowler 的方法更有前途:
- 第 1 步:您可以将
cars_and_lnos
移动到单独的 class CarRegistration
,并重写 Car
和 Person
以与 CarRegistration
交互根据精心设计的 api. 更新所有权
- step2: 重构当时工作但效率低下的
CarRegistration
以用 2 个字典替换 cars_and_lnos
以优化搜索。
在 Martin Fowler 的 UML Distilled 中,在“双向关联”部分,他说:
Implementing a bidirectional association in a programming language is often a little tricky because you have to be sure that both properties are kept synchronized. Using C#, I use code along these lines to implement a bidirectional association:
书中的代码
class Car...
public Person Owner {
get {return _owner;}
set {
if (_owner != null) _owner.friendCars().Remove(this);
_owner = value;
if (_owner != null) _owner.friendCars().Add(this);
}
}
private Person _owner;
...
class Person ...
public IList Cars {
get {return ArrayList.ReadOnly(_cars);}
}
public void AddCar(Car arg) {
arg.Owner = this;
}
private IList _cars = new ArrayList();
internal IList friendCars() {
//should only be used by Car.Owner
return _cars;
}
....
问题 1:
我尝试在 python 中实现这个(get_cars()
在 Person
&& get_owner_v2()
在 Car 中),我想知道我的代码是否可以用来描述一个'bidirectional association' 还是不行,如果不行,应该怎么修改才行?
注意:第一个版本(检查调用者的 class/object)工作正常,直到我开始独立创建汽车对象并通过两个步骤将它们分配给所有者(最后 4 个打印语句证明了这一点)。第二个版本使用许可证号 lno
来确定所有者。不确定我是否做对了,但根据我的理解,它确实起作用了。
我的实现:
#trying the inverse-bidirectional association
class Person:
cars_and_lnos = []
def __init__(self,name):
self.__cars = []
self.__cars.append("Dummy")
self.__name = name
def add_car(self, *args, obj = None):
# import inspect
if not obj:
car_object = Car(*args)
else:
car_object = obj
Person.cars_and_lnos.append((car_object,self.__name))
self.__cars.append(car_object)
def __repr__(self):
return f"{self.__name}"
def get_cars(self):
return self.__cars
class Car:
car_count = 0
def __init__(self, lno, price ,year, make, model):
import inspect
self.__lno = lno
self.__price = price
self.__year = year
self.__make = make
self.__model = model
Car.car_count += 1
self.__car_id = Car.car_count
if "self" in inspect.getargvalues(inspect.stack()[1][0]).args :
self.__owned_by = f"Car (ID:{self.__car_id}) is Owned By: {inspect.stack()[1][0].f_locals['self']}, which is an instance of Class: {inspect.stack()[1][0].f_locals['self'].__class__.__name__}"
else:
self.__owned_by = "This car is not owned by anyone."
def __repr__(self):
return f"Car ID: {self.__car_id}."
def get_specs(self):
print(f"+{'-'*30}+")
print(f"""
Liscense No.: {self.__lno}
Price: {self.__price}
Year: {self.__year}
Make: {self.__make}
Model: {self.__model}
""")
@property
def get_lno(self):
return self.__lno
def get_owner_v1(self):
# import inspect
return self.__owned_by
def get_owner_v2(self):
if Person.cars_and_lnos:
for tup in Person.cars_and_lnos:
if self.__lno == tup[0].get_lno:
return f"Car (ID: {self.__car_id}) is owned by: {tup[1]}, he is a: {Person.__name__} Class."
return "[0] This car is not owned by anyone."
else:
return "[1] This car is not owned by anyone."
owner1 = Person("William")
owner1.add_car("4567781",10000,2012,"Toyota","Corrolla")
owner1.add_car("2137813",8000,2010,"Porshe","GT3")
owner1.get_cars()[1].get_owner_v1()
print(f"{owner1} owns {len(owner1.get_cars())-1} Car(s).")
print(owner1.get_cars()[1].get_owner_v1())
print("=====================================================")
owner2 = Person("Defoe")
owner2.add_car("8729120",10000,2012,"Dodge","Challenger")
print(f"{owner2} owns {len(owner2.get_cars())-1} Car(s).")
print(owner2.get_cars()[1].get_owner_v1())
print("=====================================================")
car1 = Car("7839291",10000,2012,"Chevrolet","Camaro")
car2 = Car("6271531",10000,2012,"Ford","Mustang")
print(car2.get_owner_v1())
print("=====================================================")
owner3 = Person("Lyan")
owner3.add_car("656721",9000,2013,"Toyota", "Camry")
owner3.add_car("652901",9000,2013,"Nissan", "Sunny")
owner3.add_car("870251",9000,2013,"BMW", "6 Series")
print(owner3.get_cars()[1].get_owner_v2())
print(owner2.get_cars()[1].get_owner_v2())
print(owner1.get_cars()[1].get_owner_v2())
print("=====================================================")
car3 = Car("5424201",10000,2012,"Volks","Eos")
print(car3.get_owner_v1())
print(car3.get_owner_v2())
owner4 = Person("Daphne")
owner4.add_car(obj=car3)
print(car3.get_owner_v1())
print(car3.get_owner_v2())
问题二:
在本节中,他说这两个符号可能相同:
下面的符号(没有指定任何箭头)是否也可以认为是双向关系?
编辑:
我知道对 class 图的逻辑改进是 * 到 *(多对多),但我只关心我对 description/design 的实现有多正确,并且如何改进,以及无箭头关联线在图片中的位置。
Q2:是的。没有箭头表示未指定。这意味着可导航。聚丙烯。 UML 2.5 的 18:
Arrow notation is used to denote association end navigability. By definition, all class-owned association ends are navigable. By convention, all association-owned ends in the metamodel are not navigable.
An association with neither end marked by navigability arrows means that the association is navigable in both directions.
第2题是
什么是适航性?
导航性是在 UML 规范中以非常广泛的术语定义的:
Navigability means that instances participating in links at runtime (instances of an Association) can be accessed efficiently from instances at the other ends of the Association. The precise mechanism by which such efficient access is achieved is implementation specific. If an end is not navigable, access from the other ends may or may not be possible, and if it is, it might not be efficient.
导航性表达了对实现的一些承诺或约束。因此,在早期阶段通常不指定适航性。在后面的阶段,您将明确显示必须支持的导航路径。但它很少系统地完成,而且很少看到 non-navigability(X 而不是箭头)。
结论:当你没有看到箭头和 X 时,你无法得出任何结论,除非你的团队定义了一些更精确的约定。
它是如何实现的?
关联可以通过多种方式实现。一个典型的实现是在 class A 的对象中保留对 class B 的关联对象的一个或多个引用。根据定义,这确保了导航性 A ---> B
因为有一个有效的访问。但是回去的路呢?
如果你什么都不做,对象B就不能轻易找到它关联的对象A。唯一的方法是遍历所有 A 对象以找到它的引用(可能但效率不高)如果引用不是私有的(这将使搜索变得不可能)。所以你有一条不可通航的路返回A x--> B
要使其可双向导航,您可以在 B 对象中保留对关联的 A 对象的引用。这使得导航双向:A <--> B
.
双向导航面临哪些挑战?
这里的主要挑战是,您必须使两个对象中的引用保持同步。
- 如果
Person
A 保留了一个拥有Car
的 C1、C2 和 C3 的列表,则每个Car
C1、C2、C3 都会保留对其所有者 A 的引用。 - 如果你把车主C2的车主改成了B,你最好更新车主A的车主列表(去掉C2),和车主B的车主列表(添加C2)。
- 如果 B 将新车 C4 添加到其拥有的汽车列表中,则 C4 的所有者应相应更新。
设计问题:每个class应该很好地封装并使用另一个class的public访问。所以 Owner.AddCar(Car)
和 Car.SetOwner(Owner)
应该暴露在 public 接口中。但是,如果您在某处调用 B.AddCar(C4)
,则 AddCar()
方法会希望调用 C4.SetOwner(B)
以确保一致性。但是 SetOwner()
方法也可以从外部调用,因此我们也希望通过调用 B.AddCar(C4)
来保持同步然后......你将以两个方法的堆栈溢出结束永远互相调用保持同步。
Fowler 的代码 通过 friendCars()
授予 Car
访问 Person
的汽车列表的特权,从而解决了这个问题。这解决了这个问题,但需要 Car
了解 Person
的内部结构并创建不必要的耦合。
你的代码有问题
我不是 Python 的专家,但据我了解,您的代码是不同的:
- 您维护一个 class 变量
cars_and_lnos
,其中包含所有汽车及其所有者的元组。 - 该人在将汽车添加到拥有的汽车时维护此列表。
- 汽车的
__owned_by
仅在汽车制造时更新,之后不会更新。所以它可能在基于*arg
的添加过程中构建汽车时有效,但在将现有汽车添加到所有者时失败。 - 因此,
get_owner_v1()
失败,因为它基于__owned_by
。但是get_owner_v2()
效果很好,因为它在cars_and_lnos
上迭代(非常低效),即 up-to-date.
简而言之,v1 失败是因为您的代码生成了不一致的对象。
也就是说,您的方法比 Fowler 的方法更有前途:
- 第 1 步:您可以将
cars_and_lnos
移动到单独的 classCarRegistration
,并重写Car
和Person
以与CarRegistration
交互根据精心设计的 api. 更新所有权
- step2: 重构当时工作但效率低下的
CarRegistration
以用 2 个字典替换cars_and_lnos
以优化搜索。